cuioss

plan-retrospective

Opt-in plan quality audit — analyzes artifacts, logs, metrics, chat, and invariants; compiles report and seeds lessons

cuioss 5 Updated 2mo ago

Resources

3
GitHub

Install

npx skillscat add cuioss/plan-marshall/plan-retrospective

Install via the SkillsCat registry.

SKILL.md

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_handshake or status.metadata.invariants directly — 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-done in archived mode or user-invocable live mode — only the finalize-step mode emits the handshake tail.
  • Never call manage-lessons add in orchestration context (orchestrated: true). Step 5b routes every proposal to the epic inbox on that branch, and the already_closed deletion 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-report warning as a clean pass. A non-empty sections_dropped MUST 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.md in live modes (overwrite on repeat invocation) and quality-verification-report-audit-{YYYYMMDDTHHMMSSZ}.md in archived mode.

Foundational Practices

Skill: plan-marshall:persona-plan-marshall-agent

Dispatch 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-id provided, invoked by phase-6-finalize → finalize-step mode (emit mark-step-done tail).
  • --plan-id provided, invoked by user or command → user-invocable live mode (no mark-step-done tail).
  • --archived-plan-path provided → archived mode (no mark-step-done tail, 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 live

or for archived mode:

python3 .plan/execute-script.py plan-marshall:plan-retrospective:collect-plan-artifacts \
  run --archived-plan-path {path} --mode archived

Capture 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 durable
work/metrics-accumulator-6-finalize.toon into the phase row only later, and
record-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 the
6-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 until
record-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's
metrics.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 legacy references.modified_files key 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:

  1. Resolve the audited plan's domain. Read the plan's domain from its task metadata — the domain field on the plan's tasks (e.g., plan-marshall-plugin-dev). In live mode, read a representative task via manage-tasks; in archived mode, read the domain from the archived status.metadata / task files. When the plan has no resolvable domain, skip the merge entirely (no domain aspects apply).

  2. List domain-contributed aspects across all extensions:

    python3 .plan/execute-script.py plan-marshall:extension-api:extension_discovery \
      list-retrospective-aspects

    Parse the aspects[] rows from the TOON output. Each row carries aspect, domain, script, reference, description, and order.

  3. Filter by the audited plan's domain. Keep only rows whose domain matches 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 its script notation with run --mode {live|archived} plus the resolution flags, pipe stdout to work/fragment-{aspect}.toon, then register it via collect-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}.toon

    For example, a plan-marshall-plugin-dev plan picks up the wrapper-tangle aspect (pm-plugin-development:plan-marshall-plugin:wrapper-tangle-scan) — the former Surface C of the generic direct-gh-glab-usage aspect, 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}.toon

LLM 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}.toon

Aspect 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.toon

Skip 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.toon

Skip 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.md

The 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-stalled

Re-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) or plans_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 naming store_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_id appears in stalled_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_id appears in duplicate_lessons[] — a carried lesson id ALREADY exists in the active corpus, so restore-from-plan would fail with destination_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_id appears in unclassifiable_plans[] — the plan carries a lesson-*.md but its status.json could 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's reason, 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 readable status.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: present and 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: false

On "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 retrospective

On 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: K

On 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 of status.metadata for 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.