marketplace/bundles/plan-marshall/skills/manage-lessons/SKILL.md
Manage lessons learned with global scope
npx skillsauth add cuioss/plan-marshall manage-lessonsInstall 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.
Manage lessons learned with global scope. Stores lessons as markdown files with key=value metadata headers. A lesson's lifecycle state ("unapplied" vs "applied") is encoded by its on-disk location, not by metadata: unapplied lessons live in .plan/local/lessons-learned/{id}.md, and become applied by being moved into a plan directory as .plan/local/plans/{plan_id}/lesson-{id}.md via convert-to-plan.
Base contract: See manage-contract.md for shared enforcement rules, TOON output format, and error response patterns.
Skill-specific constraints:
bug, improvement, anti-pattern--plan-id parameterfrom-error command expects JSON context as --context argumentCanonical flag names (do not invent aliases):
--lesson-id on every verb that targets a single lesson (get, update, set-body, set-title, convert-to-plan, remove, supersede, and the explicit-ids mode of cleanup-superseded). There is no --id flag — the bare id token appears only as an output field and as a metadata header key (see Metadata Fields), never as an input argument. Passing --id is rejected by argparse (exit_code: 2).list is done with --status {active|superseded|removed|all} (default active; use all to include superseded/removed lessons). There is no --include-tombstoned flag — --status all is the canonical way to surface non-active lessons. Tombstones at .tombstones/{id}.json are the audit trail for supersede/remove events and are not listed by any verb; they are never exposed through a list flag.Lessons are stored globally:
.plan/lessons-learned/
2025-12-02-001.md
2025-12-02-002.md
...
Markdown with key=value metadata header:
id=2025-12-02-001
component=maven-build
category=bug
created=2025-12-02
# Build fails with missing dependency
When running a Maven clean install, the build fails with a missing
dependency error for `jakarta.json-api`.
## Solution
Add the dependency explicitly to pom.xml:
```xml
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>
This affects all projects using jakarta.json without explicit dependency.
### Metadata Fields
| Field | Description |
|-------|-------------|
| `id` | Unique identifier (date-sequence). Appears as a metadata header key and in command output; the input flag that selects a lesson by this value is **`--lesson-id`**, not `--id`. |
| `component` | Component that lesson applies to |
| `category` | bug, improvement, anti-pattern |
| `created` | Creation date |
| `bundle` | Optional: bundle that the lesson relates to (e.g., `pm-dev-java`). Used for filtering when applying lessons to specific bundles. |
---
## Operations
Script: `plan-marshall:manage-lessons:manage-lessons`
### add
Allocate a new lesson file with metadata header and title (empty body). The call returns the absolute path of the created file; the caller then populates the body via `set-body` (canonical form, see below) — typically by writing a body file under `{plan_dir}/work/lesson-body-{id}.md` and passing it via `--file`.
```bash
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons add \
--component maven-build \
--category bug \
--title "Build fails with missing dependency" \
[--bundle planning]
Parameters:
--component (required): Component that lesson applies to--category (required): bug, improvement, or anti-pattern--title (required): Lesson title--bundle: Optional bundle referenceOutput (TOON):
status: success
id: 2025-12-02-001
path: /abs/path/to/.plan/local/lessons-learned/2025-12-02-001.md
component: maven-build
category: bug
Populate (or replace) the body of an existing lesson. This is the canonical form for writing lesson bodies. Two mutually exclusive input modes are supported: --file PATH (preferred, shell-safe for arbitrary markdown) and --content STRING (secondary form, suitable only for tiny single-line payloads).
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons set-body \
--lesson-id 2025-12-02-001 \
--file /abs/path/to/.plan/local/plans/{plan_id}/work/lesson-body-2025-12-02-001.md
Parameters:
--lesson-id (required): Lesson ID whose body to set--file (preferred): Absolute path to a markdown file containing the body. Use this for any non-trivial content — sections with ## headings, code fences, multi-paragraph prose — because the body never passes through a shell argument.--content (secondary, tiny payloads only): Inline string body. Use only for single-line or very short content; any payload containing newlines, backticks, quotes, or shell metacharacters MUST use --file instead.--file and --content are mutually exclusive — exactly one must be provided.
Output (TOON):
status: success
id: 2025-12-02-001
path: /abs/path/to/.plan/local/lessons-learned/2025-12-02-001.md
body_bytes_written: 1234
Rewrite the H1 title of an existing lesson file in place. The metadata header (key=value frontmatter), blank lines, and lesson body are preserved on disk — only the first # line is replaced. Both active and superseded lifecycle states are rewriteable; only a missing file or a malformed lesson (no H1 line) fail.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons set-title \
--lesson-id 2025-12-02-001 \
--title "Build fails with missing dependency (canonical)"
Parameters:
--lesson-id (required): Lesson ID whose title to rewrite--title (required): New title; replaces the H1 line verbatimIdempotent: rewriting with the existing title produces no on-disk change but still returns status: success with old_title == new_title so callers can re-run safely.
Fenced-code-block safety: the rewriter walks the markdown line-by-line tracking ``` fence state, so a literal # heading line inside a code example is not mistaken for the lesson H1.
Output (TOON):
status: success
lesson_id: 2025-12-02-001
old_title: "Build fails with missing dependency"
new_title: "Build fails with missing dependency (canonical)"
file: /abs/path/to/.plan/local/lessons-learned/2025-12-02-001.md
Path-allocate flow (canonical):
The standard sequence for creating a lesson with a non-trivial body is:
add — allocate the lesson file and capture the returned id.Write {plan_dir}/work/lesson-body-{id}.md — write the body markdown directly to a plan-scoped staging file using the Write tool. This bypasses shell quoting entirely and supports arbitrary markdown content.set-body --lesson-id {id} --file {path} — apply the staged body to the lesson file. The script reads the file from disk and replaces the body section while preserving the metadata header and title.Worked example:
# Step 1: allocate
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons add \
--component maven-build --category bug \
--title "Build fails with missing dependency"
# → returns id=2025-12-02-001
# Step 2: stage body via Write tool (no shell quoting concerns)
Write("/abs/path/to/.plan/local/plans/EXAMPLE-PLAN/work/lesson-body-2025-12-02-001.md", body_markdown)
# Step 3: apply
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons set-body \
--lesson-id 2025-12-02-001 \
--file /abs/path/to/.plan/local/plans/EXAMPLE-PLAN/work/lesson-body-2025-12-02-001.md
The inline --content STRING form is the secondary path — reserve it for tiny single-line payloads (e.g., a one-sentence note) where staging a file would be overhead. For anything multi-line, code-bearing, or containing shell-significant characters, always use the path-allocate flow above.
Update lesson metadata.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons update \
--lesson-id 2025-12-02-001 \
[--component new-component] \
[--category bug|improvement|anti-pattern]
Parameters:
--lesson-id (required): Lesson ID to update--component: Update component name--category: Update categoryOutput (TOON):
status: success
id: 2025-12-02-001
field: component
value: new-component
previous: maven-build
Get a single lesson.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons get \
--lesson-id 2025-12-02-001
Output (TOON):
status: success
id: 2025-12-02-001
component: maven-build
category: bug
created: 2025-12-02
title: Build fails with missing dependency
content: |
When running a Maven clean install...
List lessons with filtering.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons list \
[--component maven-build] \
[--category bug] \
[--status active|superseded|removed|all] \
[--full]
Parameters:
--component: Filter by component name--category: Filter by category (bug, improvement, anti-pattern)--status: Filter by lifecycle status — active (default), superseded, removed, or all. Use --status all to surface superseded/removed lessons; this is the canonical mechanism (there is no --include-tombstoned flag).--full: Include the full lesson body content in each rowOutput (TOON):
status: success
total: 5
filtered: 2
lessons:
- id: 2025-12-02-001
component: maven-build
category: bug
title: Build fails with missing dependency
- id: 2025-12-02-002
component: plan-files
category: improvement
title: Add validation for plan_id format
Move a lesson out of the global lessons-learned directory and into a plan directory as lesson-{id}.md. This is how a lesson transitions from "unapplied" to "applied" — the lifecycle state is encoded in the file's location, not in metadata.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons convert-to-plan \
--lesson-id 2025-12-02-001 \
--plan-id EXAMPLE-PLAN
Parameters:
--lesson-id (required): Lesson ID to move--plan-id (required): Target plan directory under .plan/local/plans/Output (TOON):
status: success
lesson_id: 2025-12-02-001
plan_id: EXAMPLE-PLAN
source: .plan/local/lessons-learned/2025-12-02-001.md
destination: .plan/local/plans/EXAMPLE-PLAN/lesson-2025-12-02-001.md
Prune the markdown stubs of superseded lessons. Tombstones at
.tombstones/{id}.json are NEVER touched — they remain as the audit trail
for the supersede event so historical references resolve by id even after
the redirect stub is gone.
Two mutually exclusive modes:
--lesson-id ID (repeatable). Each id is evaluated
regardless of file age. Required metadata.status == 'superseded' and
the matching tombstone must exist.--retention-days N. Walks every .md whose
metadata.status == 'superseded' and whose mtime is older than
now - N days. When --retention-days is omitted, the value falls back
to system.retention.lessons_superseded_days from marshal.json,
with a hard fallback of 7 if marshal.json is absent or unreadable.Per-id outcomes:
| Bucket | Condition |
|--------|-----------|
| removed[] | Lesson .md was unlinked (or, on --dry-run, would have been) |
| already_removed[] | .md already absent and tombstone present (idempotent re-run) |
| skipped_no_tombstone[] | Tombstone missing — refused to act because the audit trail would be lost |
# Age-filtered (uses marshal.json retention or hard fallback 7 days)
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons cleanup-superseded
# Age-filtered with explicit threshold
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons cleanup-superseded \
--retention-days 30
# Explicit ids
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons cleanup-superseded \
--lesson-id 2025-12-02-001 \
--lesson-id 2025-12-02-002
# Dry-run (report only)
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons cleanup-superseded \
--retention-days 7 --dry-run
Parameters:
--lesson-id: Repeatable lesson ID; mutually exclusive with --retention-days--retention-days: Age threshold in days; mutually exclusive with --lesson-id--dry-run: Report what would be removed without unlinking anythingOutput (TOON):
status: success
dry_run: false
retention_days_effective: 7
removed[1]{lesson_id}:
2025-12-02-001
already_removed[0]{lesson_id}:
skipped_no_tombstone[0]{lesson_id}:
Each successful unlink emits an INFO line to script-execution.log:
(plan-marshall:manage-lessons) Pruned superseded stub {id}.
Create lesson from error context (JSON).
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons from-error \
--context '{"component":"maven-build","error":"Missing dependency","solution":"Add explicit dep"}'
Parameters:
--context (required): JSON object with error context
component: Component name (defaults to "unknown")error: Error message (required)solution: Optional solution descriptionOutput (TOON):
status: success
id: 2025-12-02-003
created_from: error_context
Read-only classifier that groups the active lessons corpus into multi-lesson groups whose work would land in a single plan. Never mutates lesson files — set-body, set-title, supersede, and cleanup-superseded are NOT invoked. Use the orchestrator action (/plan-marshall:plan-marshall Action: lessons-aggregate) when you want the merge actually applied; use this verb when you want to inspect the classification first.
The classifier rules, signal-priority order, primary-pick tie-breakers, and merged-body-preview template are specified in references/aggregate-analysis.md.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons aggregate \
[--top-n 5]
Parameters:
--top-n (optional, default 5): Number of headline /plan-marshall:plan-marshall lesson={primary_id} commands to surface in top_n_commands. The full groups[] list is always returned regardless of this flag.Output (TOON):
status: success
top_n: 5
groups[N]{primary_id,primary_title,absorb_count,tier,enacted,absorbed,merged_body_preview}:
...
top_n_commands[N]:
- "/plan-marshall:plan-marshall lesson=2025-12-02-001"
- "/plan-marshall:plan-marshall lesson=2025-12-04-002"
Each group carries tier (the producing signal: cross-ref | shared-component | shared-standards-dir | shared-workflow-boundary) and enacted (true only for the cross-ref tier — weaker tiers are opt-in co-location suggestions, not auto-applied merges). Each absorbed[] row carries {lesson_id, title, reason} where reason names the strongest signal that placed the lesson in the group (e.g., cross-ref to 2025-12-02-001, shared component plan-marshall:phase-5-execute, shared standards-dir marketplace/bundles/.../standards/, shared workflow-boundary plan-marshall:phase-5-execute). merged_body_preview is the first ~400 characters of the would-be merged body so callers can sanity-check the grouping before invoking the orchestrator action.
Singletons (lessons that match no other lesson at any signal tier) are dropped — only multi-member groups are emitted.
The classification logic for the read-side corpus operations lives under references/:
references/dedup-analysis.md — single-candidate classifier (new / merge_into / already_closed). Used by the dedup gate before any new lesson is recorded.references/aggregate-analysis.md — full-corpus classifier. Specifies the signal-priority order (cross-ref > shared-component > shared-standards-dir > shared-workflow-boundary), primary-pick tie-breakers (cross-ref-fan-in → recurrence-count → lesson-id), and the merged-body-preview template consumed by the aggregate verb and the lessons-aggregate orchestrator action.Script: plan-marshall:manage-lessons:manage-lessons
| Command | Parameters | Description |
|---------|------------|-------------|
| add | --component --category --title [--bundle] | Allocate a new lesson file and return its absolute path. Caller populates body via set-body. |
| set-body | --lesson-id (--file PATH \| --content STRING) | Populate or replace lesson body. --file is the canonical form (shell-safe for arbitrary markdown); --content is the secondary form for tiny single-line payloads only. |
| set-title | --lesson-id --title | Rewrite the H1 title in place. Preserves frontmatter and body; idempotent; works on active and superseded lessons. Fenced-code-block aware. |
| update | --lesson-id [--component] [--category] | Update lesson metadata |
| get | --lesson-id | Get single lesson |
| list | [--component] [--category] [--full] | List with filtering. --full includes lesson body content. |
| aggregate | [--top-n N] | Read-only classifier: group active lessons that would land in one plan. Returns groups + headline commands. See references/aggregate-analysis.md. |
| from-error | --context | Create from JSON error context (programmatic; body synthesized from context) |
| convert-to-plan | --lesson-id --plan-id | Move lesson into a plan directory as lesson-{id}.md. This is the move-semantics replacement for marking a lesson "applied". |
| cleanup-superseded | [--lesson-id ID ...] \| [--retention-days N] [--dry-run] | Prune superseded .md stubs while preserving tombstones. Age-filtered when --retention-days (falls back to system.retention.lessons_superseded_days, hard fallback 7); explicit when --lesson-id is repeated. |
| auto-suggest | --plan-id [--max-suggestions N] [--no-emit] | Recipe-registry matcher for phase-1-init Step 5c. Scans the live recipe registry (manage-config list-recipes) and returns up to --max-suggestions recipes (default 3) ordered by deterministic confidence — keyword overlap (request narrative ∩ recipe description) + domain alignment + scope alignment. Each suggestion is also written as a plan-scoped tip finding (artifacts/findings/tip.jsonl) so the orchestrator can surface them in the audit log; pass --no-emit to inspect without writing findings. No LLM dispatch — the matcher is pure regex + set algebra. Falls through to the existing Step 5c LLM path when no recipe clears the 0.35 confidence floor. |
| Category | When to Use |
|----------|-------------|
| bug | Script is broken or produces wrong results |
| improvement | Script works but could be better |
| anti-pattern | Script was misused or documentation unclear |
See manage-contract.md for the standard error response format.
| Error Code | Cause |
|------------|-------|
| not_found | Lesson ID doesn't exist (get, update, set-body, convert-to-plan) |
| invalid_category | Category not in: bug, improvement, anti-pattern |
| invalid_context | JSON context parsing failed (from-error) |
| invalid_input | set-body invoked without exactly one of --file / --content, or both supplied |
| file_not_found | set-body --file PATH points at a non-existent path or a non-regular file (directory, broken symlink, special file) |
| file_read_error | set-body --file PATH failed with an OSError while reading (permission denied, I/O error, etc.) |
| malformed_lesson | set-body target lesson file is missing its metadata header / title structure |
| missing_required | Required parameter missing |
The canonical argparse surface for manage-lessons.py. The D4 plugin-doctor analyzer
(_analyze_manage_invocation.py) reads this section as source-of-truth for markdown
notation occurrences across the marketplace. Consuming skills xref this section by
name (e.g., "see manage-lessons Canonical invocations → add") instead of
restating the command inline.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons add \
--component COMPONENT --category {bug|improvement|anti-pattern} --title TEXT \
[--bundle BUNDLE]
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons update \
--lesson-id LESSON_ID \
[--component COMPONENT] [--category {bug|improvement|anti-pattern}]
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons get \
--lesson-id LESSON_ID
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons list \
[--component COMPONENT] [--category {bug|improvement|anti-pattern}] \
[--status {active|superseded|removed|all}] [--full]
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons convert-to-plan \
--lesson-id LESSON_ID --plan-id PLAN_ID
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons set-body \
--lesson-id LESSON_ID (--file PATH | --content TEXT)
--file and --content are mutually exclusive; exactly one is required.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons set-title \
--lesson-id LESSON_ID --title TEXT
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons aggregate \
[--top-n N]
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons from-error \
--context JSON
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons remove \
--lesson-id LESSON_ID --reason TEXT \
[--force]
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons supersede \
--lesson-id LESSON_ID --by CANONICAL_LESSON_ID --reason TEXT
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons cleanup-superseded \
[--lesson-id LESSON_ID ...] [--retention-days N] [--dry-run]
--lesson-id (repeatable) and --retention-days are mutually exclusive.
python3 .plan/execute-script.py plan-marshall:manage-lessons:manage-lessons auto-suggest \
--plan-id PLAN_ID [--max-suggestions N] [--no-emit]
| Client | Operation | Purpose |
|--------|-----------|---------|
| phase-5-execute | add, from-error | Document errors and solutions during execution |
| phase-6-finalize | add | Promote findings to lessons |
| plugin-doctor | add | Capture recurring component issues |
| Client | Operation | Purpose |
|--------|-----------|---------|
| plugin-apply-lessons-learned | list, convert-to-plan | Apply lessons to marketplace components by moving them into a plan directory |
| phase-6-finalize | list | Query unapplied lessons (those still in .plan/local/lessons-learned/) for promotion |
manage-findings — Findings promoted to lessons at 6-finalizemanage-run-config — Complementary global persistence (execution state)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