marketplace/bundles/plan-marshall/skills/plan-doctor/SKILL.md
Diagnose plan artifacts (TASK-*.json) for unresolved lesson-ID references and other plan-level data integrity defects
npx skillsauth add cuioss/plan-marshall plan-doctorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Role: Post-hoc diagnostic for plan artifacts. Walks TASK-*.json files for one or all plans, scans title and description for lesson-ID-shaped tokens, verifies each token against the live manage-lessons inventory, and emits structured findings for any tokens that resolve to non-existent lessons.
In addition to the TASK-level lesson-ID sweep, scan --all also runs three plan-directory-shape diagnostics:
orphan-plan-directory — a subdirectory under .plan/local/plans/ that lacks status.json, or has status.json but none of request.md / references.json / solution_outline.md.stuck-low-confidence-archive — a subdirectory under .plan/local/archived-plans/ whose status.json has metadata.confidence < 95 (or the project-configured threshold), every phase after 2-refine is pending, and metadata.archived_reason is absent.dangling-worktree — a subdirectory under .plan/local/worktrees/ whose corresponding .plan/local/plans/{name}/ directory does not exist.This skill complements the at-write-time validation in manage-tasks (which prevents new bad references) by sweeping plans that may already contain stale or phantom lesson-ID references introduced before the at-write check existed (or via direct file edits that bypassed manage-tasks).
Execution mode: Select scan or scan-task-file based on inputs and execute immediately.
Prohibited actions:
scan_lesson_id_tokens and verify_lesson_ids_exist from tools-input-validation (single source of truth — no duplication)LessonInventoryUnavailable / LessonRegexAnchoringError failures so live-anchor discipline is enforcedmanage-findings qgate add so they participate in the standard triage loopConstraints:
plan-marshall:dev-agent-behavior-rules rules (one Bash command per call, no shell constructs, no improvisation)5-execute so they surface in the Phase 5 triage path; the script also prints a TOON summary to stdout1) when findings_count > 0 so CI / orchestrators can fail fast on regressionsScript: plan-marshall:plan-doctor:plan_doctor
Scan TASK-*.json files for one plan or for every plan under .plan/local/plans/.
# Single plan
python3 .plan/execute-script.py plan-marshall:plan-doctor:plan_doctor scan \
--plan-id EXAMPLE-PLAN
# Every plan in the inventory
python3 .plan/execute-script.py plan-marshall:plan-doctor:plan_doctor scan --all
Parameters:
--plan-id (mutually exclusive with --all): Scan a single plan--all (mutually exclusive with --plan-id): Scan every plan under .plan/local/plans/--no-emit (optional): Skip emitting findings to the Q-Gate store; only print the TOON summary. Useful for read-only audits.Behavior:
title + description for lesson-ID-shaped tokens via scan_lesson_id_tokensmanage-lessons list inventory via verify_lesson_ids_existrule: phantom_lesson_id)--all is passed, additionally sweeps the three plan-directory-shape rules described below (orphan_plan_directory, stuck_low_confidence_archive, dangling_worktree); these rules do NOT run on a single-plan --plan-id callsource: qgate, type: bug, severity: warning for orphan/dangling/phantom; severity: info for stuck-low-confidence; phase: 5-execute) unless --no-emit is passedplan_id does not have a live .plan/local/plans/{plan_id}/ directory (dangling-worktree and stuck-low-confidence-archive cases) are emitted in the TOON payload but NOT written to a Q-Gate store — there is no destination plan-dir to receive them1 when any findings are produced; 0 otherwise--all only)orphan-plan-directory (severity: warning)Triggers when a subdirectory of .plan/local/plans/ either:
status.json, ORstatus.json but none of request.md, references.json, solution_outline.md exist.The finding carries a remediation field with one of three values:
rm_rf — no logs/ content; the partial init produced nothing worth keeping.archive_with_reason — logs/ has content; archive with manage-status archive --reason orphan-init-incomplete (see D2 — manage-status archive --reason).operator_review — status.json claims current_phase: 6-finalize but artifacts are absent; needs operator decision (likely "stuck finalize on a shell" from a parallel session).stuck-low-confidence-archive (severity: info)Triggers when an archived plan (subdirectory of .plan/local/archived-plans/) meets ALL of:
status.json has every phase after 2-refine in status: pending.status.metadata.confidence is a number strictly less than 95 (the default threshold).status.metadata.archived_reason is missing or empty.This is advisory only — the archive is a record of an operator decision. The finding carries confidence and threshold fields so the operator can decide whether to (a) restore + raise the threshold or (b) annotate the archive with archived_reason. Do NOT auto-remediate.
dangling-worktree (severity: warning)Triggers when a subdirectory of .plan/local/worktrees/ does not have a corresponding .plan/local/plans/{name}/ directory. The likely cause is a cleanup race or a failed git worktree remove on a prior finalize. Operator should inspect the worktree for uncommitted work, then remove it.
Scan a single explicit TASK-*.json file. Useful for ad-hoc validation when a plan-id is not convenient (e.g., scratch files in .plan/temp/).
python3 .plan/execute-script.py plan-marshall:plan-doctor:plan_doctor scan-task-file \
--plan-id EXAMPLE-PLAN \
--task-file /absolute/path/to/TASK-001.json
Parameters:
--plan-id (required): Plan identifier the task file belongs to (used as the plan-id in findings; not used to resolve the path)--task-file (required): Absolute or repo-relative path to a single TASK-*.json file--no-emit (optional): Skip emitting findings; only print the TOON summaryBehavior:
scan, but operates on a single file--plan-id so the standard triage loop can pick them upBoth verbs print:
status: success
checked_files: 12
findings_count: 4
findings[4]{plan_id,task_file,line,token,rule,reason,remediation,confidence,threshold}:
EXAMPLE-PLAN,TASK-003.json,1,2099-01-01-00-001,phantom_lesson_id,phantom_lesson_id,,,
EXAMPLE-PLAN,TASK-007.json,1,2099-12-31-23-999,phantom_lesson_id,phantom_lesson_id,,,
doc-revamp,,,,orphan_plan_directory,orphan_plan_directory,operator_review,,
2026-05-18-lesson,,,,stuck_low_confidence_archive,stuck_low_confidence_archive,,47.0,95.0
summary:
plans_scanned: 1
emit_to_qgate: true
The TOON table widens to the union of fields across all rule types — TASK-level rows leave the rule-specific cells empty, and rule-level rows leave the TASK-specific cells empty. The TOON parser surfaces each row as a dict keyed by the column names; missing cells parse as empty strings.
Errors (TOON, exit 1):
status: error
error: lesson_inventory_unavailable
message: "manage-lessons list exited 2: 'boom'"
| Field | Description |
|-------|-------------|
| status | success (always for non-fatal results) or error (fatal — see error table) |
| checked_files | Number of TASK-*.json files actually parsed |
| findings_count | Number of phantom lesson-ID references discovered |
| findings[]{plan_id, task_file, line, token, rule, reason, remediation, confidence, threshold} | Per-finding rows. line is 1 for phantom rows (the file is JSON; per-line attribution is best-effort). Rule-level rows leave TASK-specific cells empty. |
| rule | Identifies the diagnostic: phantom_lesson_id, orphan_plan_directory, stuck_low_confidence_archive, or dangling_worktree. Mirrors reason for backward compatibility. |
| reason | Same value as rule (retained for backward compatibility with the original single-rule contract). |
| remediation | Set only on orphan_plan_directory rows: rm_rf, archive_with_reason, or operator_review. |
| confidence / threshold | Set only on stuck_low_confidence_archive rows. |
| summary.plans_scanned | Distinct plans inspected |
| summary.emit_to_qgate | true when findings were emitted, false for --no-emit |
| Error | Meaning |
|-------|---------|
| invalid_plan_id | --plan-id failed canonical validation (kebab-case) |
| plan_not_found | The plan directory does not exist for --plan-id |
| task_file_not_found | --task-file does not exist on disk |
| task_file_unreadable | The file exists but failed to parse as JSON |
| mutually_exclusive | --plan-id and --all were both supplied |
| lesson_inventory_unavailable | The manage-lessons list subprocess failed — surfaces as a fatal error to enforce live-anchor discipline |
| lesson_regex_anchored_to_drifted_inventory | The canonical LESSON_ID_RE matches none of the live IDs — the regex shape has drifted from reality |
The canonical argparse surface for plan_doctor.py. 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".
python3 .plan/execute-script.py plan-marshall:plan-doctor:plan_doctor scan \
[--plan-id PLAN_ID] [--all] [--no-emit]
python3 .plan/execute-script.py plan-marshall:plan-doctor:plan_doctor scan-task-file \
--plan-id PLAN_ID --task-file TASK_FILE [--no-emit]
plan-marshall:tools-input-validation — Owns scan_lesson_id_tokens and verify_lesson_ids_exist (the single source of truth for lesson-ID detection; this skill never re-implements either function)plan-marshall:manage-findings — Receives the Q-Gate findings (qgate add) so the standard Phase 5 triage loop can resolve them (FIX / SUPPRESS / ACCEPT)plan-marshall:manage-tasks — At-write-time validator (companion to this post-hoc sweep); together they cover both the prevention and detection paths for lesson-ID drift2026-04-29-10-001 (regex/data drift surfacing as silent green) and 2026-05-03-21-002 (live-anchored test fixtures). See standards/check-lesson-id-references.md for the rationale and how to interpret/resolve findings.Read standards/check-lesson-id-references.md
development
The single append-only change-ledger — one worktree_sha-stamped substrate for kind=build and kind=change entries — plus the first-class worktree-sha freshness API
development
Authoring standards for ASCII box diagrams in skill and doc source — box-drawing conventions, right-border alignment, and a deterministic check/fix validator over fenced/literal code blocks in .md and .adoc files
testing
Recipe for verifying and fixing alignment of ASCII box diagrams across .md skill source and .adoc documentation, one deliverable per offending file
development
Pure platform-agnostic terminal-title composition consumed by platform-runtime via PYTHONPATH