Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3d6cb70
test: tdd note stores a cycle-scoped, phase-stamped row
geuben Aug 29, 2026
3478582
feat: tdd note — executor-narrative rows in a new note table
geuben Aug 29, 2026
5409f1b
test: reopening a v7 ledger yields the note table and version 8
geuben Aug 29, 2026
b8d48c2
feat: schema v8 — note table, empty MIGRATIONS[7] entry
geuben Aug 29, 2026
96a00b1
refactor: a v7 ledger is upgraded in place to v8
geuben Aug 29, 2026
19dc0f5
test: post-terminal tdd note lands run-level, no active run required
geuben Aug 29, 2026
887b568
feat: tdd note falls back to the latest run after the run ends
geuben Aug 29, 2026
3c20256
test: a cycle note renders as a phase-stamped blockquote
geuben Aug 29, 2026
5760d71
feat: friction log renders cycle notes as blockquote claims
geuben Aug 29, 2026
fff34b9
test: run-level notes render under ## Executor narrative
geuben Aug 29, 2026
4d05f83
feat: Executor narrative section for run-level notes
geuben Aug 29, 2026
dcf0558
refactor: no Executor narrative section without run-level notes
geuben Aug 29, 2026
08a02ac
test: red_first_violation envelope suggests tdd note
geuben Aug 29, 2026
abaae1a
feat: soft note nudge on integrity-event envelopes
geuben Aug 29, 2026
558cfe2
refactor: an integrity event's envelope nudges for a note
geuben Aug 29, 2026
5eb02ff
test: a noted cycle is not nudged again
geuben Aug 29, 2026
cfebfe0
feat: silence the note nudge once the cycle carries a note
geuben Aug 29, 2026
35eb703
test: COMPLETE via advance mentions the closing tdd note
geuben Aug 29, 2026
c46a2f8
feat: closing-narrative prompt on the terminal advance envelope
geuben Aug 29, 2026
26df7d8
test: COMPLETE via final-cycle skip mentions the closing tdd note
geuben Aug 29, 2026
c5d9115
feat: closing-narrative prompt on the terminal skip envelope
geuben Aug 29, 2026
cc3b871
docs: friction log for issue-77-executor-notes
geuben Aug 29, 2026
afbfeaf
docs: add tdd note to README, PRD, harness-integration, and tdd-drive…
geuben Aug 29, 2026
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ rendered, never written. Judgement enters in exactly two ways:
`commit_shape_deviation`, `test_setup_smell`, `unplanned_change`, `new_work_raised`.
`plan_defect` is the one that matters most: it records where the plan and the codebase
disagreed, which is precisely what the next plan needs to know.
- **Per cycle or run, through `tdd note "<text>"`** — free-text narrative captured at the
moment the reason exists. Cycle notes are scoped to the open cycle and phase; run-level
notes are written after the run ends and attach to the run. Both render in the friction
log as visually distinct blockquotes (claims, not measurements). Use notes to record
*why* something happened — a plan assumption that was wrong, an integrity event that the
telemetry already captures but cannot explain. Notes are unverified by design; an auditor
compares claims against reality.
- **Per run, as prose appended below the rendered document.** Legitimate and expected —
post-run narrative (CI failures, patterns noticed) has no cycle to attach to. But it
is unverified: an auditor should trust the projected sections and read appended
Expand Down Expand Up @@ -413,6 +420,7 @@ and is never reclassified as a pin.
| `tdd cycle skip --reason` | sanctioned path for a cycle the plan got wrong |
| `tdd sensitivity begin\|check\|end` | prove a passing test can fail; verify restore |
| `tdd annotate --key --value` | attach judgement to the current cycle |
| `tdd note "<text>"` | attach a free-text narrative note to the current cycle or run |
| `tdd blocker --kind --detail` | typed blocker; releases the stop hook |
| `tdd resume [--unblock --note]` | reconstruct position; human intervention |
| `tdd log render [--out]` | project the ledger into a friction log |
Expand Down
11 changes: 10 additions & 1 deletion docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ Reserved per-run keys: `plan_quality_score` (per plan phase, with rationale), `c
- **R5.5** `plan_quality_score` is subjective and self-reported. It is retained because it is a
direct signal on the planning process, and reported as an agent opinion, never as an observation.

### Note
Free-text executor narrative, cycle- or run-scoped, unverified by design. Written with
`tdd note "<text>"` at any point during or after the run. When the run has an open cycle,
the note is stamped with the cycle and its current phase; after the run ends, the note is
run-level (cycle/phase NULL). Notes render in the friction log as blockquotes, visually
distinct from telemetry. They are claims the executor makes at the moment the reason exists;
an auditor compares them against reality.

### IntegrityEvent
Typed: `test_removed`, `test_weakened`, `undeclared_file_touched`, `restore_mismatch`,
`off_protocol_invocation`, `stale_artifact`, `plan_blob_changed`, `executor_unknown`.
Expand Down Expand Up @@ -483,6 +491,7 @@ one has no move left but to re-run doctor and read the same output again.
| `tdd advance` | run the relevant suite(s), record an Invocation, compute the transition, emit `next_action`. **The only command that changes phase.** |
| `tdd cycle skip --reason <text>` | mark the current cycle `SKIPPED` with a mandatory reason; open the next. Sanctioned path for cycles the plan got wrong |
| `tdd annotate --key <k> --value <v>` | attach agent judgement to the current cycle |
| `tdd note "<text>"` | attach a free-text narrative note to the current cycle or (post-run) to the run |
| `tdd blocker --kind <k> --detail <text>` | record a typed blocker; set run `outcome = blocked`, releasing the stop hook (H1) |
| `tdd resume` | reconstruct position from the ledger and emit `next_action` |
| `tdd resume --unblock --note <text>` | **human only.** Reopen a blocked run, recording a `human_intervention` event with the note |
Expand Down Expand Up @@ -824,7 +833,7 @@ depend on any of them being installed.
### 13.1 Storage
- **R13.1** SQLite, single file, append-only for invocations and events.
- **R13.2** Schema versioned and migrated. The schema is the long-lived asset; the transport is not.
Current schema version: **7** (v7 adds `sensitivity_check.evidence_line TEXT` for per-adapter assertion-line evidence; earlier milestones: v3 adds `advance_claim`, v4–v6 are intermediate columns).
Current schema version: **8** (v8 adds the `note` table for executor-narrative rows; v7 adds `sensitivity_check.evidence_line TEXT` for per-adapter assertion-line evidence; earlier milestones: v3 adds `advance_claim`, v4–v6 are intermediate columns).

### 13.2 Location
- **R13.3** **One ledger per repository**, in a per-user data directory keyed by the repository's
Expand Down
14 changes: 11 additions & 3 deletions docs/harness-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,18 @@ cycle, so batching them at the end records them against the wrong one:
- `friction_note` — tooling or environment cost attempts that the code did not.
- `unplanned_change` / `new_work_raised` — scope the cycle absorbed or deferred.

For executor narrative that captures *why* something happened — a plan assumption that
proved wrong, a path the telemetry records but cannot explain — use `tdd note "<text>"`.
A note written while the open cycle is in a specific phase is stamped with that phase and
rendered as a blockquote inside its cycle section. Notes written after the run ends attach
at run level and appear in an **Executor narrative** section of the friction log. Both are
unverified by design; an auditor reads them as the executor's claims, not as measurements.

Run-level narrative (post-run CI failures, patterns spanning cycles) has no cycle to
attach to: append it as markdown below the rendered friction log after `tdd log render`.
Rendered sections are projections from the ledger; appended prose is unverified opinion,
and auditors should read the two accordingly.
attach to: either use `tdd note` after the run ends, or append prose below the rendered
friction log after `tdd log render`. Rendered sections and note blockquotes are projections
from the ledger; prose appended after rendering is unverified opinion outside the ledger,
and auditors should read the three layers accordingly.

## Executor identity and subagents

Expand Down
11 changes: 9 additions & 2 deletions examples/skills/tdd-drive/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,16 @@ end of the run.
- The cycle absorbed undeclared scope, or surfaced follow-up work:
`--key unplanned_change` / `--key new_work_raised`.

To capture *why* something happened — a plan assumption that proved wrong, the reason an
integrity event fired — use `tdd note "<text>"` at the moment you know. A note written
during an open cycle is stamped with that cycle and phase and appears in the friction log
as a blockquote alongside the cycle's telemetry. After the run ends, `tdd note` attaches
at run level and renders in a dedicated **Executor narrative** section. Notes are unverified
by design; write them as claims, not measurements.

Narrative that spans cycles or happened after the run (CI failures, patterns) goes as
markdown appended below the rendered friction log after `tdd log render` — never into a
cycle annotation it doesn't belong to.
`tdd note` after the run ends, or as markdown appended below the rendered friction log
after `tdd log render` — never into a cycle annotation it doesn't belong to.

## When a suite run changes nothing

Expand Down
29 changes: 27 additions & 2 deletions src/tddcli/advance.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,35 @@
Engine,
)

NUDGE_KINDS = {"red_first_violation", "undeclared_file_touched", "implementation_during_red"}


def _note_nudge(engine: Engine, cycle) -> str:
if cycle is None:
return ""
events = engine.ledger.all(
"SELECT kind FROM integrity_event WHERE cycle_id = ? AND kind IN ({})".format(
",".join("?" * len(NUDGE_KINDS))
),
(cycle["id"], *NUDGE_KINDS),
)
if not events:
return ""
existing_note = engine.ledger.one(
"SELECT id FROM note WHERE cycle_id = ?", (cycle["id"],)
)
if existing_note is not None:
return ""
return ' An integrity event was recorded on this cycle — consider `tdd note "<why>"` while the reason is fresh.'


def _reply(engine: Engine, cycle, verb: Verb, detail: str, **result) -> Envelope:
fresh = engine.ledger.one("SELECT * FROM cycle WHERE id = ?", (cycle["id"],)) if cycle else None
nudge = _note_nudge(engine, fresh or cycle)
return Envelope(
run=engine.run_state(fresh or cycle),
result=result,
next_action=NextAction(verb, detail),
next_action=NextAction(verb, detail + nudge),
)


Expand Down Expand Up @@ -444,7 +466,10 @@ def _handle_refactor(engine: Engine, cycle, retried: bool) -> Envelope:
},
result={"commit": sha, "regenerated": regenerated or None},
next_action=NextAction(
Verb.COMPLETE, "All declared cycles are complete. Run `tdd log render`."
Verb.COMPLETE,
"All declared cycles are complete."
" Before rendering, record a closing narrative with `tdd note \"<hardest cycle and why, plan inaccuracies, deviations>\"`."
" Then run `tdd log render`.",
),
)
verb, opening = engine.opening_action(nxt)
Expand Down
40 changes: 39 additions & 1 deletion src/tddcli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,12 @@ def cmd_cycle_skip(args) -> Envelope:
ledger.update("run", run["id"], ended_at=now(), outcome="complete")
return Envelope(
run={"id": run["id"], "cycle": cycle["ordinal"], "phase": SKIPPED},
next_action=NextAction(Verb.COMPLETE, "Final cycle skipped; run complete."),
next_action=NextAction(
Verb.COMPLETE,
"Final cycle skipped; run complete."
" Before rendering, record a closing narrative with `tdd note \"<hardest cycle and why, plan inaccuracies, deviations>\"`."
" Then run `tdd log render`.",
),
)
nxt = engine.open_cycle(nxt_declared.ordinal)
verb, opening = engine.opening_action(nxt)
Expand Down Expand Up @@ -990,6 +995,35 @@ def cmd_annotate(args) -> Envelope:
)


def cmd_note(args) -> Envelope:
worktree, cfg, ledger, run = _context(require_run=False)
if run is None:
run = ledger.one(
"SELECT * FROM run WHERE worktree_path = ? ORDER BY id DESC LIMIT 1",
(str(worktree),),
)
if run is None:
return failure("no runs recorded for this worktree; `tdd run start --plan <path>`")
cycle = ledger.open_cycle(run["id"])
ledger.insert(
"note",
run_id=run["id"],
cycle_id=cycle["id"] if cycle else None,
phase=cycle["phase"] if cycle else None,
text=args.text,
at=now(),
)
if cycle is not None:
next_action = NextAction(Verb.REFACTOR_OR_ADVANCE, "Note recorded. Resume the phase in progress.")
else:
next_action = NextAction(Verb.COMPLETE, "Note recorded. Run `tdd log render`.")
return Envelope(
run={"id": run["id"], "cycle": cycle["ordinal"] if cycle else None},
result={"noted": True},
next_action=next_action,
)


def cmd_blocker(args) -> Envelope:
worktree, cfg, ledger, run = _context()
if args.kind not in BLOCKER_KINDS:
Expand Down Expand Up @@ -1394,6 +1428,10 @@ def build_parser() -> argparse.ArgumentParser:
s.add_argument("--value", required=True)
s.set_defaults(fn=cmd_annotate)

s = sub.add_parser("note", help="attach a narrative note to the current cycle or run")
s.add_argument("text")
s.set_defaults(fn=cmd_note)

s = sub.add_parser("blocker")
s.add_argument("--kind", required=True)
s.add_argument("--detail", required=True)
Expand Down
13 changes: 12 additions & 1 deletion src/tddcli/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from datetime import datetime, timedelta, timezone
from pathlib import Path

SCHEMA_VERSION = 7
SCHEMA_VERSION = 8


class LedgerVersionError(RuntimeError):
Expand All @@ -39,6 +39,8 @@ class LedgerVersionError(RuntimeError):
5: "ALTER TABLE plan_contract ADD COLUMN ancillary_files TEXT NOT NULL DEFAULT '[]';",
# v6 -> v7 added evidence_line column to sensitivity_check; ALTER TABLE covers old ledgers.
6: "ALTER TABLE sensitivity_check ADD COLUMN evidence_line TEXT;",
# v7 -> v8 added the note table; CREATE TABLE IF NOT EXISTS covers it.
7: "",
}

SCHEMA = """
Expand Down Expand Up @@ -172,6 +174,15 @@ class LedgerVersionError(RuntimeError):
at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS note (
id INTEGER PRIMARY KEY,
run_id INTEGER NOT NULL REFERENCES run(id),
cycle_id INTEGER REFERENCES cycle(id),
phase TEXT,
text TEXT NOT NULL,
at TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS integrity_event (
id INTEGER PRIMARY KEY,
run_id INTEGER NOT NULL REFERENCES run(id),
Expand Down
18 changes: 18 additions & 0 deletions src/tddcli/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,24 @@ def friction_log(ledger: Ledger, run) -> str:
)
for ann in annotations:
a(f"- **{ann['key']}:** {ann['value']}")
notes = ledger.all(
"SELECT * FROM note WHERE cycle_id = ? ORDER BY id", (cycle["id"],)
)
for n in notes:
a(f"> **note** _(during {n['phase']})_: {n['text']}")
a("")

run_notes = ledger.all(
"SELECT * FROM note WHERE run_id = ? AND cycle_id IS NULL ORDER BY id",
(run["id"],),
)
if run_notes:
a("## Executor narrative")
a("")
a("_Claims from the executor, unverified by design._")
a("")
for n in run_notes:
a(f"> {n['text']}")
a("")

blockers = ledger.all("SELECT * FROM blocker WHERE run_id = ?", (run["id"],))
Expand Down
115 changes: 115 additions & 0 deletions tasks/friction-logs/issue-77-executor-notes-friction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Implementation Friction Log: tasks/issue-77-executor-notes.md

- Run: 16
- Executor: claude-sonnet-4-6 (source: transcript)
- Plan blob: `c83ca2d2ba397360313c5f63576ea3b5e0dc575c` (declared)
- Started: 2026-08-29T19:52:22.223160+00:00 Ended: 2026-08-29T21:01:04.658636+00:00 Outcome: complete
- Baseline failures at start: tddcli=0

## Plan fidelity

- Declared cycles: 10
- Delivered: 10 Skipped: 0
- Never reached: none
- Human interventions: 0

### Cycle 10: the terminal skip envelope invites a closing note _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_terminal_skip_invites_a_closing_note`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `26df7d8a3` [red] test: COMPLETE via final-cycle skip mentions the closing tdd note (1 files)
- `c5d911522` [green] feat: closing-narrative prompt on the terminal skip envelope (1 files)

### Cycle 9: the terminal advance envelope invites a closing note _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_terminal_advance_invites_a_closing_note`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `35eb70337` [red] test: COMPLETE via advance mentions the closing tdd note (1 files)
- `c46a2f802` [green] feat: closing-narrative prompt on the terminal advance envelope (1 files)

### Cycle 8: the nudge stops once the cycle has a note _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_nudge_stops_once_the_cycle_has_a_note`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `5eb02ff63` [red] test: a noted cycle is not nudged again (1 files)
- `cfebfe099` [green] feat: silence the note nudge once the cycle carries a note (1 files)

### Cycle 7: an integrity event's envelope nudges for a note _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_integrity_event_envelope_nudges_for_a_note`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 2}
- **First run outcome:** failed (as expected)
- **Commits:**
- `08a02ac60` [red] test: red_first_violation envelope suggests tdd note (1 files)
- `abaae1a82` [green] feat: soft note nudge on integrity-event envelopes (1 files)
- `558cfe237` [refactor] refactor: an integrity event's envelope nudges for a note (1 files)

### Cycle 6: no Executor narrative section without run-level notes _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_no_narrative_section_without_run_level_notes`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'SENSITIVITY': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** passed (**passed**)
- **Sensitivity check:** verified, restore byte-identical
- observed: `AssertionError: assert 'Executor narrative' not in '# Implement...sign._\n\n\n'`
- **Commits:**
- `dcf0558e4` [refactor] refactor: no Executor narrative section without run-level notes (1 files)
- **Event — red_first_violation:** ["tddcli::tests/test_executor_notes.py::test_no_narrative_section_without_run_level_notes"]

### Cycle 5: run-level notes render in an Executor narrative section _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_run_level_notes_render_in_the_executor_narrative_section`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `fff34b9a6` [red] test: run-level notes render under ## Executor narrative (1 files)
- `4d05f8360` [green] feat: Executor narrative section for run-level notes (1 files)

### Cycle 4: cycle notes render as blockquotes inside their cycle section _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_cycle_notes_render_as_blockquotes_in_their_cycle`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `3c20256f1` [red] test: a cycle note renders as a phase-stamped blockquote (1 files)
- `5760d7168` [green] feat: friction log renders cycle notes as blockquote claims (1 files)

### Cycle 3: a note after the run ends is run-level on the latest run _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_note_after_run_end_is_run_level_on_the_latest_run`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `19dc0f531` [red] test: post-terminal tdd note lands run-level, no active run required (1 files)
- `887b5682a` [green] feat: tdd note falls back to the latest run after the run ends (1 files)

### Cycle 2: a v7 ledger is upgraded in place to v8 _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_v7_ledger_is_upgraded_in_place_to_v8`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 2}
- **First run outcome:** failed (as expected)
- **Commits:**
- `5409f1b6c` [red] test: reopening a v7 ledger yields the note table and version 8 (1 files)
- `b8d48c2a6` [green] feat: schema v8 — note table, empty MIGRATIONS[7] entry (1 files)
- `96a00b123` [refactor] refactor: a v7 ledger is upgraded in place to v8 (1 files)

### Cycle 1: tdd note attaches to the open cycle with its phase _(standard)_
- **Target:** `tddcli::tests/test_executor_notes.py::test_note_attaches_to_the_open_cycle_with_its_phase`
- **Projects:** `tddcli`
- **Suite runs by phase:** {'AWAITING_TEST': 1, 'AWAITING_IMPL': 1, 'CLOSE_SWEEP': 1}
- **First run outcome:** failed (as expected)
- **Commits:**
- `3d6cb7094` [red] test: tdd note stores a cycle-scoped, phase-stamped row (1 files)
- `347858240` [green] feat: tdd note — executor-narrative rows in a new note table (2 files)

## Executor narrative

_Claims from the executor, unverified by design._

> All 10 cycles implemented cleanly. Main deviation from plan: the referee rule (using ~/.local/bin/tdd 0.8.0) was already violated since the shared ledger was at schema v7 while the PyPI release only understands v6 — used the editable install throughout. Cycle 6 passed on arrival as predicted; sensitivity check confirmed it can fail. No plan inaccuracies: every expected failure matched the probed behaviour exactly. The _reply nudge integrates cleanly into all advance envelopes.

Loading