Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/lean-full-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **A full-mode feature keeps one plan file and one closing record.** The owner's request is `plan.md` § Request (no `feature.md`). The retro lives in the feature's learnings file, with § What went wrong and action items only when something did (no `retro.md`). After QA pass the main chat reads QA's record instead of re-running the checks, Full-stack updates the project fact docs as plan steps, and the close takes three steps instead of a retro file and its checks. The plan checklist's group index moved into `harness/TECHNICAL_LEAD.md`, so the main chat prints only the groups a plan needs, in one call.
77 changes: 77 additions & 0 deletions scripts/kit-selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,70 @@ def check_doc_sections(bad, t):
bad(f"only {seen} doc-section.sh calls found in the rendered docs: harness/TECHNICAL_LEAD.md § Full mode, step by step names the sections to read")


def check_lean_records(bad, t):
"""I3: a full-mode feature keeps one plan file (the request is its § Request) and one closing record (the
learnings file, with the retro inside it and § What went wrong only when something did); the main chat prints
only the checklist groups the plan needs, in one call, from the index in TECHNICAL_LEAD.md."""
for gone in ("harness/templates/feature.md", "harness/templates/retro.md"):
if os.path.exists(os.path.join(t, gone)):
bad(f"{gone} is still installed: the request is plan.md § Request and the retro is in the learnings file")
for path in files_under(t, exts=(".md",)):
rel = os.path.relpath(path, t)
# _harness/LEARNINGS.md is a run record an update never rewrites, so its intro keeps the old wording
if rel.startswith(("_harness/features", "node_modules", "updates/", "CHANGELOG")) or rel == "_harness/LEARNINGS.md":
continue
m = re.search(r"[\w/]*(?:feature|retro)\.md", read(path))
if m:
bad(f"{rel} still names {m.group(0)}: full mode has no feature.md or retro.md")
plan = read(os.path.join(t, "harness/templates/plan.md"))
req = plan.split("\n## Request\n", 1)[1].split("\n## ", 1)[0] if "\n## Request\n" in plan else ""
for want in ("**What we want:**", "**Why:**", "**Success looks like:**"):
if want not in req:
bad(f"harness/templates/plan.md § Request doesn't ask for {want!r}")
if "project fact docs" not in plan.split("\n## Implementation steps\n", 1)[-1].split("\n## ", 1)[0]:
bad("harness/templates/plan.md § Implementation steps doesn't make the project fact docs Full-stack steps")
entry = read(os.path.join(t, "harness/templates/learnings-entry.md"))
went = entry.split("\n## What went wrong\n", 1)[1].split("\n## ", 1)[0] if "\n## What went wrong\n" in entry else ""
if not went or "`None.`" not in went or "**Retro:**" in entry or "Agent runs" in entry:
bad("harness/templates/learnings-entry.md isn't the one closing record: § What went wrong, `None.` when nothing did, no retro link")
setup = read(os.path.join(t, "harness/SETUP.md"))
after = setup.split("\n## After QA pass\n", 1)[-1].split("\n## ", 1)[0]
if "don't run the checks again" not in after:
bad("harness/SETUP.md § After QA pass doesn't stop the main chat re-running the checks QA recorded")
retro = setup.split("\n## Retrospective\n", 1)[-1].split("\n## ", 1)[0]
if "**Close in few calls:**" not in retro or "writes `None.`" not in retro:
bad("harness/SETUP.md § Retrospective doesn't close in one call, or still asks for a retro when nothing went wrong")
# the group index in TECHNICAL_LEAD.md names every group of PLAN_CHECKLIST.md with its item range
tl = read(os.path.join(t, "harness/TECHNICAL_LEAD.md"))
rows = dict(re.findall(r"^\| ([A-Z][^|]+?) \| (\d+-\d+) \|", tl.split("\n## Before asking for approval\n", 1)[-1], re.M))
pc = read(os.path.join(t, "harness/PLAN_CHECKLIST.md")).split("\n## Before asking for approval\n", 1)[-1]
groups = re.split(r"^### ", pc, flags=re.M)[1:]
if not groups:
bad("harness/PLAN_CHECKLIST.md § Before asking for approval has no groups")
for g in groups:
name = g.split("\n", 1)[0].strip()
nums = [int(n) for n in re.findall(r"^(\d+)\. ", g, re.M)]
want = f"{min(nums)}-{max(nums)}" if nums else "?"
if rows.get(name) != want:
bad(f"harness/TECHNICAL_LEAD.md § Before asking for approval's group index has {rows.get(name)!r} for {name!r}, "
f"PLAN_CHECKLIST.md has items {want}: keep the index in step")
skip = dict(re.findall(r"^\| ([A-Z][^|]+?) \| \d+-\d+ \| ([^|]+?) \|", tl, re.M))
for name in ("Planning and scope", "Testing", "Process"):
if "always run it" not in skip.get(name, ""):
bad(f"harness/TECHNICAL_LEAD.md's group index lets a plan skip {name!r}: it always runs")
for name, want in (("Security", ("adds or changes", "permission", "route", "access gap")), ("Data", ("migrates",)),
("Release and operations", ("adds or changes", "ships separately"))):
if not all(w in skip.get(name, "") for w in want):
bad(f"harness/TECHNICAL_LEAD.md's group index skips {name!r} on a narrower condition than before ({want!r})")
steps = re.findall(r"^(\d+)\. (.*)$", after, re.M)
if dict(steps).get("4", "").split("**")[1:2] != ["Human-only steps."]:
bad("harness/SETUP.md § After QA pass step 4 isn't the human-only steps: other docs cite it as step 4")
if len(rows) != len(groups):
bad(f"harness/TECHNICAL_LEAD.md's group index has {len(rows)} rows for {len(groups)} checklist groups")
if '"Group index"' in tl:
bad("harness/TECHNICAL_LEAD.md still prints PLAN_CHECKLIST.md § Group index: the index is in TECHNICAL_LEAD.md")


def check_risk_review(bad, t):
"""H2: the code review before QA runs for every full or bug plan on a risk path, not only at xhigh; xhigh is
for sign-in, secrets and money (harness/SETUP.md § Code review before QA, § Choosing a level)."""
Expand Down Expand Up @@ -1679,6 +1743,7 @@ def check(name, fx, t):
check_small_bugs(bad, t)
check_risk_review(bad, t)
check_doc_sections(bad, t)
check_lean_records(bad, t)
# 6. named checks have commands, and no row names an unknown check
cfg = lambda key: run(["bash", "-c", f'. scripts/lib/config.sh; config_get {key}'], t).stdout.strip()
names = set()
Expand Down Expand Up @@ -2270,6 +2335,18 @@ def check(name, fx, t):
bad(f"evidence.sh printed a summary that left most entries out (exit {r.returncode}): {r.stdout!r}")
open(lf, "w").write(keep)
os.remove(pf)
# I3: a bug-mode learnings file has no retro line; its Escaped line marks the mode
bf = os.path.join(t, "_harness/learnings/selftest-bug-v1.md")
open(bf, "w").write("# selftest-bug v1 learnings\n\n### selftest-bug v1, 2026-01-03\n- **Verdict:** pass; QA rounds: 1; review round: no\n"
"- **Escaped:** notes-list v1, AC 3 missed the tie\n\n## What went wrong\n\n1. (cause: plan gap) the tie; AC 3\n")
r = run(["bash", "scripts/evidence.sh", "--json"], t)
try:
modes = json.loads(r.stdout).get("modes_unfolded", {})
except ValueError:
modes = {}
if modes.get("bug") != 1:
bad(f"evidence.sh doesn't count a bug-mode learnings file (one with an Escaped line) as bug mode: {modes!r}")
os.remove(bf)
# evidence.sh options are stable (STABILITY.md § Stable item 7): the field names of --json (none renamed
# or removed; new ones may be added), --out files, a bad option fails
r = run(["bash", "scripts/evidence.sh", "--json"], t)
Expand Down
2 changes: 1 addition & 1 deletion skills/morph-init/references/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ it after install rather than guessing a repo name.
| Supabase | Supabase | user connector | Read-only checks on remote (list tables, advisors, logs) in data-access stage B. Account-wide: record the project ref to target in `project/STACK.md`. |
| Error tracking in use (Sentry, PostHog, etc.) | That vendor's connector | user connector | Lets the TL and QA read real errors when planning a fix. |
| Hosting, DNS, storage or app builds with an official MCP (Vercel, Cloudflare, Expo, etc.) | That vendor's connector | user connector | Read deploy status and logs, and let the Technical Lead do setup work (create a project, set a non-secret variable, add a domain) instead of handing it to you. Every write is asked in chat first, one approved action at a time. |
| Issue tracker (Linear, Jira, GitHub Issues) | That vendor's connector | user connector | Lets the TL pull the ask and acceptance notes straight into `feature.md`. |
| Issue tracker (Linear, Jira, GitHub Issues) | That vendor's connector | user connector | Lets the TL pull the ask and acceptance notes straight into `plan.md` § Request. |

A remote connector (such as Context7's hosted server) can't be pinned: its text comes from the vendor's
server at each call. Say so when suggesting it.
Expand Down
4 changes: 1 addition & 3 deletions template/_harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ features/
<feature-slug>/
<version>/
Plan/
feature.md
plan.md
plan.md # TL: the request, the plan and its acceptance criteria
review.md # TL: code review findings (risk path or Full-stack xhigh)
retro.md # TL: retrospective (after QA pass or escalation)
Research/
<topic>.md # researcher: optional research before the plan
Legal/
Expand Down
2 changes: 1 addition & 1 deletion template/_harness/learnings/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Each feature's retro entry and its new action items live in their own file here, `<feature-slug>-v<version>.md`, so two feature branches never edit the same lines (a shared learnings file is the most common merge conflict between parallel branches).

- **Written by** the Technical Lead with the feature's retro, on the feature branch ([harness/SETUP.md](../../harness/SETUP.md) § Retrospective).
- **Format:** a `# <feature-slug> v<version> learnings` title, the entry in the format of [`LEARNINGS.md`](../LEARNINGS.md) § Entry format, then `## Action items` with a table in the columns of `LEARNINGS.md` § Action items → Open (`From retro | # | Type | Action | Why raised | Status | Note`), or "None".
- **Format:** [harness/templates/learnings-entry.md](../../harness/templates/learnings-entry.md): a `# <feature-slug> v<version> learnings` title, the entry, `## What went wrong` (one line per miss, or `None.`), then `## Action items` with a table in the columns of `LEARNINGS.md` § Action items → Open (`From retro | # | Type | Action | Why raised | Status | Note`), or "None".
- **Decisions:** the approve / skip / defer is the Technical Lead's, except an item that adds a step for the owner, changes product behaviour or customer-facing copy, or costs money, which the owner decides. It is recorded in the Status column of this file; an item applied later names the feature or file in Note.
- **Prune counter:** the number of `*-v*.md` files here is the number of features since the last prune (`bash scripts/prune-counter.sh` counts them with the integration branch's and prints the line for the hand-off); the prune is due at `loop.prune_every` in `harness.config.yml`.
- **Folded by** the prune: it condenses each entry to one line in `LEARNINGS.md` § Entries, moves each action item row to the matching `LEARNINGS.md` table, and deletes the file.
9 changes: 4 additions & 5 deletions template/harness/HANDOFFS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@

| File | Created by | May edit | Notes |
|------|------------|----------|--------|
| `Plan/feature.md` | Human (draft OK with Tech Lead) | Human; Tech Lead before plan approved | Plain-language intent |
| `Plan/plan.md` | Technical Lead | Tech Lead until approved; then frozen except Status, Steps file, Approval, Open questions, Decisions, and a small plan's § Check | Includes acceptance criteria |
| Steps file | Technical Lead | Technical Lead; the human ticks items | The human's steps after QA pass, or none (§ Steps file below; [SETUP.md](./SETUP.md) § After QA pass, step 4) |
| `Plan/review.md` | Technical Lead | Technical Lead only | Pre-QA code review findings: plans on a risk path or at Full-stack `xhigh` ([SETUP.md](./SETUP.md) § Code review before QA) |
| `Plan/retro.md` | Technical Lead | Technical Lead only | Retrospective after pass or escalation ([templates/retro.md](./templates/retro.md), [SETUP.md](./SETUP.md) § Retrospective) |
| `Legal/legal-copy.md` | Legal writer | Legal writer only | Exact legal sentences and their checks; only when the plan says **Legal wording: yes** ([modules/legal/](./modules/legal/README.md)) | <!-- module:legal -->
| `_harness/research/<date>-<slug>.md`, `Research/<topic>.md` | Technical Lead | TL, all of it: above the first line and below the second (§ Check, § Decision, § How it went) in its own words, and between the two lines the sections of the researcher's reply, word for word (the researcher has no edit tools; [templates/research.md](./templates/research.md)) | One question with cited findings ([MODES.md](./MODES.md) § Research mode) |
| `Development/progress.md` | Full-stack engineer | Full-stack engineer only | Tracker and validation |
| Project fact docs (`project/PROJECT.md` facts, domain docs) | Install, then plans | Full-stack for the facts a plan's steps name; the Technical Lead otherwise | Rule and process docs in `project/` stay the Technical Lead's ([agents/fullstack-engineer.md](./agents/fullstack-engineer.md) § Ownership limits) |
| `QA/qa-result.md` | QA engineer | QA engineer only | Verdict and evidence |
| `_harness/learnings/<slug>-v<version>.md` | Technical Lead | Technical Lead; the prune folds it | Retro entry and action items ([templates/learnings-entry.md](./templates/learnings-entry.md)) |
| `_harness/learnings/<slug>-v<version>.md` | Technical Lead | Technical Lead; the prune folds it | The feature's closing record: verdict, cost, what went wrong and action items ([templates/learnings-entry.md](./templates/learnings-entry.md)) |

**Application code:** Full-stack engineer, or the main chat itself in light and small mode, where no subagent runs ([MODES.md](./MODES.md) § Working alone). **No product code edits:** QA engineer, the Technical Lead in full and bug mode, researcher. One exception: in bug mode QA writes the regression test the plan assigns, before the fix ([MODES.md](./MODES.md) § Bug mode).

Expand Down Expand Up @@ -128,9 +127,9 @@ Read: <abs-path>/Plan/plan.md (must be approved).
Implement the plan in the application codebase.
Add or update a test for each behaviour the change adds or changes (.claude/rules/testing.md): one that fails when that behaviour breaks.
Path rules: <each .claude/rules/*.md file whose paths: match the plan's files, such as data.md for a migration, or none>. Read them before the first edit.
Update only: <abs-path>/Development/progress.md
Update only: <abs-path>/Development/progress.md, and the project fact docs the plan's steps name (never their source-path list, never a doc of rules or process)
Include Started and Ended in progress.md when handing off.
Do not edit plan.md, feature.md, qa-result.md, the steps file or Legal/legal-copy.md.
Do not edit plan.md, qa-result.md, the steps file or Legal/legal-copy.md.
Run every before_qa check in harness/CONTROLS.md § Before Full-stack → QA handoff (commands: project/CONTROLS.md), scoped by § Scoping by what changed; record each check, its scope and exit code in progress.md § Validation (required before handoff). The checks.on_stop checks are recorded as end-of-task hook: they run when you finish (harness/CONTROLS.md § End-of-task checks).
Do not mark Ready for QA until validation is recorded.
Run long checks to completion before handing back. A check longer than the shell's foreground limit runs as a background command with a long enough time limit, and you wait for it to finish in the same turn. A job that outlives your turn is not allowed.
Expand Down
Loading
Loading