Opt-in plan quality audit — analyzes artifacts, logs, metrics, chat, and invariants; compiles report and seeds lessons
Resources
3Install
npx skillscat add cuioss/plan-marshall/plan-retrospective Install via the SkillsCat registry.
Plan Retrospective Skill
Role: Opt-in plan quality audit. Analyzes a plan's artifacts, logs, metrics, chat history, and invariant outcomes; compiles a quality-verification-report.md (or quality-verification-report-audit-{timestamp}.md in archived mode) and proposes lessons-learned drafts.
Design intent: Python scripts produce deterministic TOON fragments (facts). The orchestrator loads aspect reference docs on-demand so the LLM can synthesize judgement from those facts. Scripts never judge; references never run code.
Enforcement
Execution mode: Select a mode (finalize-step live, user-invocable live, archived) from the Input Contract, dispatch the 14 aspect references in the documented order, compile the report, then record proposals per Step 5b's orchestration branch — to the global lessons store when orchestrated: false, to the epic inbox as kind: candidate-lesson messages when orchestrated: true — and emit the mode-appropriate termination (mark-step-done tail for finalize-step mode only).
Prohibited actions:
- Never re-run invariant capture. Read
status.metadata.phase_handshakeorstatus.metadata.invariantsdirectly — invariants are already captured by phase transitions. - Never write to archived plan directories. Archived mode writes the report next to the archived plan, but the plan state itself is read-only.
- Never call
mark-step-donein archived mode or user-invocable live mode — only the finalize-step mode emits the handshake tail. - Never call
manage-lessons addin orchestration context (orchestrated: true). Step 5b routes every proposal to the epic inbox on that branch, and thealready_closeddeletion path does not run — deleting a global lesson is a corpus mutation the orchestrator owns. - Never silently skip aspect dispatch. If a script fails, record the failure in the report under "Script failure analysis" and continue.
- Never treat a
compile-reportwarning as a clean pass. A non-emptysections_droppedMUST be surfaced in the report and carried into the Step 5 lessons proposal — a dropped fragment may have carried a live finding. - Do not modify any .plan/ files directly — all plan state access goes through
manage-*scripts and the scripts in this skill.
Constraints:
- Strictly comply with all rules from
plan-marshall:persona-plan-marshall-agent. - Report filename is
quality-verification-report.mdin live modes (overwrite on repeat invocation) andquality-verification-report-audit-{YYYYMMDDTHHMMSSZ}.mdin archived mode.
Foundational Practices
Skill: plan-marshall:persona-plan-marshall-agentDispatch shape: 9 aspects iterate inside one envelope
This workflow dispatches under --phase phase-6-finalize --role post-run-review as one execution-context-{level} envelope. The post-run-review sub-key bundles retrospective with lessons-capture — both workflows look back at the full plan history and ride the same level. The 8 LLM analytical aspects (metrics, decision/work logs, references vs deliverables, deliverable vs lesson alignment, scope-deviation footprint, behavioural observations, execution-context dispatch audit, chat-history aspect when --session-id is present, lesson-quality audit) iterate in-context inside that single envelope — the orchestrator never spawns N × envelope per aspect. Bundling matches granularity Heuristic 2 (steps share context): every aspect reads the same plan artefacts, runs the same skill loads, and contributes to the same final retrospective document. Per-aspect dispatch would pay 8× envelope cost with no parallelism payoff. See `../extension-api/standards/dispatch-granularity.md` § 3.
Input Contract
| Parameter | Type | Required | Description |
|---|---|---|---|
--plan-id |
string | Conditional | Live plan identifier. Required for finalize-step mode and user-invocable live mode. |
--archived-plan-path |
string | Conditional | Absolute path to an archived plan directory (.plan/archived-plans/{date}-{plan_id}/). Required for archived mode. Mutually exclusive with --plan-id. |
--session-id |
string | No | Optional session identifier. When present, the chat-history aspect is dispatched; otherwise it is skipped. |
--iteration |
integer | No | Finalize-step iteration counter. Forwarded by phase-6-finalize; ignored by user-invocable and archived modes. |
orchestrated |
bool | No | true when this plan was launched from an epic's staged plan spec, so Step 5b routes every proposal to the epic inbox instead of the global lessons store. In finalize-step mode the dispatcher forwards it (resolved once per finalize run at phase-6-finalize/SKILL.md Step 3 item 4b.a0); this body MUST NOT recompute it. |
epic |
string | No | The epic slug when orchestrated is true, the empty string otherwise. Same forwarded-and-must-not-recompute obligation. |
Orchestration-context resolution per mode: in finalize-step mode the dispatcher supplies orchestrated / epic. In user-invocable live mode (a real caller with no dispatcher) this body resolves them itself through the SAME two-call seam — manage-plan-documents request read --plan-id {plan_id} --section source_id, then orchestrator inbox detect --source-id "{source_id}" — never a third detector. Archived mode is out of scope and unchanged: it is a read-only historic audit of an already-landed plan, not part of an orchestrated plan's finalize run, and the epic tree may itself be archived — so archived mode never resolves orchestration context and never writes an inbox message.
Mode resolution:
--plan-idprovided, invoked byphase-6-finalize→ finalize-step mode (emitmark-step-donetail).--plan-idprovided, invoked by user or command → user-invocable live mode (nomark-step-donetail).--archived-plan-pathprovided → archived mode (nomark-step-donetail, timestamped filename).
Mode detection heuristic: when --iteration is present alongside --plan-id, treat as finalize-step mode; otherwise user-invocable live mode.
Workflow
Step 1: Validate Inputs and Resolve Plan Paths
Validate mutual exclusion of --plan-id and --archived-plan-path. Resolve:
- Live modes:
plan_dir = .plan/local/plans/{plan-id}/ - Archived mode:
plan_dir = --archived-plan-path(verify directory exists).
Canonical plan-status read — when this workflow needs to read plan status (current phase, metadata, worktree binding) it MUST use the manage-status script's read subcommand. The supported invocation is:
python3 .plan/execute-script.py plan-marshall:manage-status:manage-status read \
--plan-id {plan_id}Do not extrapolate status get, manage_status get, or manage-status:status — none of those exist. The canonical script notation is the 3-part form plan-marshall:manage-status:manage-status (the third segment matches the on-disk script filename manage-status.py), and the only read verb is read. The full canonical-forms entry for manage-status (covering read, metadata --get --field, transition, get-worktree-path, change-type-heuristic, and friends) lives in `persona-plan-marshall-agent/standards/argument-naming.md` — that table is the regression guard against the invented-verb drift that motivated this entry (see lesson 2026-05-14-00-001). Future maintainers editing this workflow MUST cross-check any new manage-status call against that table before committing.
Capture the runtime session token for downstream metrics:
python3 .plan/execute-script.py plan-marshall:platform-runtime:platform_runtime \
session capture --plan-id {plan_id}On Claude Code the runtime reads the stored session_id. On OpenCode it returns no-op (no platform session id available) — the retrospective proceeds normally.
Log start:
python3 .plan/execute-script.py plan-marshall:manage-logging:manage-logging \
work --plan-id {plan_id} --level INFO --message "[STATUS] (plan-marshall:plan-retrospective) Starting retrospective — mode={mode}"(Skip logging in archived mode — the archived plan's log files are write-frozen.)
Step 2: Collect Plan Artifacts
Script: plan-marshall:plan-retrospective:collect-plan-artifacts. Produces a manifest TOON of all files present in the plan directory, classified by kind (status, references, tasks, logs, metrics, reports). Both live and archived modes are supported via the --mode flag.
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-plan-artifacts \
run --plan-id {plan_id} --mode liveor for archived mode:
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-plan-artifacts \
run --archived-plan-path {path} --mode archivedCapture the manifest TOON for later aspects.
Step 2.5: Reconcile the phase accumulators into metrics.md (live modes only)
The plan-efficiency aspect (aspect 4) reads per-phase token totals from metrics.md.
At this workflow's order: 995 the 6-finalize phase is still open —default:record-metrics (order 998) folds its durablework/metrics-accumulator-6-finalize.toon into the phase row only later, andrecord-metrics cannot move earlier because it must first fold this retrospective's
own spend. So an unreconciled metrics.md renders the 6-finalize row as zero,
and the aspect would read the largest finalize phase — this step's own dispatch spend
plus every earlier finalize step's — as if it did no work. The plan that motivated
this named it R2: the retrospective reads an unclosed accumulator.
Close the accumulator here — do not merely rely on the reader's order. Fold the
open accumulators into metrics.md before aspect 4 reads it by regenerating the
report:
python3 .plan/execute-script.py plan-marshall:manage-metrics:manage-metrics generate \
--plan-id {plan_id}generate reconciles each phase's durable work/metrics-accumulator-{phase}.toon
into its row (backfill-only, explicit-wins) and re-renders metrics.md, so the6-finalize row now carries its accumulated floor rather than zero. It does
not stamp an end_time, so the partiality machinery is left intact: 6-finalize
stays in phases_missing_end_time and is still reported partial untilrecord-metrics performs the authoritative close at order 998. That final close
overwrites the floor with the complete total (the accumulator read is
assign-cumulative, not additive — see manage-metrics/standards/data-format.md), so
this reconcile never double-counts and never pre-empts the close.
Live modes only. Archived mode is a read-only historic audit and MUST NOT write
to the archived plan directory (see Prohibited actions): an already-landed plan'smetrics.md was closed by its own record-metrics at archive time, so skip this
reconcile in archived mode entirely.
Step 3: Dispatch Aspects (in order)
Before dispatching aspects, initialize the fragment bundle. collect-fragments init creates an empty TOON bundle file at the mode-appropriate path: live mode writes to {plan_dir}/work/retro-fragments.toon; archived mode writes to an OS tmp directory so the archived plan stays read-only. Capture the returned bundle_path for use in Step 4. The mode is persisted into the bundle by init, so subsequent register and finalize calls read it back automatically and accept only --plan-id and the fragment inputs.
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \
init --plan-id {plan_id} --mode {live|archived} [--archived-plan-path {path}]Parse bundle_path from the TOON output.
For each aspect below, produce a TOON fragment on disk at work/fragment-{aspect}.toon (live mode) or the tmp equivalent (archived mode), then register it via collect-fragments add. Fragments are persisted to disk so that compile-report in Step 4 can consume them from a single bundle file assembled by collect-fragments.
| Order | Aspect | Script(s) | Reference |
|---|---|---|---|
| 1 | Artifact consistency | check-artifact-consistency |
references/artifact-consistency.md |
| 2 | Log analysis | analyze-logs |
references/log-analysis.md |
| 3 | Invariant outcomes | summarize-invariants |
references/invariant-check-summary.md |
| 4 | Plan efficiency | (LLM on metrics.md + logs) | references/plan-efficiency.md |
| 5 | Request-result alignment | (LLM on request.md + solution_outline.md + logs) | references/request-result-alignment.md |
| 6 | LLM-to-script opportunities | (LLM on logs + scripts) | references/llm-to-script-opportunities.md |
| 7 | Logging gap analysis | (LLM on references + logs) | references/logging-gap-analysis.md |
| 8 | Script failure analysis | script-failure-analysis |
references/script-failure-analysis.md |
| 9 | Permission prompt analysis | (LLM on description or session) | references/permission-prompt-analysis.md |
| 10 | Direct gh/glab usage (Surfaces A+B: plan logs + plan diff) | direct-gh-glab-usage |
references/direct-gh-glab-usage.md |
| 11 | Execution-context dispatch audit (both directions: spawns rode the envelope AND DISPATCHED steps were dispatched) | (LLM on logs + dispatch decisions + phase_steps) | standards/execution-context-dispatch-audit.md |
| 12 | Manifest decisions (conditional) | check-manifest-consistency |
standards/manifest-crosscheck.md |
| 13 | Routing decisions (conditional) | check-routing-decisions |
references/routing-decision-verification.md |
| 14 | Chat history (conditional) | (LLM on session transcript) | references/chat-history-analysis.md |
| 15 | Lessons proposal | (LLM on compiled fragments) | references/lessons-proposal.md |
The Execution-context dispatch audit (aspect 11) consumes the [DISPATCH] work-log lines emitted by every dispatch site per `../ref-workflow-architecture/standards/dispatch-logging.md` — that standard is the authoritative source for the line shape, and this aspect is its enforcement consumer. The aspect asserts dispatch discipline in both directions: that every spawn that DID happen rode the canonical execution-context-{level} envelope, AND that every finalize/execute step classified DISPATCHED actually WAS dispatched. The inverse-coverage half (dispatch_coverage_violation category) cross-references the SKILL's own dispatched/inline classification against the status.metadata.phase_steps["6-finalize"] outcome records — a step marked done with zero matching [DISPATCH] evidence is flagged as inline-where-dispatch-was-required. Report readers tracing an audit finding back to its evidence land in dispatch-logging.md § "Emission contract" for the canonical log-line format.
Coverage contract: the Artifact-consistency aspect (aspect 1) is the declared-vs-achieved coverage comparison — declared in-scope files (
Affected files:) vs the plan's actual footprint — which is the deterministic item-coverage half of the thoroughness dial, graded to the FLOOR. The footprint is resolved through the shared footprint resolver: live worktree diff ({base}...HEAD∪ porcelain) when one is on disk, else the persisted realized-footprint capture, a merge-commit fallback, then the legacyreferences.modified_fileskey for pre-ledger archives. See the two-dial scope × thoroughness contract (ladders, grade-to-the-floor rule, coupling constraint) in `../persona-plan-marshall-agent/standards/thoroughness.md`.
Aspect 12 (manifest decisions) is skipped when execution.toon is absent. When present, the aspect loads the manifest via plan-marshall:manage-execution-manifest:manage-execution-manifest read --plan-id {plan-id} and pairs it with matching (plan-marshall:phase-4-plan:manifest) decision-log entries — manifest = WHAT was decided, decision.log = WHY. The cross-check engine is plan-marshall:plan-retrospective:check-manifest-consistency which evaluates each manifest assumption against the actual end-of-execute diff and emits one finding per violation. See standards/manifest-crosscheck.md for the cross-check matrix.
Aspect 13 (routing decisions) is skipped when execution.toon is absent. When present, it re-evaluates the lane prune predicates against the realized footprint (see Aspect 13 capture pattern below).
Aspect 14 is skipped when --session-id is absent.
Achieved thoroughness: there is no mechanical achieved-thoroughness measurement. The achieved side of coverage is the floor-graded self-report defined in `../persona-plan-marshall-agent/standards/thoroughness.md` § Floor-Graded Self-Report; the Artifact-consistency aspect (aspect 1, above) supplies the deterministic declared-vs-actual footprint (resolved through the shared footprint resolver: live worktree diff, else the realized-footprint capture / merge-commit / legacy key) that the self-report grades against.
Domain-contributed aspects (merged after the fixed table, gated by plan domain):
The fixed aspect table above is domain-invariant — it runs for every plan. Domain bundles may contribute ADDITIONAL deterministic, script-backed aspects via the provides_retrospective_aspects() extension point (see `../extension-api/standards/ext-point-retrospective.md`). Merge them after the fixed table and before compiling the report:
Resolve the audited plan's domain. Read the plan's domain from its task metadata — the
domainfield on the plan's tasks (e.g.,plan-marshall-plugin-dev). In live mode, read a representative task viamanage-tasks; in archived mode, read the domain from the archivedstatus.metadata/ task files. When the plan has no resolvable domain, skip the merge entirely (no domain aspects apply).List domain-contributed aspects across all extensions:
python3 .plan/execute-script.py plan-marshall:extension-api:extension_discovery \ list-retrospective-aspectsParse the
aspects[]rows from the TOON output. Each row carriesaspect,domain,script,reference,description, andorder.Filter by the audited plan's domain. Keep only rows whose
domainmatches the plan's domain. Skip every aspect from a non-matching domain. The remaining aspects are deterministic script-backed fragments — run each exactly like the built-in script-backed aspects (1-3, 8, 10): invoke itsscriptnotation withrun --mode {live|archived}plus the resolution flags, pipe stdout towork/fragment-{aspect}.toon, then register it viacollect-fragments add --aspect {aspect}.python3 .plan/execute-script.py {script} \ run --plan-id {plan_id} --mode {live|archived} > work/fragment-{aspect}.toon python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \ add --plan-id {plan_id} --aspect {aspect} --fragment-file work/fragment-{aspect}.toonFor example, a
plan-marshall-plugin-devplan picks up thewrapper-tangleaspect (pm-plugin-development:plan-marshall-plugin:wrapper-tangle-scan) — the former Surface C of the genericdirect-gh-glab-usageaspect, now homed in pm-plugin-development. Plans of other domains skip it.
Per-aspect capture pattern:
Deterministic aspects (1-3, 8, and 10, script-backed) — pipe the script's stdout to the fragment file, then register it:
The script-failure-analysis script (aspect 8) consumes the plan's script-execution.log directly and classifies non-zero-exit calls by stderr signature (invalid choice: → invented_subcommand; the following arguments are required: → missing_required_flag; unrecognized arguments: → invented_flag; non-argparse exit-1 → script_internal_error). The TOON fragment carries deduped findings[] and seed lessons[] for downstream classification; the orchestrator does NOT inject LLM judgement at this point. See references/script-failure-analysis.md for the finding shape; the LLM aspects that follow may augment the script-emitted findings with source-component tracing.
The analyze-logs script (aspect 2) additionally parses the plan's folded-in global logs (the {prefix}-YYYY-MM-DD.log files folded into <plan_dir>/logs/ at integrate-into-main) and surfaces per-plan operational signals under global_log_signals (error/non-INFO lines, slow calls, fixture leaks). This per-plan view complements the cross-plan global-log-analysis audit check in audit-archived-plan-retrospectives — the two are non-overlapping facets: this aspect surfaces each plan's own phases 5-6 signals from its folded-in copies, while the audit check parses the live .plan/local/logs/ corpus with cross-plan execution-window attribution covering phases 1-4. See references/log-analysis.md § "Folded-in global logs".
python3 .plan/execute-script.py plan-marshall:plan-retrospective:{script} \
run --plan-id {plan_id} --mode {live|archived} > work/fragment-{aspect}.toon
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \
add --plan-id {plan_id} --aspect {name} --fragment-file work/fragment-{aspect}.toonLLM aspects (4-7, 9, 11, and 14) — load the aspect reference via Read, produce the TOON fragment body per the reference's schema, emit it with the Write tool to work/fragment-{aspect}.toon, then register:
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \
add --plan-id {plan_id} --aspect {name} --fragment-file work/fragment-{aspect}.toonAspect 12 (manifest-decisions, conditional) — when execution.toon exists in the plan directory, run the deterministic script pattern with aspect name manifest-decisions:
python3 .plan/execute-script.py plan-marshall:plan-retrospective:check-manifest-consistency \
run --plan-id {plan_id} --mode {live|archived} > work/fragment-manifest-decisions.toon
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \
add --plan-id {plan_id} --aspect manifest-decisions --fragment-file work/fragment-manifest-decisions.toonSkip the aspect entirely when the manifest file is absent.
Aspect 13 (routing-decisions, conditional) — when execution.toon exists, run the deterministic script pattern with aspect name routing-decisions. The script re-evaluates each absent prunable step's prunable_when predicate against the realized footprint and emits the deterministic facts; the LLM judgment (the OVER/UNDER posture counterfactual) is synthesized from those facts per `references/routing-decision-verification.md` — the script never judges. Pass the realized footprint as --diff-file (one path per line) so the prune-predicate re-evaluation has a footprint to test:
python3 .plan/execute-script.py plan-marshall:plan-retrospective:check-routing-decisions \
run --plan-id {plan_id} --mode {live|archived} --diff-file work/footprint.txt > work/fragment-routing-decisions.toon
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \
add --plan-id {plan_id} --aspect routing-decisions --fragment-file work/fragment-routing-decisions.toonSkip the aspect entirely when the manifest file is absent.
Aspect 14 (chat-history, conditional) — the session-transcript channel is target-dependent: only a target that exposes a session transcript can supply the chat history this aspect analyzes. On the Claude target, resolve the absolute transcript path from the canonical Claude Code path pattern ~/.claude/projects/{cwd-slug}/{session_id}.jsonl (where {cwd-slug} is the absolute project cwd with each / replaced by -); read the file directly, and if absent try a parent-directory glob under ~/.claude/projects/ for cross-cwd recovery. Never substitute Bash file discovery (ls, find, Glob) for this resolution — the canonical path derivation is the only sanctioned lookup mechanism for the session JSONL. On a target that exposes no session transcript (e.g. OpenCode, where the metrics enrichment op itself returns transcript_not_found), there is no transcript to resolve: skip the aspect and emit a status: skipped fragment carrying the transcript_unavailable skip-reason token (the same data-absence path Tier 2 below takes).
Run the extract-chat-signal.py signal-extraction pre-pass against the resolved transcript_path to obtain the tier decision and the reduced transcript:
python3 .plan/execute-script.py plan-marshall:plan-retrospective:extract-chat-signal run \
--transcript-path {abs_transcript_path}The pre-pass output drives the two-tier degradation path: when no_signal == false AND over_budget == false (Tier 1), feed reduced_transcript to the LLM analysis prompt and synthesize the status: success fragment; otherwise (Tier 2 — transcript absent, no signal, or still over the 2 MiB read budget), emit a status: skipped fragment carrying the canonical skip-reason token. The two-tier path and the normative skip-reason token contract (transcript_too_large for a size-driven skip vs transcript_unavailable for a genuine data absence, and how downstream aggregation MUST distinguish them) are specified in references/chat-history-analysis.md — see `references/chat-history-analysis.md` §§ "Two-Tier Degradation Path" and "Skip-Reason Token Contract". Do not restate the token semantics here.
Step 4: Compile Report
Finalize the fragment bundle and pass it to compile-report run. collect-fragments finalize prints the bundle_path and the list of registered aspects in its TOON output; use that path as the --fragments-file input to compile-report.
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments \
finalize --plan-id {plan_id}Parse bundle_path from the TOON output, then:
python3 .plan/execute-script.py plan-marshall:plan-retrospective:compile-report \
run --plan-id {plan_id} --mode {live|archived} --fragments-file {bundle_path}The script returns the report's absolute path and a three-valued section outcome. sections_written names every section the report carries. sections_omitted is the BENIGN half of the non-emit path — the section's trigger fragment was absent or carried nothing renderable, so there was nothing to lose. sections_dropped is the LOUD half — a registered fragment that was present and carried payload but still did not render. When sections_dropped is non-empty the script returns status: warning (never success) and a message naming the dropped headings; the process exit code stays 0 because the report itself was written. Section order follows references/report-structure.md.
Cleanup: compile-report run auto-deletes the fragment bundle after a successful report write. On failure paths (before the report is flushed to disk), the bundle is retained so the aspect fragments remain available for debugging.
Step 5: Propose Lessons (optional, interactive)
Load references/lessons-proposal.md. Recording proceeds in two sub-steps: 5a classifies each proposal against the existing corpus, and 5b records only what the classification admits.
Step 5a: Classify (required before any recording)
Load plan-marshall:manage-lessons:references/dedup-analysis.md and classify every proposal into exactly one of three statuses:
| Status | Meaning | Caller action |
|---|---|---|
new |
No existing lesson covers this component + root cause | Proceed to 5b manage-lessons add |
merge_into |
An existing lesson shares component + root cause (target_id recorded) |
Skip add; Edit target lesson file to append ## Recurrence — YYYY-MM-DD ({plan_id}) section |
already_closed |
Existing lesson filed AND code fix has since landed (target_id recorded) |
Skip add; record target_id in report; delete stale lesson file (requires user confirmation in finalize-step mode) |
Dedup-gate enforcement is documented in references/lessons-proposal.md ("Dedup gate (required before recording)" section) and authoritatively specified in dedup-analysis.md. Recording without classification is prohibited.
Step 5b: Record (gated by 5a)
Branch on orchestrated before recording anything.
orchestrated: true — route to the epic inbox. Make zero manage-lessons add calls. Step 5a's dedup classification does NOT run on this branch: the corpus it dedups against is the global store, which is not the destination, and only the orchestrator holds the cross-plan context that classification needs. Every proposal — regardless of the kind of finding that produced it — is emitted as one kind: candidate-lesson inbox message. Stage each payload body with the Write tool first (the same shell-safety reason the global-store flow uses a path-allocate sequence), then write it:
python3 .plan/execute-script.py plan-marshall:plan-orchestrator:orchestrator inbox write \
--slug {epic} --sender-type plan --sender-id {plan_id} --kind candidate-lesson \
--payload-file {plan_dir}/work/inbox-payload.mdThe proposal body composed for the global store is the payload verbatim — no transcoding, which is exactly what the markdown + key=value envelope choice buys. No already_closed deletion happens on this branch: deleting a global lesson is a corpus mutation the orchestrator owns. See `../plan-orchestrator/standards/inbox-envelope.md` for the envelope schema and the kind enum; do not restate them here.
orchestrated: false — unchanged. Every existing path below applies byte for byte: Step 5a dedup classification, the status: new gate, manage-lessons add + Write the body, the merge_into Edit path, and the already_closed deletion path.
Only status: new proposals reach manage-lessons add:
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons add \
--component "plan-marshall:plan-retrospective" --category improvement \
--title "{lesson title}"Parse path from the output and Write the body. references/lessons-proposal.md defines the prompting rules and category choices. merge_into proposals are applied via Edit on the target file; already_closed proposals are surfaced in the report and the stale lesson file at .plan/local/lessons-learned/{target_id}.md is deleted (requires user confirmation in finalize-step mode because deletion is destructive).
In non-interactive finalize-step mode, emit lessons automatically only when confidence is high (documented in the reference). User-invocable mode uses AskUserQuestion for each draft.
Step 5.5: Stalled-lesson detection (detection-and-prompt)
When the audited plan is itself a stalled plan carrying a relocated lesson, that lesson is trapped inside the plan directory and out of the active corpus. Consume the signal — do not re-derive it. manage-lessons list-stalled already scans every plan for exactly this condition, so this step reads its output and looks for the audited plan rather than re-implementing the predicate from status.metadata.plan_source + current_phase in prose:
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons list-stalledRe-deriving the predicate here would be actively wrong, not merely duplicative: any gate on status.metadata.plan_source matching the lesson-id pattern reports clean for precisely the plans whose lesson is trapped, because that field is unset on every convert-to-plan-carried plan. The verb derives its population from the observable lesson-*.md file instead, and consuming the verb is what keeps this step in lockstep with that predicate.
Branch on the returned payload:
status: error(store_unresolved) orplans_root_state: missing— the scan could not look at the store, so this is NOT evidence the plan is healthy. Surface the could-not-look outcome in the report namingstore_resolution/plans_root, and do NOT prompt. Reporting a clean result here would be the same fail-open the verb exists to close.- The audited
plan_idappears installed_plans[]— the plan is stalled with a trapped lesson. Surface the signal and prompt (live modes only — skip the prompt in archived mode, which is read-only). - The audited
plan_idappears induplicate_lessons[]— a carried lesson id ALREADY exists in the active corpus, sorestore-from-planwould fail withdestination_exists. Surface this distinctly from the absence case and do NOT offer "Restore lesson(s)" as an unqualified option; the collision must be resolved first. - The audited
plan_idappears inunclassifiable_plans[]— the plan carries alesson-*.mdbut itsstatus.jsoncould not be read, so it was neither confirmed stalled nor cleared. The lesson may well be trapped; the scan simply could not say. Surface the residual naming the row'sreason, and do NOT prompt — offering "Restore lesson(s)" here would act on a condition that was never established. This branch exists because the population predicate is the presence of the carried file, while the stalled/cleared split needs a readablestatus.json: a row that fails the second test is absent from BOTH lists above, so without this branch it would fall to the clean result below. That is the could-not-look-reports-benign collapse this verb exists to close, at row granularity. The sibling consumer at `plan-marshall/workflow/planning.md` § Action: cleanup Step 4a case (iv) takes the same disposition. - Otherwise —
plans_root_state: presentand the plan is absent from all three lists: a genuine, scanned clean result. Nothing to surface.
AskUserQuestion:
question: "This plan is stalled and carries a relocated lesson ({plan_id}) — its lesson(s) are trapped out of the active corpus. Resume the plan or restore the lesson(s)?"
header: "Stalled lesson"
options:
- label: "Resume plan"
description: "Continue the plan from its current phase ({current_phase}) to a terminal state"
- label: "Restore lesson(s)"
description: "Run restore-from-plan to return the lesson(s) to .plan/local/lessons-learned/"
multiSelect: falseOn "Restore lesson(s)", invoke the inverse of convert-to-plan:
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons restore-from-plan \
--plan-id {plan_id}Branch on its action rather than on status alone: only restored is a completed restore, and restore_incomplete / plan_dir_unresolved must each be reported as the outcome it is — never as "nothing to restore". See `../manage-lessons/SKILL.md` § restore-from-plan, the single home for the four-state contract.
This detection note is the per-plan counterpart of the corpus-wide tooling: list-stalled (see `../manage-lessons/SKILL.md` § list-stalled) is the shared scanner, and the Action: cleanup stalled-lesson restore pass (see `../plan-marshall/workflow/planning.md` § "Action: cleanup" → "Stalled-lesson restore") restores them in bulk. Keep this addition thin — it is a detection-and-prompt note that consumes an existing signal, not a new script-backed aspect in the Step 3 table.
Step 6: Mode-Specific Termination
Before terminating, record phase metrics through the platform runtime:
python3 .plan/execute-script.py plan-marshall:platform-runtime:platform_runtime \
metrics capture --plan-id {plan_id} --phase retrospectiveOn Claude Code the runtime reads the stored session_id and captures token usage from the transcript. On OpenCode it returns no-op — the retrospective still completes and accepts manual --total-tokens when available.
Finalize-step mode: emit the mark-step-done handshake so the phase_steps_complete invariant is satisfied:
python3 .plan/execute-script.py plan-marshall:manage-status:manage-status mark-step-done \
--plan-id {plan_id} --phase 6-finalize --step plan-marshall:plan-retrospective --outcome done \
--display-detail "{N} findings across {M} aspects"When the Step 5b orchestration branch fired, report the inbox route instead — same ≤80-char ASCII no-trailing-period constraint:
python3 .plan/execute-script.py plan-marshall:manage-status:manage-status mark-step-done \
--plan-id {plan_id} --phase 6-finalize --step plan-marshall:plan-retrospective --outcome done \
--display-detail "{N} candidate-lesson(s) -> epic {epic}"User-invocable live mode and archived mode: skip the handshake. Emit only the final TOON contract below.
Step 7: Return Results
status: success
display_detail: "<{aspects} aspects, {lessons_recorded} lessons recorded>"
plan_id: {plan_id or basename of archived_plan_path}
mode: {finalize-step|user-invocable|archived}
report_path: {absolute path to report}
aspects_dispatched: N
lessons_proposed: M
lessons_recorded: KOn error:
status: error
display_detail: "<retrospective error: {code}>"
plan_id: {id}
mode: {mode}
error: {code}
message: {human-readable}Output
Step 7 (above) is the single source of truth for the return TOON. The minimum contract every workflow doc that implements ext-point-execution-context-workflow MUST return is:
status: success | error
display_detail: "<{aspects_dispatched} aspects, {lessons_recorded} lessons recorded>"display_detail shape on success: "{aspects_dispatched} aspects, {lessons_recorded} lessons recorded" (e.g. "8 aspects, 3 lessons recorded"); ≤80 chars, ASCII, no trailing period.
Canonical invocations
The canonical argparse surface for the eleven entry-point scripts this skill registers (thirteen invocation forms — collect-fragments carries three sub-verbs). The plugin-doctor analyzer (_analyze_manage_invocation.py) reads this section as source-of-truth for the manage-invocation-invalid and missing-canonical-block rules. Consuming docs xref this section by name instead of restating the command inline. See `pm-plugin-development:plugin-script-architecture` cross-skill-integration.md § "Script invocation in documentation". The single-aspect scripts share the same run flag surface; collect-fragments carries the init / add / finalize sub-verbs.
extract-chat-signal — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:extract-chat-signal run \
--transcript-path TRANSCRIPT_PATH [--read-budget-bytes READ_BUDGET_BYTES]check-manifest-consistency — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:check-manifest-consistency run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH] \
[--diff-file DIFF_FILE] [--base-ref BASE_REF]--base-ref is required when --diff-file is absent.
check-routing-decisions — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:check-routing-decisions run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH] \
[--diff-file DIFF_FILE]--diff-file carries the realized footprint (one path per line) that the prune-predicate re-evaluation tests; absent → the footprint is recovered through the shared resolver (realized-footprint capture → merge-commit → legacy key), and only a still-unresolvable footprint SKIPs the mis-prune checks.
script-failure-analysis — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:script-failure-analysis run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH]check-artifact-consistency — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:check-artifact-consistency run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH]analyze-logs — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:analyze-logs run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH]summarize-invariants — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:summarize-invariants run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH]collect-plan-artifacts — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-plan-artifacts run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH]compile-report — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:compile-report run \
--mode {live,archived} --fragments-file FRAGMENTS_FILE \
[--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH] [--session-id SESSION_ID]direct-gh-glab-usage — run
python3 .plan/execute-script.py plan-marshall:plan-retrospective:direct-gh-glab-usage run \
--mode {live,archived} [--plan-id PLAN_ID] [--archived-plan-path ARCHIVED_PLAN_PATH] \
[--base BASE] [--project-root PROJECT_ROOT] [--audit-plan-id AUDIT_PLAN_ID]collect-fragments — init
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments init \
--plan-id PLAN_ID --mode {live,archived} [--archived-plan-path ARCHIVED_PLAN_PATH]collect-fragments — add
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments add \
--plan-id PLAN_ID --aspect ASPECT --fragment-file FRAGMENT_FILE \
[--archived-plan-path ARCHIVED_PLAN_PATH] [--overwrite]collect-fragments — finalize
python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-fragments finalize \
--plan-id PLAN_ID [--archived-plan-path ARCHIVED_PLAN_PATH]Related
plan-marshall:phase-6-finalize— optional orchestrator that dispatches this skill when the bundle-opt-in finalize step is enabled.plan-marshall:manage-status— source ofstatus.metadatafor invariant summary.plan-marshall:manage-logging— canonical log reader.plan-marshall:manage-lessons— lessons draft/seed API.plan-marshall:manage-metrics— metrics.md is an input to the plan-efficiency aspect.