ipman v2.1 is purely additive. The JSON protocol is unchanged from v2.0: same envelope, same operations, same fields. What v2.1 adds is a thin CLI veneer over that protocol so a supervising human can run the most common transitions without hand-rolling JSON.
If your code talks to ipman through the JSON envelope, nothing in this release changes your wire. Skip to No-goals and Migration patterns if you want to know what ipman deliberately did not do — and how the new CLI maps onto the v2 ops you already use.
| Group | Verb | Serializes to |
|---|---|---|
| Context | ipman --activate <plan> |
plan.activate |
| Context | ipman --current <task-or-phase> |
task.set_current or phase.set_current (auto-detected) |
| Read view | ipman -N / ipman --next |
composite read of workspace.context_get + plan.get + phase.get + task.get + instruction.list (×3 scopes) + task.list |
| Lifecycle | ipman --start <task> |
task.transition (status: in_progress) |
| Lifecycle | ipman --close <task> … |
task.close (with optional structured evidence and git auto-capture) |
| Lifecycle | ipman --cancel <task> … |
task.cancel |
| Lifecycle | ipman --defer <task> … |
task.defer |
| Modifier | --dry-run |
prints the would-be JSON envelope to stdout and exits without touching the DB |
Every verb resolves selectors the same way: numeric id, uid (e.g. task_42), or label (resolved against the active plan).
Each subsection shows the shell form on the left and the exact JSON envelope ipman would send on the right (captured via --dry-run). The point is: there is no parallel mutation path. The CLI verb produces the same JSON your agent already uses.
ipman --activate ship-login-refactor{"op":"plan.activate","params":{"id":1}}--activate accepts id, code, uid, or label and resolves to the plan's id before dispatch. Same as the JSON op accepts directly.
ipman --current move-jwt-verification{"op":"task.set_current","params":{"id":42}}ipman --current auth-rewrite{"op":"phase.set_current","params":{"id":3}}The verb auto-detects whether the selector points to a task or a phase (the resolver rejects plans — use --activate for plans).
ipman --nextThis runs no mutation. It calls workspace.context_get to find the active plan and cursors, fetches the plan/phase/task records, lists instructions at every scope (workspace, plan, phase), and lists pending tasks. The output is a stack of box-drawn tables. Use it as the session entry-point — one keystroke replaces six JSON envelopes.
ipman --start review-pr-42{"op":"task.transition","params":{"id":42,"status":"in_progress"}}The minimal form:
ipman --close review-pr-42 --summary "Approved" --comment "LGTM"{"op":"task.close","params":{
"id":42,
"outcome_summary":"Approved",
"closing_comment":"LGTM",
"commit_sha":"791a7a0…",
"dirty":false,
"files_changed":["README.md"]
}}When the CLI runs inside a git work tree, it shells out to git rev-parse HEAD, git status --porcelain, and git diff --name-only HEAD~1 and attaches the results as commit_sha, dirty, and files_changed. Outside a repo, those fields are silently absent. To override or suppress:
| Flag | Effect |
|---|---|
--commit <sha> |
Override commit_sha (works outside a repo too) |
--files a,b,c |
Override files_changed with a comma-separated list |
--no-git |
Skip auto-capture entirely; no git fields sent |
The override flags exist so a closure record can attribute work to a specific commit even when the closure happens later or from a different checkout.
ipman --close review-pr-42 \
--summary "Approved with two small tweaks" \
--comment "Caller updated the cache key" \
--validation "make test:passed" \
--validation "shellcheck scripts/:passed" \
--decision "Defer the rename to a follow-up — out of scope here"{"op":"task.close","params":{
"id":42,
"outcome_summary":"Approved with two small tweaks",
"closing_comment":"Caller updated the cache key",
"validations_run":[
{"cmd":"make test","status":"passed"},
{"cmd":"shellcheck scripts/","status":"passed"}
],
"decisions":[
"Defer the rename to a follow-up — out of scope here"
]
}}Both flags are repeatable. --validation splits on the last colon, so commands containing colons (URLs, paths) parse correctly: --validation "curl https://example.com:200" becomes {"cmd":"curl https://example.com","status":"200"}. The passed/failed convention is documentation, not enforcement — status is any non-empty string.
validations_run and decisions are surfaced by closure.get and rendered in the per-plan markdown (ipman --render) so future agents resume with the evidence intact, not just the prose.
ipman --cancel migrate-redis-cluster \
--summary "Out of scope after planning review" \
--comment "Deprioritized; tracked in the next plan"{"op":"task.cancel","params":{
"id":17,
"resolution":"canceled",
"outcome_summary":"Out of scope after planning review",
"closing_comment":"Deprioritized; tracked in the next plan"
}}task.cancel writes resolution=canceled; task.close writes resolution=completed. Use the right one — they mean different things in the audit trail.
ipman --defer migrate-redis-cluster \
--reason-text "Blocked on infra ticket INFRA-2031" \
--reason-code external_dependency{"op":"task.defer","params":{
"id":17,
"reason_text":"Blocked on infra ticket INFRA-2031",
"reason_code":"external_dependency"
}}deferred is not a terminal state. Resume the task later with --start (which transitions back to in_progress).
--dry-run is composable with every write verb. It prints the JSON envelope that would be sent to stdout, then exits with code 0. No DB mutation, no event, no closure record:
ipman --close review-pr-42 \
--summary "..." --comment "..." --validation "make test:passed" --dry-runUse it to script-review the wire before sending it, to teach the mapping to a human reviewer, or to capture a request shape for testing.
The point of v2.1 is to make supervision faster without changing the data model the agents have been using since v2.0. These were considered and explicitly rejected:
Selectors on the wire stay id-only. uid and label are accepted on the CLI as a convenience — the resolver translates them to id before dispatch — but the JSON envelope never carries them. This preserves the v2.0 invariant that names cannot silently mutate under a request (see v2-migration.md for the full rationale).
The cost: a CLI selector can be ambiguous (the same label may exist in a different plan than the one currently active). The CLI handles that with explicit error messages; the protocol stays unambiguous.
Every write op still takes an explicit id. There is no "close whatever the cursor points at" shortcut, on the wire or in the CLI. The reasoning: cursor-implicit mutations are convenient until two agents share a workspace, at which point a misaligned cursor silently writes to the wrong task. v2.1 keeps the cursor a navigation concept, not a mutation target.
If you want the convenience locally, you already have it: ipman --close $(ipman --next | grep current-task | awk …) is a shell composition, not a protocol primitive.
ipman doesn't shell out to run your commands. --validation "make test:passed" records that you ran make test and it passed; ipman never runs make test for you. The only shell-outs ipman makes are the three read-only git invocations that back --close auto-capture, and they're suppressible with --no-git.
This is a deliberate refusal. Once a planner runs commands, it owns their failure modes (timeouts, env, secrets, exit-code semantics, retry policy). v2.1 stays a planner.
4. No "soft delete" or hidden state
Closed, canceled, deferred — every terminal transition writes a closure_record. There is no flag to suppress evidence capture, no way to write "I closed this without saying why". task.close and task.cancel require outcome_summary and closing_comment; the CLI flags are required for the same reason.
If you've been hand-rolling JSON envelopes against v2.0, you don't have to change anything. The CLI is an alternative, not a replacement. But here's what each pattern collapses into when you adopt the new surface.
- echo '{"protocol_version":2,"request_id":"r1","actor":"agent",
- "op":"task.transition",
- "params":{"id":42,"status":"in_progress"}}' | ipman
+ ipman --start 42If you prefer a label:
- # First lookup the id, then transition
- echo '{"op":"task.lookup","params":{"label":"review-pr-42","plan_id":1}}' | ipman
- echo '{"op":"task.transition","params":{"id":42,"status":"in_progress"}}' | ipman
+ ipman --start review-pr-42 # resolved against the active planThe hand-rolled v2.0 form was already verbose; v2.1 collapses the boilerplate:
- SHA=$(git rev-parse HEAD)
- DIRTY=$(git status --porcelain | grep -q . && echo true || echo false)
- echo "{\"op\":\"task.close\",\"params\":{
- \"id\":42,
- \"outcome_summary\":\"Approved\",
- \"closing_comment\":\"LGTM\",
- \"commit_sha\":\"$SHA\",
- \"dirty\":$DIRTY
- }}" | ipman
+ ipman --close 42 --summary "Approved" --comment "LGTM"The CLI captures the same git fields automatically when run inside a work tree.
- echo '{"op":"task.close","params":{
- "id":42,
- "outcome_summary":"...","closing_comment":"...",
- "validations_run":[
- {"cmd":"make test","status":"passed"},
- {"cmd":"shellcheck scripts/","status":"passed"}
- ],
- "decisions":["Defer the rename to a follow-up"]
- }}' | ipman
+ ipman --close 42 --summary "..." --comment "..." \
+ --validation "make test:passed" \
+ --validation "shellcheck scripts/:passed" \
+ --decision "Defer the rename to a follow-up"- echo '{"op":"plan.activate","params":{"id":1}}' | ipman
- echo '{"op":"phase.set_current","params":{"id":3}}' | ipman
- echo '{"op":"task.set_current","params":{"id":42}}' | ipman
+ ipman --activate 1
+ ipman --current 3 # auto-resolves to phase
+ ipman --current 42 # auto-resolves to task- echo '{"op":"workspace.context_get","params":{}}' | ipman
- echo '{"op":"plan.get","params":{"id":1}}' | ipman
- echo '{"op":"phase.get","params":{"id":3}}' | ipman
- echo '{"op":"task.get","params":{"id":42}}' | ipman
- echo '{"op":"instruction.list","params":{"scope":"workspace"}}' | ipman
- echo '{"op":"instruction.list","params":{"plan_id":1}}' | ipman
- echo '{"op":"instruction.list","params":{"phase_id":3}}' | ipman
- echo '{"op":"task.list","params":{"plan_id":1,"status":"todo"}}' | ipman
+ ipman --next--next runs the same ops; the CLI just composes them and renders the result as tables.
- Protocol version: still
2. Noprotocol_version: 3exists. - Operation set: unchanged by v2.1. Current builds expose the operation count reported by
.ipman/manifest.json; v2.1 itself added zero protocol ops and existing ops gained two optional fields each (see migrations 0002 and 0003). - Selector semantics on the wire:
id-only on every non-lookup op. - Closure record fields:
outcome_summary,closing_comment,lessons_learned,open_items_summary,followup_needed— all unchanged. v2.1 addedvalidations_runanddecisionsas siblings, never replacing them. - Error codes and exit-code split:
validation_failed,not_found,conflictkeep their v2.0 semantics. - Storage layout:
.ipman/directory and SQLCipher-encryptedipman.dbare unchanged. Workspaces created on v2.0 work on v2.1 after the schema migrations apply on first open.
If you have an existing v2.0 client, it keeps working. The only thing v2.1 requires is the migrations 0002 (extend closures with git fields) and 0003 (extend closures with structured evidence) applying on first open — both are append-only column additions and don't break readers.