The Retro stage of the harness loop. The place the loop turns observation into encoded improvement. Two modes:
Encode, don't document. A wiki paragraph that says "remember to do X" is worth nothing; an automated step that does X for you is worth everything. The whole point of retro is that friction observed during work becomes executable knowledge — a justfile recipe, a skill edit, a staged diff — not a prose note that rots. The
[e]ncodeaction stages the change as a diff; that diff IS the encoding. Prefer an automated command over a wiki paragraph, a seed script over a manual setup step, a pre-flight check over a "remember to…". Executable knowledge > prose.
Input
$ARGUMENTS
# Modes:
# --drain Session-end: read the per-agent buffer, present the soft prompt, route entries to .retro.md
# --harvest Long-horizon: scan + cluster + prioritize .retro.md files; print the curated view
# --harvest --json Machine-readable render of the harvest view (for `just compound-value`, CI hooks)
# Plus --harvest runtime filters: --plan <slug> / --agent <slug> / --since <date> / --kind <kind>
# Plus --harvest --prune --older-than <Nd> [--apply] Reversible stale-retro pruning (dry-run by default)
| Mode | Replaces | When | What it does |
|---|---|---|---|
--drain |
session-end bubble | end of session / logical pause / cross-session leftover | Drains the per-agent buffer via the [s/t/p/e/d/a] menu into .retro.md |
--harvest |
long-horizon curate | FINAL phase / merge end / review end / ad-hoc | Scans + clusters + prioritizes .retro.md files; prints the curated view (or --json) |
The producer that fills the buffer --drain reads is harness-2-observe. The Boot stage is harness-1-boot.
Mode: --drain (session-end)
The consumer-side surface of the loop. The ONE place retro talks to the user.
When to fire
- Auto-fired by pipeline skills at natural logical pauses (plan-1a end, plan-3 end, plan-6 end-of-phase, plan-6-companion end-of-phase, plan-7 end, plan-8 end)
- Manually fired by the user (
/harness-3-retro --drain) at any time - Start-of-skill check on any auto-firing skill — if the buffer has leftover entries from a prior session (cross-session carryover), drain immediately
Each firing handles entries accumulated since the last drain. Once drained, the buffer is empty; the next firing on an unchanged buffer is silent.
Sentinel check
If docs/harness/.disabled exists → silently no-op. No prompt, no output.
Step 1 — Read the buffer
Path: docs/harness/_buffers/<agent>.session-buffer.md
Where <agent> is the calling CLI's slug (claude-code, codex, github-copilot, opencode, pi, or a companion slug like plan-6-companion).
If the file is missing or empty → silent (no prompt). Exit.
If non-empty → parse it as a sequence of YAML entry blocks (each prefixed with - id: …).
Step 2 — Present the soft prompt
Single prompt at end of session. Never asks twice. Format:
💡 harness retro — 3 entries from this session:
1. [difficulty/tooling] grep on src/ took 47s
→ encode as: justfile recipe wrapping ripgrep
2. [magic-wand/project] A `just rg <pattern>` recipe would shave 40+s per search
→ encode as: justfile recipe
3. [gift/compound] harness ledger scaffolded cleanly
→ no encoding needed (it's a gift)
4. [difficulty/project-sensor] Had to infer whether the website rendered correctly because no smoke path or screenshot evidence was available
→ encode as: add smoke command or visual evidence capture
[s]ave all to scope file
[t]ask: emit /plan-5 --fix invocations for the encodable ones
[p]lan: emit /plan-1b invocations for the bigger ones
[e]ncode: stage diffs in scratch/encode-<id>-<target>.diff
[d]ismiss all (entries dropped, not saved)
[a]ll-save (default — press Enter)
[s/t/p/e/d/a]: ▮
Notes on the prompt:
- One line per entry, prefixed
[kind/target] - One-line encoding hint per entry (from
suggested_encodingor a sensible default) - Action menu fits in two screen-lines
- Pressing Enter without typing =
[a]ll-save(the default) - Drain should make both improvement shapes visible: ease/friction improvements (faster, clearer, less annoying) and proof/back-pressure improvements (new deterministic signals, sensors, evidence paths, architecture checks). Both use the same schema; distinguish them by
target, description, and encoding hint, not by newkindvalues.
Step 3 — Route by action
[a]ll-save (default)
Wrap all buffer entries in a single universal retro envelope and write one .retro.md file:
---
schema_version: "1.0"
retro_id: "<ISO>-<agent>-<short-hash>"
agent: <agent>
plan_id: <plan-id-from-cwd-or-branch-detection-or-null>
started_at: "<ISO of first entry's first_seen_at, or session start>"
ended_at: "<now in ISO UTC>"
summary: "harness-3-retro --drain session-end save (N entries)"
entries:
# ... all buffer entries verbatim
system:
compound:
bubble_action: "all-save"
---
File path via resolvePath() (workshop 006 § Path Resolver):
docs/harness/agents/<slugified-agent>/<YYYY-MM-DD>/T<HH-MM-SS>Z-<hash>.retro.md
Then clear the buffer (truncate to empty; keep the file).
[s]ave (selective save)
Prompt: "Which entries to save? [1,2,3 or a]". Save the selected ones into a .retro.md (same envelope as [a]ll-save); discard the rest. Clear buffer.
[t]ask — emit copy-pasteable /plan-5 --fix invocations
For each encodable entry, print:
/plan-5 --fix --description "<entry.description>" --target <entry.target>
The user copy-pastes one or more of these. The entries are ALSO saved to a .retro.md (so the suggestion is captured even if the user doesn't run all of them). Clear buffer.
[p]lan — emit copy-pasteable /plan-1b invocations
For each entry suggesting a larger piece of work, print:
/plan-1b "<one-line spec derived from entry.description + entry.suggested_encoding>"
User copy-pastes the ones they want. Entries also saved to .retro.md. Clear buffer.
[e]ncode — stage diffs
For each entry where the encoding is a small mechanical edit (frequently true for kind: difficulty with a clear suggested_encoding):
- Generate the diff (the agent makes a best-effort guess at the change)
- Append the Validation footer (mandatory — see template below) so the staged diff documents how a reviewer verifies the encoded fix actually works
- Write to
scratch/encode-<entry-id>-<target-slug>.diff - Print the file path: "Staged scratch/encode-DL-001-tooling.diff — review and
git applyto land"
Nothing is auto-applied. The diff is staged for user review. This is the "encode, don't document" mechanism — the encoding is in the diff, not in a doc.
Entries are also saved to .retro.md with system.compound.status: suggested and system.compound.resolved_by: scratch/encode-<id>-<target>.diff. Clear buffer.
Validation footer template (mandatory on every encoded diff)
Every staged scratch/encode-<id>-<target>.diff MUST end with a literal ## Validation block of this shape:
## Validation
Run:
<command 1>
<command 2 — optional>
Expected:
- <observable outcome 1>
- <observable outcome 2 — optional>
Compound lifecycle:
<entry-id> transitions system.compound.status: suggested → encoded when this diff lands.
resolved_by: <commit-sha-after-land>
How the three sub-sections are filled:
Run:— best-effort shell command(s) that exercise the encoded change. If the entry'ssuggested_encodingmentions a recipe/command, use it; otherwise the agent picks a sensible reproduction or verification command (compile / test / grep / curl). If genuinely unknown, writeRun: (manual review only).Expected:— observable outcomes (file content matches, command exits 0, output contains substring). Plain bullets — no full test framework needed.Compound lifecycle:— names the entry id and the transition the--harvest [r]esolvedlifecycle action will execute on this entry. Theresolved_byline is a placeholder the user fills with the actual SHA after the diff lands.
The footer makes "encoded" mean the loop changed AND we can prove it, not just we wrote a patch. Reviewers see the verification path inline with the change.
[d]ismiss all
Truncate the buffer. Entries are dropped — not saved anywhere. Print one line: "✓ buffer dismissed (3 entries dropped)".
This is the "I don't want this captured" escape hatch. Use sparingly — entries dismissed here can't be recovered.
Step 4 — Plan ID detection
When saving, populate frontmatter.plan_id from:
- Current working directory: if cwd matches
docs/plans/<NNN-slug>/, setplan_id: <NNN-slug> - Else: current git branch: if branch matches
<NNN>-<slug>, setplan_id: <branch-name> - Else:
plan_id: null(no plan context)
Cross-session leftover check
At the start of any auto-firing skill, before doing its primary work, check the buffer:
- If
_buffers/<agent>.session-buffer.mdis non-empty → fireharness-3-retro --drainimmediately - Then proceed with the skill's primary work
This catches entries left over from a prior session (e.g. the user pressed Ctrl-C before the auto-drain fired).
What --drain does NOT do
- No mid-session prompting. Only at end-of-session / logical pauses / next-session leftover.
- No auto-applying any encoded diff. Staged-only.
- No editing of
.retro.mdfiles after writing them. (That's--harvest's job for lifecycle status mutations.) - No reading or aggregating retros from other sessions. Each drain handles its OWN buffer; cross-session aggregation is
--harvest.
--drain edge cases
- Empty buffer: silent. No prompt. Exit cleanly.
- Sentinel during drain: if
.disabledappears mid-drain, abort (don't write). Buffer stays as-is. - Concurrent drains (two agents simultaneously): each has its own buffer, no collision.
- Save to a path that already exists (extremely rare hash collision): append
-2,-3, etc. to the filename per workshop 006 § EC3. - User interrupts mid-prompt: buffer stays unchanged. Next drain will see the same entries.
- Malformed entry in buffer: skip with a warning ("⚠ skipped 1 malformed entry"); save the valid ones; clear buffer (the malformed one is lost — better than corrupting a
.retro.md).
One-line encoding hint generation
For each entry, the prompt shows a one-line encoding hint. Source:
- If
entry.suggested_encodingis set → use it verbatim - Else, derive from
entry.kind+entry.target:difficulty/tooling→ "wrap in a justfile recipe"difficulty/skill→ "edit the SKILL.md"magic-wand/<any>→ "encode as the suggestion above"gift/<any>→ "no encoding needed"insight/<any>→ "document in AGENTS.md or a docs/how article"
- Else → "(no encoding hint — review manually)"
Mode: --harvest (long-horizon)
The reader/curator side of the loop. Auto-fires at long-horizon reflection moments (plan-6-companion FINAL-phase debrief, plan-8 merge end, plan-7 end). Can also be run manually for ad-hoc curation.
When to fire
- AUTO-fired at:
plan-6-companionFINAL-phase debrief (the dominant flow — replaces /plan-7 as harvest anchor)plan-8-mergeend (plan-completion reflection)plan-7-code-reviewend (preserved for rare solo /plan-6 flow)
- SUGGESTED at the start of:
plan-1a-explore(if ≥5 unharvested entries — print invocation as one-liner; do NOT auto-fire)plan-3-architect(if ≥10 unharvested entries — same)
- Manually by the user at any time:
/harness-3-retro --harvest [--plan <slug>] [--agent <slug>] [--since <date>] [--kind <kind>]
Sentinel check
If docs/harness/.disabled exists → print one line: "📴 the harness retro ledger is disabled (docs/harness/.disabled present). Remove the sentinel to re-enable." and exit. Do not scan, do not print views.
Buffer-non-empty advisory
At start, check docs/harness/_buffers/<agent>.session-buffer.md for the calling agent. If non-empty → print one line before scanning:
ℹ️ Buffer has N unbubbled entries. Consider running
/harness-3-retro --drainfirst so they land in the harvest view.
Then proceed with the scan anyway (the harvest reads .retro.md files; buffer entries are unrelated).
Step 1 — Scan + validate
Canonical path: docs/harness/agents/**/*.retro.md
For each file:
- Parse the YAML frontmatter (between the first two
---lines) - Validate against the bundled
references/retro.schema.json(a deployment mirror of the canonical, frozendocs/harness/schemas/retro.schema.json— copied into this skill folder so it travels with the skill vianpx skills add; in a source checkout either path is identical). If neither is present, skip validation and print⚠ retro schema not found — skipping validation(best-effort: a missing schema never blocks the harvest). - If invalid → print a warning naming the file path + the validation error; SKIP the entire retro (strict — no half-parse)
- If valid → add to the in-memory view
Back-compat path: docs/retros/*.md (minih's legacy single-big-file-per-agent format)
For each file (skip *.legacy.md — those are post-migration archives):
- Parse the file as a sequence of
## <ISO timestamp> — <slug> / <runId>blocks (regex on/^## \d{4}-\d{2}-\d{2}T/m) - For each block:
- Run the minih block → universal retro mapping inline (workshop 005 § D9
minihToUniversal) - Add to the in-memory view
- Run the minih block → universal retro mapping inline (workshop 005 § D9
Step 2 — Dedup by retro_id
If the same retro_id appears in both the canonical and back-compat paths (rare; happens during minih dual-write phase), the canonical (universal) version wins. Skip the back-compat copy.
Step 3 — Schema-version skew handling
If a retro has schema_version starting with a major number this reader doesn't know (e.g. 2.x when the reader supports 1.x):
- Print:
"⚠ Skipped 1 retro with unsupported schema_version: <path>" - Skip the retro
Minor-version skew (1.1 when reader is 1.0) is silent — forward-compat per workshop 005 § D8.
Step 4 — Curate
Build the in-memory view:
Cluster open entries
Group entries by (kind, target). Within each cluster:
- Count entries
- Age-order (oldest first by
system.compound.first_seen_ator fallback to retro'sstarted_at) - Track which agents/retros they came from
Stale flag
openentries older than 4 weeks → tag as stalesuggestedentries older than 2 weeks withoutresolved_by→ tag as stale
These thresholds are observational hints (printed in the view), not enforced. No auto-mutations.
Prioritize top-10
Sort clusters by:
- Recurrence (count of entries) — highest first
- Severity (entries with
severity: blockingrank higher; thendegrading; thenannoying; entries with no severity rank lowest) - Back-pressure leverage (clusters whose target or representative entry indicates missing proof/sensors/evidence/architecture/security/schema checks should stay legible as proof-improvement candidates)
- Age (older clusters rank higher)
Cap at top-10 for the default view. Filtered views may show more.
Back-pressure leverage is advisory display guidance only. It does not create a gate, score, persisted index, or threshold, and it must not mutate entries.
Recognize proof/back-pressure clusters
Treat these as proof/back-pressure improvement candidates when printing labels or choosing representative wording:
- Targets such as
project-sensor,runtime-inspectability,architecture-fitness,security,schema,infra, ortooling - Descriptions or
suggested_encodingvalues that mention smoke paths, screenshots, logs, traces, health checks, dependency-direction rules, CodeQL/Roslyn/ArchUnit, schema validation, data checks, or missing evidence difficultyentries where the workaround was "manual review", "read code manually", "inferred", or "eyeballed"
Keep the original schema fields intact. Do not rewrite kinds to signal-gap, sensor-gap, or weak-back-pressure.
Step 5 — Print terminal view (NO on-disk writes)
Default format:
🌾 Harness retro harvest — 2026-05-18T15:30:00Z
📚 Scanned 27 retros across 3 agents (claude-code, plan-6-companion, minih-my-agent)
Date range: 2026-04-10 → 2026-05-18
Total entries: 47 (28 open, 17 encoded, 2 wontfix)
📊 Open clusters (top 10 by recurrence > severity > back-pressure leverage > age):
1. [tooling] grep/search slowness — 4 entries (claude-code: 3, plan-6-companion: 1) [r/w/s]
2. [proof/project-sensor] missing smoke or visual evidence — 3 entries [r/w/s]
3. [pipeline] missing example patterns — 3 entries [r/w/s]
4. [config] env var contract guessing — 2 entries [r/w/s]
...
⏰ Stale (>4 weeks open): 3 entries
- DL-061 (2026-04-10): "spec template § Domain Manifest unclear" [r/w/s]
...
✅ Recently encoded (last 7 days): 6 entries — see scratch/encode-*.diff for the diffs
[s/t/p/e/d/a/r/w/s]: ▮
Nothing is written to disk by the harvest itself (per workshop 006 § D4 KISS revision — no _LEDGER.md, no _AGENT.md, no rollup files). The view is transient terminal output.
--json output (machine-readable read interface)
/harness-3-retro --harvest --json emits the same computed view as the default render but as a single JSON document on stdout. This is a read-time render of transient computation — still no on-disk index, still no persisted state. Use it for just compound-value, CI hooks, or any downstream skill that wants programmatic access to loop status.
Schema (stable contract — bump compound v1.x if changed):
{
"schema_version": "1.0.0",
"generated_at": "2026-05-19T01:30:00Z",
"retros": 27,
"entries": {
"total": 47,
"open": 28,
"suggested": 2,
"encoded": 17,
"wontfix": 0,
"dismissed": 0,
"escalated": 0,
"stale": 0
},
"top_clusters": [
{
"kind": "difficulty",
"target": "tooling",
"count": 4,
"oldest": "2026-05-14T11:22:00Z",
"representative": "grep on src/ took 47s — should use ripgrep"
}
],
"harness": {
"maturity": "L2",
"last_validation": "2026-05-18",
"boot_ms": 18000,
"verdict": "healthy"
}
}
Field semantics:
schema_version— semver for the JSON contract itself; bump on breaking shape change.generated_at— ISO-8601 UTC timestamp of THIS render.retros— count of.retro.mdfiles scanned (post dedup + version-skew filter).entries.*— counts bysystem.compound.status(plustotal= sum of all entries seen). Missing-status entries count asopen.top_clusters— top-10 clusters by the same priority order as the default view (recurrence > severity > back-pressure leverage > age). Cap at 10; consumers wanting fewer should slice.harness— ifdocs/project-rules/engineering-harness.md(or legacyagent-harness.md/harness.md) exists, parse its## Maturity Assessment+## Historyfor the most recent validation. If absent, emit{"maturity": null, "last_validation": null, "boot_ms": null, "verdict": null}.
Missing/empty cases:
- Empty tree →
{..., "retros": 0, "entries": {"total": 0, ...}, "top_clusters": [], "harness": {...or null}}. Still valid JSON; consumers handle. - Sentinel
docs/harness/.disabledpresent → emit the disabled-line on stderr; exit non-zero; no JSON on stdout (consumers can detect by exit code). - Schema-version skew on a retro → still emit JSON; skipped retros contribute to neither counts nor clusters.
Consumed by scripts/compound-value.sh (the cross-CLI portable pretty-printer) and just compound-value. Other consumers should pipe <their-CLI invokes the skill> --harvest --json | jq ....
Step 6 — Action menu
The same [s/t/p/e/d/a] actions as --drain, plus three lifecycle ops:
[s]ave(selective save) — typically a no-op here (entries already saved); operates on top-10 cluster selection[t]ask— emit/plan-5 --fixinvocations for selected entries[p]lan— emit/plan-1binvocations[e]ncode— stagescratch/encode-<id>-<target>.difffiles (same as--drain)[d]ismiss— mutatesystem.compound.status: dismissedIN-PLACE in the source.retro.md[a]ll-save— typically a no-op (entries already saved)[r]esolved— mutatesystem.compound.status: encoded; prompt forresolved_by:(free-text; user pastes a commit hash / PR URL / scratch diff path)[w]ontfix— mutatesystem.compound.status: wontfix[s]tale— mutatesystem.compound.status: stale
For lifecycle ops, the harvest reads the source file, updates the entry's system.compound.status, writes back. The file's overall schema_version, retro_id, etc. are untouched.
Runtime filters
All combinable. Each filters the scan + view to matching retros:
--plan <slug>→ only retros wherefrontmatter.plan_id == <slug>--agent <slug>→ only retros wherefrontmatter.agent == <slug>(slugified match)--since <YYYY-MM-DD>→ only retros where the directory date >=<date>(orstarted_atif directory not date-sliced)--kind <kind>→ only entries (not retros) whereentry.kind == <kind>; retros with no matching entries are omitted
Example: /harness-3-retro --harvest --plan 023-difficulty-ledger-skill --since 2026-05-15 --kind difficulty — show only difficulty entries from plan 023 retros newer than May 15.
Pruning (--prune)
/harness-3-retro --harvest --prune --older-than 90d → dry-run by default:
- List all retros older than 90 days
- Show what would be deleted
- Print:
"This is a dry run. Add --apply to actually delete (and recommend running on a clean git working tree)."
/harness-3-retro --harvest --prune --older-than 90d --apply → actually delete the files. Single-confirmation prompt before deletion.
User responsibility — best-effort framing; no auto-pruning ever.
What --harvest does NOT do
- No on-disk index files written. Every cross-cutting view is terminal print, computed at read time.
- No auto-applying any encoded diff. Staged-only via
[e]ncode. - No buffer reading. Buffer is
--drain's territory; this mode reads.retro.mdfiles only. - No mid-session firing. The auto-firing sites are at logical pauses (end of plan-N, debrief, merge).
- No schema expansion. Missing-signal and back-pressure entries stay schema-compatible by using existing
kindvalues plus targets and encoding hints. - No proof gates. Harvest can surface missing sensors as high-leverage improvement candidates, but it never blocks a plan, applies a threshold, or declares compliance.
--harvest edge cases
- Empty tree (no
.retro.mdfiles anywhere): print"🌾 No retros found. Start logging via harness-2-observe during sessions."and exit. - Sentinel mid-harvest: if
.disabledappears mid-scan, abort. Don't write any pending lifecycle mutations. - Concurrent harvests: lifecycle mutations are last-write-wins per file. Unlikely to collide because both readers compute identical views.
- Hash collision in retro_id (two retros, same ID): dedup keeps the canonical one. Print warning if both are canonical (
agents/**); skip the second. - Schema-version skew: see Step 3 above.
docs/retros/absent: just skip the back-compat path. No error.
Why no on-disk index files
Per workshop 006 § D4 KISS revision (the user demanded this in review):
Persisting derived state creates four problems the system doesn't need: drift between index and source, git diff noise on every harvest, agent maintenance burden, and bureaucratic ceremony.
The harvest computes the view in <1s for typical repos (≤100 retros). Re-computation per invocation is cheap; persistence is expensive (in attention and git noise).
For ad-hoc shell-level browsing without the skill:
ls docs/harness/agents/*/$(date -u +%Y-%m-%d)/ # today's retros
ls docs/harness/agents/<agent>/ # one agent's date dirs
cat docs/harness/agents/<agent>/<date>/*.retro.md # the files themselves
grep -l 'plan_id: "023-' docs/harness/agents/*/*/*.retro.md # all plan-023 retros
The tree IS the browse surface. Harvest is for clustered/prioritized views; shell tools are for raw browsing.
References
- Workshop 001 — Self-improvement vibe (§ Anti-vibe 1 nag-ware; § D5 terse one-line hints)
- Workshop 004 — SDD pipeline integration (§ Walkthrough D)
- Workshop 005 — Universal retro contract (§ envelope; § validation; § D8 versioning; § D9 round-trip)
- Workshop 006 — Compound folder layout (§ Path Resolver; § Runtime Views; § D4 KISS no-indexes; § D6 pruning; § D7 minih back-compat; § EC2 cross-session carryover; § EC9 dual-source dedup)
- Spec § Acceptance Criteria #7-17, #23
- Spec § Q5.3 (stale heuristics — calibrate during dogfood)
- Spec § Q6.1 (task-boundary check only when buffer empty)