verify-agent-lifecycle
Formally verify the subagent lifecycle before or after changing it. Use when editing internal/subagent/orchestrator.go's Spawn, trackAgent, forwardAgentEvents, terminalStatus, Cancel, ShutdownWithTimeout or evictCompletedAgentsLocked, or internal/subagent/pool.go — or when a canceled agent is reported as failed, an agent vanishes from the list with no verdict, the orchestrator panics on a pool release, or an agent can be canceled twice.
- License 许可
- MIT
- 所属仓库
- dimetron/pi-go
- 最近更新
- 2026-11-12
- Stars
- ★ 224
- Forks
- 34
01 前言 Front Matter
- name
- verify-agent-lifecycle
- description
- Formally verify the subagent lifecycle before or after changing it. Use when editing internal/subagent/orchestrator.go's Spawn, trackAgent, forwardAgentEvents, terminalStatus, Cancel, ShutdownWithTimeout or evictCompletedAgentsLocked, or internal/subagent/pool.go — or when a canceled agent is reported as failed, an agent vanishes from the list with no verdict, the orchestrator panics on a pool release, or an agent can be canceled twice.
03 SKILL.md 正文
Verify the subagent lifecycle
One subagent has six states, three writers, and a pool slot that must be released
exactly once. pool.Release is an unguarded channel receive
(internal/subagent/pool.go:36-38), so a second release panics the process, and
nothing in a single-agent test can produce the interleaving that does it.
The properties: a cancel is never overwritten by the process's own exit reason, an agent that leaves the list has already published its verdict, and exactly one pool slot is released per spawn.
Does my change touch the verified code
git diff --stat <sync-commit>..HEAD -- \
internal/subagent/orchestrator.go internal/subagent/pool.go internal/subagent/types.go
Model covers (each model step names the function it stands for):
| Model action | Code |
|---|---|
SlotAcquired |
Spawn past pool.Acquire, orchestrator.go:487-499 |
SpawnRefused |
trackAgent refuses after shutdown → abandonSpawn, orchestrator.go:499-505, 599-607 |
Event / ProcExits |
the event loop and proc.Wait, orchestrator.go:610-629 |
Cancel |
Cancel + validateAgentForCancel, orchestrator.go:806-834 |
ShutdownRunning / ShutdownIdle |
ShutdownWithTimeout, orchestrator.go:859-894 |
PublishKept / PublishEvicted |
forwardAgentEvents' tail: evict, publish, release, orchestrator.go:628-648, 683-711 |
Not modelled, and why: the worktree manager (its own stash lifecycle, modelled
separately if it becomes a target); spawner.go's event channel and its silent
drops when the 64-slot buffer fills; terminalStatus's signal classification
(the model takes the outcome as a parameter); recentTasks and the prune loop;
concurrency above one agent; Get/List as anything other than a read. A bug in
any of those is invisible here.
Run it
hack/formal/check.sh agent # all rules hold, or the first one that breaks
hack/formal/check.sh agent --chart
Cost: 35 distinct states, under a second. Exit 0 all rules hold, 1 a rule
broke, 2 TLC or the environment failed.
Rules, and the harm each one names
| Rule | The promise | What breaking it means for a user |
|---|---|---|
CancelIsSticky |
a cancel request wins over the process exit reason | you press cancel, and pi reports the agent failed instead |
PublishedIsTerminal |
run_done never says running |
a consumer waits forever for a verdict already sent |
EvictionStillPublishes |
an agent missing from the list has published | an agent disappears with no status: neither running nor done |
SlotReleasedOnce |
at most one pool.Release per spawn, and only if acquired |
pool.Release on an empty semaphore panics the process |
NoLateCancel |
a finished agent is never relabeled canceled |
a completed result is reported as canceled |
Two traps, both found while building this model
A rule phrased on the state the bug overwrites cannot see the bug. The first
CancelIsSticky read status = "canceled" => ..., which is trivially true once a
later step overwrites status with "failed". The mutation that drops the
if status == "running" guard at orchestrator.go:626 passed cleanly, on 361
explored states. The fix is the cancelAsked variable: track the request, and
state the rule about what a consumer reads after it. Re-run the mutation after any
edit to this rule.
A mutant that does not compile is not a caught regression. Two attempted
mutations "failed" because they produced a null variable — a TLA+ error, not an
invariant violation. Always read which line fired: it must be
Error: Invariant <name> is violated. A TLC threw an unexpected exception or
Attempted to apply the operator is a broken mutant, and proves nothing.
Also: go run ./hack/formal/lint/main runs first and catches unassigned variables and
conjoined assignments, the two defects that produced false passes in the /goal
and /run models.
After confirming the model is still true
- Sync commit:
cc79aceb43df31012e9e5c1f69a7ba1231c32470 - Constants: none.
MaxCompletedAgentswas modelled as the eviction decision it produces rather than a counter, because a counter made the state space explode past 2 million. - Chart budget: 35 states, so the full chart always draws.
Move the sync commit forward only after confirming the model describes the code at the new commit.
Draw the chart
hack/formal/check.sh agent --chart writes hack/formal/agent/out/report.md and
chart.mmd via the pstack /statechart skill.
04 同仓库更多 Skills
verify-session-flow
Formally verify the interactive session flow before or after changing it. Use when editing internal/tui/tui.go's handleKey, handleInterruptKey, handleInputSubmit, ctrlCCount or quitting, internal/tui/agent_loop.go's enqueuePrompt, submit, steerPrompt, cancelAgent or handleAgentDone, or the deferred-init splash — or when a user reports that Ctrl+C quit on the first press, that Esc did not stop a turn or a goal loop, that a queued prompt was lost when they canceled, or that the TUI hung on a spinner nothing could clear.
verify-run-pipeline
Formally verify the /run phase machine before or after changing it. Use when editing internal/tui/run.go's phase transitions (handleRunAgentDone, runGates result handling, verifyRunComplete, retryRun, reportGateHang, merge), internal/sop/verdict.go's Passed, or the checklist refresh — or when a run merged work it should not have, retried a hung gate, lost a cycle at the retry budget, or merged on a stale PASS.
verify-history-rewrite
Formally verify the session-history rewrite path before or after changing it. Use when editing internal/session/autocompact.go, internal/session/compaction*.go, internal/session/store.go's Compact/ClearEvents/rewriteEvents, internal/agent/agent.go's Run/RunStreaming/runPreTurn, or internal/tui/commands.go's clearConversation/clearSessionContext and the context gauge — or when a turn is canceled mid-flight, a tool call and its result get separated, a session's history disappears while the agent is still working, or the model is sent a tool result whose call was compacted away.
verify-goal-loop
Formally verify the /goal automatic loop before or after changing it. Use when editing internal/tui/goal.go, internal/tui/agent_loop.go's handleAgentDone/cancelAgent/steerPrompt, or internal/tools/goal.go's GoalController — or when a goal loop will not stop, stops one cycle early, closes a goal that is not met, or restarts a goal you finished.
test-new-skill
暂无简介
ponytail
暂无简介