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:persona-plan-marshall-agent 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
Domain-owned OpenRewrite log-line finding parser for the java-cui domain — parses the
development
Domain-owned OpenRewrite marker detection for the java-cui domain — scans Java/Kotlin sources for cui-rewrite TODO markers, categorizes them by recipe, and fails the gate on any detected marker
development
Operator control surface for the marshalld build server — enrol/drop a project in the machine-global registry (the opt-in enable signal and anti-laundering wall), manage the daemon lifecycle (start, stop, drain, status, install, upgrade) version-pinned to the verified bundle copy, and inspect the daemon's per-project interaction-audit log (read-only)
tools
The tiny build-consumption client for the marshalld build server — submit a build job, bounded long-poll for its result, ping the daemon identity, and preflight registry-plus-liveness in one call; consumption only, never provisioning or enrolment