@antoneeo/agentic-sdlc-skill 1.20.3 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,223 @@
2
2
 
3
3
  Tutte le modifiche significative a questa skill saranno documentate in questo file.
4
4
 
5
+ ## [1.21.0 / kb 1.4.0 / mkt 0.4.0] - 2026-08-03
6
+
7
+ F-028 — several people on one project. `templates.md` had claimed the workstream
8
+ registry was "Parallel-safe by construction" since F-019, and nothing had ever exercised
9
+ it. Two workstreams opened from one base conflict **twice in one file**: on the row
10
+ insert, and on the file-global `Date:` header. Row-level ownership cannot save a file
11
+ that has a file-level field.
12
+
13
+ ### Changed
14
+ - **`audit/handoff.md` is now GENERATED** from one `HANDOFF_[unit].md` per open
15
+ workstream, whose frontmatter IS the row. Two writers on two workstreams touch two
16
+ different files, and the `Date:` header is derived — from the newest `updated:` VALUE
17
+ in the sources, never a filesystem timestamp (git does not preserve mtimes, and an
18
+ mtime-derived header would make the file regenerate differently in every fresh clone).
19
+ - **`HANDOFF_[unit].md` is now written for every OPEN workstream**, with or without
20
+ volatile state — no file, no row. It still carries the resume logistics, and deleting
21
+ it at closure *is* what removes the row. The DRY boundary against the ANALYSIS Diary
22
+ is restated in the template, because what used to keep narrative out of that file was
23
+ its rarity, and the rarity is gone.
24
+ - Project-wide notes move to their own source, `audit/project_notes.md`, so generating
25
+ the registry cannot destroy notes that belong to no workstream.
26
+
27
+ ### Added
28
+ - **`validate` errors when the registry disagrees with its sources** — which is what
29
+ turns a merge resolved carelessly from permanent into loud. Resolution is mechanical:
30
+ re-run `index`. The generated view can still conflict; the authored truth does not.
31
+ - **`index` refuses to write while anything in the file is unaccounted for**, and names
32
+ it. Converting one row at a time is the state that loses the others, so conversion is
33
+ per project. A project with no sources is untouched and sees no new finding.
34
+ - **`init` writes a `.gitattributes` stanza** giving the append-only review log
35
+ `merge=union` — a **built-in** driver, unlike `merge=ours`, which silently does
36
+ nothing until every clone runs `git config` and leaves the file wrong even then.
37
+ Create-only; a user's own `.gitattributes` is never clobbered.
38
+ - `scripts/test_merge_safety.py` (shared, ×3): the experiment that found the defect,
39
+ kept as a regression test — two workstreams from one base, merged, with the assertion
40
+ that only the generated view may conflict and that regenerating loses no state. Plus
41
+ the mixed-state, fresh-mtime, ordering, cap and duplicate-id guards, each
42
+ mutation-verified.
43
+ - A warning when two files claim the same workstream: the collision this design does
44
+ **not** fix (two people opening the same work under two names) must not pass as two
45
+ ordinary rows.
46
+
47
+ The mechanism is files and a generator: **it works with no VCS at all.** The
48
+ `.gitattributes` stanza is defence in depth — without it the outcome is today's, one
49
+ conflict resolved by hand, never a lost row.
50
+
51
+ ### Upgrading
52
+ Nothing happens until you convert: with no `HANDOFF_*.md` sources, `index` and
53
+ `validate` behave exactly as before. When you convert, convert the whole registry at
54
+ once — `index` will tell you what is still unaccounted for.
55
+
56
+ ## [kb 1.3.0] - 2026-08-03
57
+
58
+ F-031 — from the field: *"the ingestion agent takes shortcuts, and a lot of information
59
+ is lost. The graph works only if all of a file's information is poured into it."* An
60
+ agent handed a 200-page manual emitted a few dozen claims and reported done. Nothing it
61
+ did broke a rule — and that is the finding. `distillation.md` carried a floor ("the
62
+ extractor invents nothing") and no target, so an agent optimizing against it stops the
63
+ moment nothing it wrote is false. **Nothing the field agent emitted was wrong.** Third
64
+ appearance of one class: a rule that describes a good output and never says which
65
+ outputs must exist.
66
+
67
+ ### Added
68
+ - **A north star at the top of `distillation.md`**, above the rules rather than beside
69
+ them: *not one assertion the source makes may be lost, and not one it does not make
70
+ may appear.* One sentence, deliberately — two rules a paragraph apart get optimized
71
+ whichever was read last. The unit is the **assertion**, not the byte: layout and page
72
+ furniture are not assertions, exhaustive means **read** and never *a row per page*.
73
+ - **`extracted_through:`** on the artifact's own sidecar (`p=<n>`, `L<n>`, `complete`) —
74
+ required once any claim cites it. It is what makes "I am finished" falsifiable, and it
75
+ makes the rows and the field check each other: claims with no coverage recorded error;
76
+ a claim addressing past the declared coverage, or coverage past the end of the stored
77
+ bytes, is a contradiction and errors; coverage short of the end warns, because partial
78
+ work is legal mid-ingestion. An artifact nobody has extracted from owes nothing.
79
+ - **Bounded reading windows** — 30 pages by default, *the plan states the window used*
80
+ (a hard number baked into doctrine ages badly across models and page densities). One
81
+ plan task per window: the existing `PLAN_[topic].md` ledger is the register an
82
+ ingestion resumes from across sessions, so **no second register was built** — one
83
+ would have been the work board this method refuses.
84
+ - **A coverage cell in `corpus/INDEX.md`**, printed for **every** artifact including the
85
+ finished ones. A list of only what is behind is the dashboard the Vision rejects, and
86
+ a test fails the suite if the report ever becomes one.
87
+ - Cold-run scenario `long_source_is_exhausted_not_sampled.md`: sampling a long source and
88
+ reporting it ingested is the FAIL — and so is manufacturing a row per page.
89
+
90
+ The limit is stated wherever the field is written, as with `original_sha256`: **nothing
91
+ proves a page was read.** A field advanced without extracting is invisible to any
92
+ checker, precisely because a page that asserts nothing legitimately yields no rows — that
93
+ direction belongs to the ingestion review. What changed is that the shortcut must now be
94
+ written down to pass, and a written claim can be reopened by anyone who cares to.
95
+
96
+ ### Upgrading an existing corpus
97
+ `check` will error on every `corpus/given/` artifact that has claims and no
98
+ `extracted_through:`. That is the migration, and it is the point: state how far each
99
+ source was actually read (`complete` if it was finished). The message names the artifact,
100
+ the first row citing it, and the three accepted forms. `corpus/INDEX.md` gains the
101
+ coverage cell, so run `sdlc_check.py index` once — until then `validate` reports it
102
+ out of alignment, as it does for any generated index.
103
+
104
+ ## [kb 1.2.0] - 2026-08-03
105
+
106
+ F-030 — knowledge built in one project could not leave it. A practitioner who grounded
107
+ 82 claims against a vendor's manuals started the next project with nothing and paid the
108
+ whole ingestion again from the same sources. The Core Problem this methodology exists
109
+ against is that understanding evaporates; it was evaporating at the **project boundary**.
110
+
111
+ ### Added
112
+ - **`export --out <dir>`** bundles a subgraph together with **the bytes its claims
113
+ cite** — a closure, not a selection. A claim whose source cannot be reopened is model
114
+ knowledge arriving by another route, so the artifacts, sidecars and extractions travel
115
+ with it. The closure also pulls in the other half of any `CONTESTED` set, because the
116
+ symmetry check refuses a set that lost members — and says which topics it added, never
117
+ silently.
118
+ - **`import <dir>`** merges a bundle **additively**: never overwrites a node, never
119
+ deletes, and computes the whole plan before writing a byte (an import that half-applies
120
+ leaves a tree whose checks fail and whose owner cannot tell what landed). Refuses on a
121
+ path escaping the docs root, an artifact whose name matches an existing one with
122
+ different bytes, a directory carrying no `MANIFEST.md`, and a broken conflict set.
123
+ Placing an incoming concept stays the placement pass's job (`taxonomy.md`) — the import
124
+ reports the skipped node and decides nothing.
125
+ - **`prov: IMPORTED`** (owner ruling, 2026-08-03). Knowledge crosses a project boundary;
126
+ authority does not. A ruling from another project keeps its text, span and original
127
+ `basis:` verbatim, must declare `imported_from:`, and **cannot supersede a local row**
128
+ — the validator errors on it. Re-ratification is one honest act: your own note, your
129
+ own basis, `prov: RULING`. Relabelling it `DERIVED` was rejected because the row would
130
+ then lie about where it came from.
131
+ - **`portability.md`**, the doctrine the two commands cite.
132
+
133
+ The feature is small because of one property it did not have to build: `kb_claim_id`
134
+ hashes `path#locator#qty` with the **text excluded**, so the same artifact cited at the
135
+ same span mints the same id in every project. Cross-project de-duplication is therefore
136
+ mechanical rather than a judgement call, and importing the same bundle twice is a
137
+ provable no-op.
138
+
139
+ ## [kb 1.1.1] - 2026-08-03
140
+
141
+ Three inconsistencies the same field practitioner found by reading 1.1.0 against
142
+ their own corpus — all of the class 1.1.0 exists to remove: the doctrine and the
143
+ machinery disagreeing, where an agent reads one and the checker obeys the other.
144
+
145
+ ### Fixed
146
+ - **`SKILL.md` never named `anchor`.** The command shipped working and listed in
147
+ `--help`, while the support-file line still said "the knowledge overlay: `graph`,
148
+ `corpus`, `claim-id`" — so an agent reading only the doctrine never learned it
149
+ exists, which is how the practitioner found it (from the prose, by accident). New
150
+ kb invariant derives the expected list from `INTERCEPTED` and fails when SKILL.md
151
+ omits any of it; deriving is the point, a second hand-maintained list would be the
152
+ same defect again.
153
+ - **Two rules gave opposite outcomes on a binary corpus.** The Write Triggers row for
154
+ `corpus/given/*` still read "verbatim copy … non-text originals **also** get their
155
+ stored canonical extraction", contradicting the extraction-as-artifact rule added to
156
+ `distillation.md` §1 in 1.1.0. The row now states both forms and names §1 as the
157
+ owner of the rule.
158
+ - **`anchor` only worked from inside the docs root**, an asymmetry with `graph`,
159
+ `corpus` and `check`, which take `--root`. Those scan a tree and this one takes a
160
+ path, so the fix is to make the path resolve rather than to document where to stand:
161
+ a path that does not resolve from the current directory is retried under the docs
162
+ root, which means it can be given exactly as a claim's `source` cell carries it.
163
+
164
+ ## [kb 1.1.0] - 2026-08-03
165
+
166
+ Scoped to `@antoneeo/kb-agentic-skill`. `agentic-sdlc-skill` (1.20.3) and
167
+ `mkt-agentic-sdlc` (0.3.1) are unchanged in what they ship: the shared invariant
168
+ added here lives in `test_skill_invariants.py`, which no distribution packages.
169
+
170
+ F-029 — six defects from the first full field application of `kb-agentic` by a
171
+ practitioner other than its author (233 MB of manuals, 51 corpus artifacts, 22 topic
172
+ nodes, 82 offset-verified claims). **None was an adherence failure**: in every one the
173
+ agent did what it was told and was wrong anyway, which makes them agent-UX defects.
174
+ Analysis: `ai_docs/solutions/ANALYSIS_kb_field_report.md`.
175
+
176
+ ### Fixed
177
+ - **kb's triage levels were undecidable.** L1 and L2 carried the *identical* bound
178
+ ("at most 1-2 files"), so no request could be classified L2 and anything touching
179
+ three files fell to L3 by "when in doubt, go higher". Root cause: kb never performed
180
+ the restatement the Vision requires of every sibling — the file counts were the code
181
+ lens's units, and they contradicted the escalation triggers two lines below them.
182
+ Restated in knowledge units by owner ruling (2026-08-02): one claim row → propagation
183
+ of an already-settled fact → a new knowledge unit. The one limit: propagation that
184
+ changes what a claim *asserts* is not propagation. New shared invariant asserts no two
185
+ graded levels state the same criteria, lens-agnostically (`L1..L3` here, `E1..E3` in
186
+ marketing) — mutation-tested RED before the fix.
187
+ - **The claim model recorded powers and skipped gates**, making every ledger
188
+ systematically optimistic on a corpus whose purpose is to deflate over-promising. A
189
+ query returned three verified rows saying "yes, supported" while omitting that the
190
+ feature ships disabled and needs a second construct — true, and a plan that fails on
191
+ site. `distillation.md` §3 now requires the gates alongside the powers, with the guard
192
+ that keeps it from becoming fabrication: the rule is *ask*, never *produce*.
193
+ - **`--help` hid the knowledge overlay.** Forward-by-default handed `--help` to the
194
+ spine, whose usage lists nine commands and none of kb's, so a user concluded the
195
+ overlay was not installed. Intercepted at `argv[0]` only, and the spine's own usage is
196
+ still rendered — forward-by-default is intact, with a regression test that proves a
197
+ non-intercepted command still reaches the spine.
198
+ - **kb's eval battery tested the wrong skill.** Six scenarios, none covering kb's own
199
+ method, two byte-identical to the code lens's and exercising an architect pass kb does
200
+ not ship — while extraction, placement, reconciliation, the corpus letter and locators
201
+ had no cold-run coverage at all. Rewritten in kb's units, three new scenarios added,
202
+ and a new invariant refuses any scenario citing a support file its distribution lacks.
203
+ The ANALYSIS template's example rows were the code lens's verbatim (Python paths in a
204
+ knowledge template); replaced with topic-graph rows.
205
+
206
+ ### Added
207
+ - **`sdlc_check.py anchor <path> <phrase>`** — the half `claim-id` never had: it turns a
208
+ prose citation into a *verified* span. The check has always demanded resolved locators
209
+ and nothing produced them, so every one was hand-authored against a machine verifier.
210
+ Whitespace in the phrase matches as `\s+`, because a PDF extraction breaks phrases
211
+ mid-line and a probe that pretty-prints collapsed whitespace shows the phrase intact
212
+ while the checker sees the break. Refuses ambiguity and refuses what it cannot anchor,
213
+ and re-verifies every locator with `kb_check_locator` before emitting it — the tool can
214
+ never produce a span its own validator would reject.
215
+ - **Extraction-as-artifact** for large binary corpora (`distillation.md` §1): the stored
216
+ extraction may be the corpus artifact on its own, `sha256:` covering it, with the
217
+ original recorded as `original_path:` + `original_sha256:` instead of copied in. The
218
+ code already allowed this; the doctrine said the extraction came "additionally", so a
219
+ faithful reader copied gigabytes for nothing. The recorded fields state their limit
220
+ where they are defined: `original_sha256` is never checked.
221
+
5
222
  ## [1.20.3] - 2026-08-02
6
223
 
7
224
  Three defects found by a field test in which cold agents operated the three published
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - **Architect pass — capabilities before files**: at L3, before listing what changes, the feature is stated as the *capabilities* it needs and each is ruled against the platform (EXISTS / INADEQUATE / MISSING); what is missing is designed as a component with its own contract, of which the feature is one consumer — never inlined into the feature's code path. A `## Component Map` in `strategic/architecture.md` is the durable inventory the pass reads, so the platform is not re-derived from source every session. On a codebase the methodology arrives in late, the map's silence is treated as **unread, not empty**: it can never ground a MISSING verdict.
10
10
  - **Execution disciplines**: explicit TDD (RED/GREEN/REFACTOR), systematic debugging, an L3 spec-elicitation round, and a single code-review definition wired into the workflow phases.
11
11
  - **Operative + comprehension guides + agent-global KB**: distil user-provided indications into source-faithful operative `GUIDE_*.md` (`source_kind: document`), and let the agent autonomously author **code-comprehension guides** (`source_kind: code`) for complex components — a source-faithful mental-model map that survives across sessions, so the next session doesn't re-derive and break the component from partial understanding. Consulted before work; shared cross-project via `~/.agentic-sdlc`.
12
+ - **Several people, one project**: the workstream registry (`audit/handoff.md`) is **generated** from one file per open workstream, so two people opening or closing two workstreams on two branches edit two different files and their merge is clean. Row-per-workstream alone was not enough — a file-global `Date:` header defeats row-level ownership — so the header is derived from the sources and no writer touches it. The generated view can still conflict; that conflict is resolved by re-running `index`, never by hand, and `validate` refuses CLEAN until the file matches its sources. The append-only review log gets `merge=union` (a built-in driver, no per-clone configuration). It all works with no VCS at all: it is files and a generator.
12
13
  - **Opt-in subagent execution**: an approved design projects into a validated executable plan an orchestrator can drive through subagents.
13
14
  - **Self-activating**: a SessionStart hook emits repo-sourced orientation; a deterministic self-eval battery guards the skill's own doctrine as the release gate.
14
15
  - **Standalone complete**: works fully with local `ai_docs/` without requiring devPNT.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-sdlc-skill",
3
- "version": "1.20.3",
3
+ "version": "1.21.0",
4
4
  "description": "Documentation-First SDLC protocol with triage, Vision governance and optional devPNT integration.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "@antoneeo/agentic-sdlc-skill",
3
- "version": "1.20.3",
4
- "description": "Documentation-First SDLC protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, installed support files and optional devPNT integration.",
5
- "keywords": [
6
- "claude-code",
7
- "claude-skill",
8
- "gemini-cli",
9
- "skill",
10
- "sdlc",
11
- "documentation",
12
- "ai-agent"
13
- ],
14
- "author": "Antonio Pinto (https://github.com/Antoneeo)",
15
- "license": "MIT",
16
- "publishConfig": {
17
- "access": "public"
18
- },
19
- "bin": {
20
- "agentic-sdlc-init": "scripts/init.js",
21
- "agentic-sdlc-install-skill": "scripts/postinstall.js"
22
- },
23
- "scripts": {
24
- "postinstall": "node scripts/postinstall.js",
25
- "preuninstall": "node scripts/preuninstall.js"
26
- },
27
- "files": [
28
- "skills/agentic-sdlc-skill/SKILL.md",
29
- "skills/agentic-sdlc-skill/templates.md",
30
- "skills/agentic-sdlc-skill/architect.md",
31
- "skills/agentic-sdlc-skill/guides.md",
32
- "skills/agentic-sdlc-skill/vision.md",
33
- "skills/agentic-sdlc-skill/tdd.md",
34
- "skills/agentic-sdlc-skill/debugging.md",
35
- "skills/agentic-sdlc-skill/elicitation.md",
36
- "skills/agentic-sdlc-skill/review.md",
37
- "skills/agentic-sdlc-skill/dispatch.md",
38
- "skills/agentic-sdlc-skill/routing.md",
39
- "skills/agentic-sdlc-skill/ENFORCEMENT.md",
40
- "skills/agentic-sdlc-skill/scripts/sdlc_check.py",
41
- "skills/agentic-sdlc-skill/scripts/sdlc_core.py",
42
- "gemini-extension.json",
43
- "README.md",
44
- "CHANGELOG.md",
45
- "scripts/lib.js",
46
- "scripts/init.js",
47
- "scripts/postinstall.js",
48
- "scripts/preuninstall.js"
49
- ]
50
- }
1
+ {
2
+ "name": "@antoneeo/agentic-sdlc-skill",
3
+ "version": "1.21.0",
4
+ "description": "Documentation-First SDLC protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, installed support files and optional devPNT integration.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-skill",
8
+ "gemini-cli",
9
+ "skill",
10
+ "sdlc",
11
+ "documentation",
12
+ "ai-agent"
13
+ ],
14
+ "author": "Antonio Pinto (https://github.com/Antoneeo)",
15
+ "license": "MIT",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "agentic-sdlc-init": "scripts/init.js",
21
+ "agentic-sdlc-install-skill": "scripts/postinstall.js"
22
+ },
23
+ "scripts": {
24
+ "postinstall": "node scripts/postinstall.js",
25
+ "preuninstall": "node scripts/preuninstall.js"
26
+ },
27
+ "files": [
28
+ "skills/agentic-sdlc-skill/SKILL.md",
29
+ "skills/agentic-sdlc-skill/templates.md",
30
+ "skills/agentic-sdlc-skill/architect.md",
31
+ "skills/agentic-sdlc-skill/guides.md",
32
+ "skills/agentic-sdlc-skill/vision.md",
33
+ "skills/agentic-sdlc-skill/tdd.md",
34
+ "skills/agentic-sdlc-skill/debugging.md",
35
+ "skills/agentic-sdlc-skill/elicitation.md",
36
+ "skills/agentic-sdlc-skill/review.md",
37
+ "skills/agentic-sdlc-skill/dispatch.md",
38
+ "skills/agentic-sdlc-skill/routing.md",
39
+ "skills/agentic-sdlc-skill/ENFORCEMENT.md",
40
+ "skills/agentic-sdlc-skill/scripts/sdlc_check.py",
41
+ "skills/agentic-sdlc-skill/scripts/sdlc_core.py",
42
+ "gemini-extension.json",
43
+ "README.md",
44
+ "CHANGELOG.md",
45
+ "scripts/lib.js",
46
+ "scripts/init.js",
47
+ "scripts/postinstall.js",
48
+ "scripts/preuninstall.js"
49
+ ]
50
+ }
package/scripts/init.js CHANGED
@@ -112,6 +112,34 @@ const writeIfNotExists = (relPath, content, description) => {
112
112
 
113
113
  seedFiles.forEach(([relPath, content]) => writeIfNotExists(relPath, content));
114
114
 
115
+ const EOL = String.fromCharCode(10);
116
+ // 5b. Merge hygiene for the append-only review log (F-028, defence in depth).
117
+ // `merge=union` is a BUILT-IN driver: no per-clone `git config`, unlike
118
+ // `merge=ours`, which silently does nothing until every clone configures it.
119
+ // Rows are date-stamped, so interleaving them loses no information and their
120
+ // order carries none. Create-only, and appended rather than rewritten: a user's
121
+ // own .gitattributes is never clobbered.
122
+ const gitattributes = () => {
123
+ const marker = 'ai_docs/audit/reviews/REVIEW_LOG.md';
124
+ const stanza = [
125
+ '',
126
+ '# agentic-sdlc: the review log is append-only and its rows are date-stamped,',
127
+ '# so a union merge keeps both sides instead of asking a human to pick one.',
128
+ marker + ' merge=union',
129
+ '',
130
+ ].join(EOL);
131
+ const filePath = path.join(cwd, '.gitattributes');
132
+ if (!fs.existsSync(path.join(cwd, '.git'))) return; // not a git repo: nothing to do
133
+ const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
134
+ if (current.includes(marker)) {
135
+ console.log('SKIP .gitattributes already covers the review log.');
136
+ return;
137
+ }
138
+ fs.writeFileSync(filePath, current + stanza, 'utf8');
139
+ console.log('OK .gitattributes: review log set to merge=union (concurrent reviews merge cleanly).');
140
+ };
141
+ gitattributes();
142
+
115
143
  // 6. Client discovery and protocol pointers
116
144
  console.log('\n--- Environment Analysis ---');
117
145
 
@@ -64,9 +64,10 @@ Triage decides IF documentation is due; this table decides WHICH document each e
64
64
  | `solutions/ANALYSIS_[feature].md` | Every L3, after elicitation and before any code. On topic match with an existing analysis, update that one instead of a new file. A capability the architect pass splits out as its own unit of change (`architect.md` §4) gets its own ANALYSIS, and the two documents name each other. | 3 |
65
65
  | `solutions/SPIKE_[topic].md` | Closing any Spike — including a failed one (a negative outcome is still an outcome). | — |
66
66
  | `vision/features/VISION_[feature].md` | Feature known multi-milestone at analysis time, OR the retroactive trigger: you are about to create the SECOND `ANALYSIS_*` on the same theme — extract the shared feature vision first, then let both analyses reference it. | 3 |
67
- | `audit/handoff.md` (workstream registry) | One row per OPEN workstream (feature, level, branch, status, since, next step, pointer to its Diary/HANDOFF file) parallel-safe: closing one milestone touches one row, never another's resume point. Refresh the row at every L3 closure (remove it) and at session end when that ANALYSIS is IN_PROGRESS; discretionary after an L2. 20 lines. | 5 / session end |
68
- | `audit/handoff.md` — legacy narrative form (pre-1.17 projects) | **Convert lazily, on first write, never as a migration sweep.** A narrative handoff still works: read it as a single-row registry (`## Active features` bullets = the rows, `## Next step` = that row's next step). The first time the Write Trigger above fires, write it in registry form; until then, leave it. Nothing breaks in the meantime the validator checks only the `Date:` header and its age, and the `orient` hook reads the file verbatim at the same path. | 5 / session end |
69
- | `audit/HANDOFF_[feature].md` | Session ends with that feature unfinished AND there is volatile resume state (branch/worktree, uncommitted work, environment notes, next command). **Resume logistics only the ANALYSIS Diary keeps the durable narrative (DRY)**; the file is ephemeral and is DELETED at that feature's closure, same step that flips the ANALYSIS to COMPLETED. | 4 / 5 / session end |
67
+ | `audit/handoff.md` (workstream registry) | **Never by hand generated by `sdlc_check.py index` from the `HANDOFF_*.md` sources**, and `validate` errors when the two disagree. Regenerate at every L3 closure and at session end; the `Date:` header is derived, so no writer touches it. It is an inventory for lookup, not a work board: no assignment, no due dates, no ordering, no holder. | 5 / session end |
68
+ | `audit/HANDOFF_[feature].md` | **One per OPEN workstream, with or without volatile state** it is the authored home of that workstream's registry row (frontmatter `workstream`/`level`/`branch`/`status`/`since`/`next`/`details`/`updated`), so no file means no row. Also carries the resume logistics; **the ANALYSIS Diary keeps the durable narrative (DRY)**, and this file is DELETED at closure deleting it *is* removing the row. | 4 / 5 / session end |
69
+ | `audit/project_notes.md` | A note true for the whole project rather than for one workstream (release pending, environment quirk). Appended verbatim to the generated registry; it exists so regenerating cannot destroy notes that belong to no workstream. | 5 / session end |
70
+ | `audit/handoff.md` — converting an existing project (hand-written or pre-1.17 narrative) | **Lazily, at the first write — and then ALL AT ONCE.** Converting one row at a time is the state that loses the others, so `index` refuses to write while anything in the file is unaccounted for and names it. Until the first source file exists nothing generates, nothing errors, and the file is read verbatim exactly as today. | 5 / session end |
70
71
  | `audit/audit_plan.md` (Standalone) | Bootstrap, and whenever a mapped area changes state (`sdlc_check.py mark` records the reference — git hash, else UTC timestamp). | 1 |
71
72
  | `reference/GUIDE_[topic].md` (`source_kind: document`) | Origin+purpose test (`guides.md`), or a proactive proposal the user accepted. Propose, never a silent write, never from model knowledge. | 4 / 5 |
72
73
  | `reference/GUIDE_[topic].md` (`source_kind: code`) | Recognized high-complexity component/feature/layer with no CURRENT guide — including one that breaks repeatedly across sessions → **duty to write autonomously** (no proposal; additive, code-anchored, reversible). Fidelity floor: every claim traces to a code excerpt. Signals + guard-rails: `guides.md` §1. Write it as soon as you recognize the signal; the Phase-5 Comprehension checkpoint is the backstop that asks the question, never the only moment it may fire. | 4 / 5 |
@@ -188,7 +189,7 @@ gate) instead of requiring an IN_PROGRESS ANALYSIS.
188
189
 
189
190
  ### 1. Audit and Alignment
190
191
 
191
- - Read `ai_docs/audit/handoff.md` if it exists — the **workstream registry**: one row per open workstream, so you see at a glance what is in PROGRESS, on which branch, since when, before touching anything. If a row's Date/Branch are inconsistent with reality, treat that row as history. When resuming a specific workstream, read its `audit/HANDOFF_[feature].md` (volatile resume logistics) AND its ANALYSIS Diary (durable narrative) — the registry row points at both. A **narrative handoff from a pre-1.17 project** reads as a single-row registry; convert it when you next write it, not now (Write Triggers).
192
+ - Read `ai_docs/audit/handoff.md` if it exists — the **workstream registry**: one row per open workstream, so you see at a glance what is in PROGRESS, on which branch, since when, before touching anything. It is generated from the `HANDOFF_*.md` files, so it is read here and never edited here. If a row's Date/Branch are inconsistent with reality, treat that row as history. When resuming a specific workstream, read its `audit/HANDOFF_[feature].md` (its row plus the resume logistics) AND its ANALYSIS Diary (the durable narrative) — the registry row points at both. A hand-written or pre-1.17 narrative handoff still works and is converted when you next write it, not now (Write Triggers).
192
193
  - Read `ai_docs/README.md` (curated must-reads), `ai_docs/INDEX.md` (generated manifest of all canonical docs) and `ai_docs/reference/INDEX.md` (the guide router) to know what exists before exploring the code. The router is a mandatory read, not an optional one: it is the only orientation step that tells you a guide already governs the work you are about to do. On a project with no guides yet it exists as an empty stub (`sdlc_check.py index` writes it precisely so the mandatory read has something to read) — the honest verdict there is `router: no match`, and if the file is genuinely absent, say `router: absent (no router file)` and regenerate it rather than inventing a match. `solutions/` and `audit/` are not indexed per file: search them with glob/grep.
193
194
  - Recommended default: a SessionStart hook (`ENFORCEMENT.md` §4) emits this orientation automatically at session start (README + INDEX + guide router + handoff + triage reminder), so the router reaches the context even in a session that never opens Phase 1 explicitly. Wire it wherever Python is available; when it is not wired, do these reads manually as above — the process never depends on it, and it fails open (a missing/empty `ai_docs/` never blocks the session).
194
195
  - If `ai_docs/` is missing or incomplete, create the structure and the **bootstrap set** by analyzing the project in batches: `README.md`, the three `vision/` docs (`Status: DRAFT`), `strategic/architecture.md`, `strategic/existing_features.md` and — Standalone — `audit/audit_plan.md`; then regenerate `INDEX.md`. Nothing else is mandatory at bootstrap (per-document triggers: Write Triggers).
@@ -255,7 +256,7 @@ Hybrid L3:
255
256
  - If the work was governed by user-provided indications and is reusable, **PROPOSE distilling a guide** (proactive trigger, `guides.md` §1) — a proposal for the user, never a silent write, never from model knowledge.
256
257
  - **Comprehension checkpoint**: ask explicitly — *did this session force me to build a model of a high-complexity component that no CURRENT guide covers?* If yes, WRITE the `source_kind: code` guide now (a duty, not a proposal — `guides.md` §1) and say so in the closure. The knowledge you paid to build is at its most complete right here; one closure later it is gone, and the next session re-derives it or breaks the component from partial understanding.
257
258
  - Update only the documents actually impacted.
258
- - **Update the workstream registry (`audit/handoff.md`)** — mandatory at every L3 closure: REMOVE the closed workstream's row and DELETE its `audit/HANDOFF_[feature].md` (history lives in git and in the Diary); leave every other row untouched — that is the parallel-safety the registry exists for. 20 lines. The session-end rule and the L2 case: Write Triggers.
259
+ - **Update the workstream registry (`audit/handoff.md`)** — mandatory at every L3 closure: DELETE the closed workstream's `audit/HANDOFF_[feature].md` (history lives in git and in the Diary) and re-run `sdlc_check.py index`. Deleting the file *is* removing the row, and no other workstream's file is touched — that is the parallel-safety the registry exists for. Never edit the generated file: `validate` errors when it disagrees with its sources. The session-end rule and the L2 case: Write Triggers.
259
260
  - **Aligned indexes (Poka-Yoke)**: if you created, moved or removed canonical documents (`vision/`, `reference/`, `architecture/`, `functional/`, `strategic/`):
260
261
  - regenerate the manifest with `sdlc_check.py index` (writes `ai_docs/INDEX.md`) — never write it by hand;
261
262
  - if the document is a must-read, add/update its line in the curated `README.md`;
@@ -215,7 +215,11 @@ DEFAULT_DOMAIN = "code"
215
215
  PORTABLE_CHECKS = {}
216
216
  # Which check namespaces this distribution actually carries. Set by the entry point;
217
217
  # a `checks:` entry outside it WARNS visibly rather than passing silently.
218
- _ENTRY_POINT = {"domain": DEFAULT_DOMAIN, "provides": ()}
218
+ _ENTRY_POINT = {"domain": DEFAULT_DOMAIN, "provides": (), "script": "sdlc_check.py"}
219
+ # "script" is what generated headers tell the reader to RUN. Declared, never
220
+ # derived from sys.argv: the generated bytes must not depend on how the command
221
+ # was invoked, or the alignment check would fail on the invocation instead of on
222
+ # the content.
219
223
 
220
224
 
221
225
  # --- distribution profile ----------------------------------------------------
@@ -292,10 +296,17 @@ def portable_check(name):
292
296
  return register
293
297
 
294
298
 
295
- def set_entry_point(domain, provides=()):
299
+ def set_entry_point(domain, provides=(), script=None):
296
300
  """Declare which domain this distribution is and which check namespaces it ships."""
297
301
  _ENTRY_POINT["domain"] = domain
298
302
  _ENTRY_POINT["provides"] = tuple(provides)
303
+ if script:
304
+ _ENTRY_POINT["script"] = script
305
+
306
+
307
+ def entry_script():
308
+ """The command name a generated header tells the reader to run."""
309
+ return _ENTRY_POINT["script"]
299
310
 
300
311
 
301
312
  def project_default_domain(root):
@@ -727,6 +738,142 @@ def build_manifest(root):
727
738
  return "\n".join(lines).rstrip() + "\n"
728
739
 
729
740
 
741
+ # ------------------------------------------------- workstream registry (F-028)
742
+ # audit/handoff.md is GENERATED from one source file per open workstream, so two
743
+ # writers working two workstreams touch two different files. Row-per-workstream
744
+ # alone was not enough -- F-019 had that and the file still conflicted twice,
745
+ # because a file-global `Date:` header defeats row-level ownership. The header is
746
+ # now DERIVED, so no writer touches it.
747
+
748
+ REGISTRY_COLUMNS = ("Workstream", "Level", "Branch", "Status", "Since",
749
+ "Next step", "Details")
750
+ REGISTRY_KEYS = ("workstream", "level", "branch", "status", "since", "next")
751
+ REGISTRY_CAP = 20 # a signal, never a truncation (see cmd_index)
752
+ PROJECT_NOTES = "project_notes.md" # NOT handoff_notes.md: the HANDOFF_*.md
753
+ # glob is case-insensitive on Windows, and that name would be collected as a
754
+ # source. The trap is real; the name is the fix.
755
+
756
+
757
+ def registry_header():
758
+ return (f"<!-- GENERATED by {entry_script()} index - do not edit by hand. "
759
+ f"Source of truth: the HANDOFF_*.md files in {docs_dir()}/audit/. -->")
760
+
761
+
762
+ def list_workstreams(root):
763
+ """[(path, meta)] for audit/HANDOFF_*.md carrying registry frontmatter.
764
+
765
+ Opt-in by the presence of `workstream:` -- a project whose handoff is still
766
+ hand-written has no sources, so nothing generates and nothing errors (the
767
+ F-019 migration lesson). Sorted by workstream id: the alignment check is a
768
+ byte comparison, and glob order differs across filesystems."""
769
+ aud = ai_path(root, "audit")
770
+ out = []
771
+ if not aud.is_dir():
772
+ return out
773
+ for p in sorted(aud.glob("HANDOFF_*.md")):
774
+ meta = load_frontmatter(read_text(p).splitlines())
775
+ if str(meta.get("workstream") or "").strip():
776
+ out.append((p, meta))
777
+ out.sort(key=lambda pm: (str(pm[1].get("workstream")).strip().lower(), pm[0].name))
778
+ return out
779
+
780
+
781
+ def _registry_cell(value):
782
+ text = str(value if value is not None else "").strip()
783
+ return text.replace("|", "\\|") or "-"
784
+
785
+
786
+ def build_registry(root):
787
+ """The generated registry, or "" when there is no source to build it from.
788
+
789
+ Deterministic by construction: sorted rows, and a `Date:` taken from the
790
+ newest `updated:` VALUE written inside the sources -- never a filesystem
791
+ timestamp. Git does not preserve mtimes, so an mtime-derived header would
792
+ regenerate differently in every fresh clone and the alignment check would
793
+ fire on a tree nobody touched."""
794
+ rows = list_workstreams(root)
795
+ if not rows:
796
+ return ""
797
+ stamp = max(str(m.get("updated") or m.get("since") or "").strip() or "0000-00-00"
798
+ for _p, m in rows)
799
+ lines = ["# Handoff — workstream registry",
800
+ f"Date: {stamp} (UTC)",
801
+ "",
802
+ registry_header(),
803
+ "",
804
+ "| " + " | ".join(REGISTRY_COLUMNS) + " |",
805
+ "|" + "---|" * len(REGISTRY_COLUMNS)]
806
+ for p, meta in rows:
807
+ extra = str(meta.get("details") or "").strip()
808
+ details = f"{p.name} · {extra}" if extra else p.name
809
+ cells = [_registry_cell(meta.get(k)) for k in REGISTRY_KEYS]
810
+ lines.append("| " + " | ".join(cells) + f" | {_registry_cell(details)} |")
811
+ notes = ai_path(root, "audit", PROJECT_NOTES)
812
+ if notes.is_file():
813
+ body = read_text(notes).strip()
814
+ if body:
815
+ lines += ["", "## Project-wide notes", "", body]
816
+ return "\n".join(lines) + "\n"
817
+
818
+
819
+ def parse_registry_rows(text):
820
+ """Workstream ids in a registry table, hand-written or generated."""
821
+ ids = []
822
+ for line in text.splitlines():
823
+ s = line.strip()
824
+ if not s.startswith("|") or set(s) <= {"|", "-", " ", ":"}:
825
+ continue
826
+ first = s.strip("|").split("|")[0].strip()
827
+ if first and first.lower() != "workstream":
828
+ ids.append(first)
829
+ return ids
830
+
831
+
832
+ def registry_conversion_blockers(root):
833
+ """What stops `index` from writing over a hand-written handoff.md.
834
+
835
+ The mixed state is the trap this exists for: converting one row at a time
836
+ leaves a project at one source file and five hand-written rows, and
837
+ regenerating from the one source DELETES the other five -- silently, in the
838
+ file whose whole purpose is not losing them. So conversion is per project.
839
+ Empty list = writing is safe."""
840
+ hand = ai_path(root, "audit", "handoff.md")
841
+ if not hand.is_file():
842
+ return []
843
+ text = read_text(hand)
844
+ if "GENERATED by sdlc_check.py index" in text:
845
+ return [] # already ours
846
+ blockers = []
847
+ known = {str(m.get("workstream")).strip() for _p, m in list_workstreams(root)}
848
+ orphans = [r for r in parse_registry_rows(text) if r not in known]
849
+ if orphans:
850
+ blockers.append("rows no HANDOFF_*.md accounts for: " + ", ".join(orphans))
851
+ # Everything else in the file must have a home too, or it is lost on write:
852
+ # a pre-1.17 narrative handoff carries no table at all, so orphan rows alone
853
+ # would not notice it.
854
+ notes_ok = ai_path(root, "audit", PROJECT_NOTES).is_file()
855
+ leftovers, in_notes = [], False
856
+ for line in text.splitlines():
857
+ s = line.strip()
858
+ if not s or s.startswith("|") or s.startswith("<!--"):
859
+ continue
860
+ if s.startswith("# ") or re.match(r"^(?:Date|Data):", s):
861
+ continue
862
+ if re.match(r"^##\s+Project-wide notes\s*$", s):
863
+ in_notes = True
864
+ continue
865
+ if s.startswith("## "):
866
+ in_notes = False
867
+ if in_notes and notes_ok:
868
+ continue
869
+ leftovers.append(s)
870
+ if leftovers:
871
+ blockers.append("content outside the table with nowhere to go (%d line(s), first: %r) "
872
+ "-- project-wide notes belong in audit/%s"
873
+ % (len(leftovers), leftovers[0][:60], PROJECT_NOTES))
874
+ return blockers
875
+
876
+
730
877
  def list_guides(root):
731
878
  """[(rel_to_ai_docs, path, meta, text)] for ai_docs/reference/GUIDE_*.md."""
732
879
  ref = ai_path(root, "reference")
@@ -821,6 +968,30 @@ def cmd_index(root):
821
968
  "a comprehension map (`source_kind: code`) -- see `guides.md`.\n",
822
969
  encoding="utf-8")
823
970
  print(f"[ok] guide router regenerated (empty stub): {gidx}")
971
+ return max(rc_registry(root), 0)
972
+
973
+
974
+ def rc_registry(root):
975
+ """Write the generated workstream registry, or refuse and say why (F-028)."""
976
+ ws = list_workstreams(root)
977
+ if not ws:
978
+ return 0 # no sources: a hand-written handoff is untouched
979
+ blockers = registry_conversion_blockers(root)
980
+ hand = ai_path(root, "audit", "handoff.md")
981
+ if blockers:
982
+ print(f"[ERROR] {docs_dir()}/audit/handoff.md NOT regenerated -- it still holds "
983
+ "state no source accounts for:")
984
+ for b in blockers:
985
+ print(f" - {b}")
986
+ print(" Convert the whole registry at once (templates.md): converting "
987
+ "one row at a time is the state that loses the others.")
988
+ return 1
989
+ hand.parent.mkdir(parents=True, exist_ok=True)
990
+ hand.write_text(build_registry(root), encoding="utf-8")
991
+ print(f"[ok] workstream registry regenerated: {hand}")
992
+ if len(ws) > REGISTRY_CAP:
993
+ print(f"[warn] {len(ws)} open workstreams: the registry is meant to stay under "
994
+ f"{REGISTRY_CAP}. Nothing was truncated -- closing one is the fix.")
824
995
  return 0
825
996
 
826
997
 
@@ -1257,8 +1428,33 @@ def cmd_validate(root, strict=False, hybrid=False):
1257
1428
  elif norm_text(read_text(gidx)) != norm_text(build_guide_index(root)):
1258
1429
  errors.append(f"{docs_dir()}/reference/INDEX.md not aligned with the guides: run 'sdlc_check.py index'")
1259
1430
 
1260
- # Handoff: header and freshness
1431
+ # Handoff: alignment with its sources (F-028), then header and freshness
1261
1432
  hand = ai / "audit" / "handoff.md"
1433
+ workstreams = list_workstreams(root)
1434
+ if workstreams:
1435
+ if not hand.is_file():
1436
+ errors.append(f"{docs_dir()}/audit/handoff.md missing while HANDOFF_*.md sources "
1437
+ "exist: the registry is the only place a cold agent sees the open "
1438
+ "workstreams -- run 'sdlc_check.py index'")
1439
+ elif norm_text(read_text(hand)) != norm_text(build_registry(root)):
1440
+ errors.append(f"{docs_dir()}/audit/handoff.md not aligned with its HANDOFF_*.md "
1441
+ "sources: run 'sdlc_check.py index'. A merge resolved by hand is "
1442
+ "exactly what this catches")
1443
+ if len(workstreams) > REGISTRY_CAP:
1444
+ warnings.append(f"{len(workstreams)} open workstreams: the registry is meant to "
1445
+ f"stay under {REGISTRY_CAP}")
1446
+ # Two files claiming one workstream is the collision this design does NOT
1447
+ # fix (two people opening the same work under different file names). It
1448
+ # would otherwise show up as two identical-looking rows and nothing else.
1449
+ seen = {}
1450
+ for p, meta in workstreams:
1451
+ wid = str(meta.get("workstream")).strip()
1452
+ if wid in seen:
1453
+ warnings.append(f"{docs_dir()}/audit/{p.name} and {seen[wid]} both claim "
1454
+ f"workstream '{wid}': the registry shows two rows for one "
1455
+ "workstream — decide which file owns it")
1456
+ else:
1457
+ seen[wid] = p.name
1262
1458
  if hand.is_file():
1263
1459
  m = re.search(r"(?:Date|Data):\s*(\d{4}-\d{2}-\d{2})", read_text(hand))
1264
1460
  if not m:
@@ -86,7 +86,7 @@ Must-reads for this project, in order. The full manifest of canonical docs is
86
86
  1. `reference/INDEX.md` — the guide router: which guide already governs the work you are about to do (generated).
87
87
  2. `vision/project_vision.md` — why the project exists (check its Status first).
88
88
  3. `strategic/architecture.md` — how it is built.
89
- 4. `audit/handoff.md` — where work stopped last session (if present).
89
+ 4. `audit/handoff.md` — where work stopped last session (generated from the `HANDOFF_*.md` beside it; never edited by hand).
90
90
 
91
91
  Directory purposes: `vision/` (project direction), `strategic/` (architecture and
92
92
  feature catalog), `reference/` (operative guides), `solutions/` (per-feature
@@ -367,36 +367,50 @@ States: PENDING (to analyze) | ANALYZED (analyzed, with reference) | SKIPPED (wi
367
367
  | vendor/ | SKIPPED | - | vendored code |
368
368
  ```
369
369
 
370
- ## ai_docs/audit/handoff.md — the workstream registry
371
-
372
- One row per OPEN workstream, ≤ 20 lines. **Parallel-safe by construction**: closing
373
- one milestone removes one row and never touches another's resume point — the defect
374
- this replaces was a single narrative slot where the last session to close overwrote
375
- everyone else's handoff. It is an **inventory for lookup** (like the generated
376
- manifest), not a work board: no assignment, no due dates, no execution ordering.
377
-
378
- Updated at every L3 closure (row removed) AND at session end with work still
379
- IN_PROGRESS (row refreshed) see Write Triggers.
380
-
381
- **Coming from a pre-1.17 project** (narrative handoff with `## Active features` /
382
- `## Next step` / `## Session notes`): nothing is broken and nothing is urgent — the
383
- validator only checks the `Date:` header and its age, and the orientation hook reads
384
- the file verbatim. Read it as a one-row registry, and convert it the next time the
385
- write trigger fires: each `## Active features` bullet becomes a row, `## Next step`
386
- becomes that row's next step, `## Session notes` becomes `## Project-wide notes`.
387
- Migrating a repository that is not being worked on buys nothing.
370
+ ## ai_docs/audit/handoff.md — the workstream registry (GENERATED)
371
+
372
+ One row per OPEN workstream. **Never written by hand**: `sdlc_check.py index` builds
373
+ it from the `HANDOFF_[feature].md` files, and `validate` errors when the two disagree.
374
+ It is an **inventory for lookup** (like the generated manifest), not a work board: no
375
+ assignment, no due dates, no execution ordering, no holder.
376
+
377
+ **Why generated, and not just one row per workstream.** Row-per-workstream alone was
378
+ tried and was not enough: two workstreams opened from one base still conflicted twice
379
+ in this file, because a file-global `Date:` header defeats row-level ownership no
380
+ matter how few rows each writer touches. So the truth moved into the per-workstream
381
+ file, and the header is derived (the newest `updated:` in the sources — a value, never
382
+ a filesystem timestamp, which git does not preserve). Two writers on two workstreams
383
+ now touch two different files. The generated view can still conflict at merge; that
384
+ conflict is resolved **mechanically** by re-running `index`, never by hand, and
385
+ `validate` refuses CLEAN until the file matches its sources.
386
+
387
+ Project-wide notes have their own source, `ai_docs/audit/project_notes.md`, appended
388
+ verbatim under `## Project-wide notes`. (Not `handoff_notes.md`: the `HANDOFF_*.md`
389
+ glob is case-insensitive on Windows and would collect it as a workstream.)
390
+
391
+ **Converting an existing project** — lazily, at the first write, and **all at once**.
392
+ Converting one row at a time is the state that loses the others: the next `index`
393
+ would regenerate from the one source and drop the rest. `index` refuses to write while
394
+ anything in the file is unaccounted for, and names it. A pre-1.17 narrative handoff
395
+ (`## Active features` / `## Next step` / `## Session notes`) is the same conversion:
396
+ each bullet becomes a `HANDOFF_[feature].md`, `## Session notes` becomes
397
+ `project_notes.md`. A project with no sources yet is not touched and reports nothing —
398
+ migrating a repository nobody is working on buys nothing.
388
399
 
389
400
  ```markdown
390
401
  # Handoff — workstream registry
391
402
  Date: 2026-06-11 (UTC)
392
403
 
404
+ <!-- GENERATED by sdlc_check.py index - do not edit by hand. Source of truth: the HANDOFF_*.md files in ai_docs/audit/. -->
405
+
393
406
  | Workstream | Level | Branch | Status | Since | Next step | Details |
394
407
  |---|---|---|---|---|---|---|
395
408
  | F-001 SSO login | L3 | feature/sso-login | PROGRESS | 2026-06-10 | wire callback tests | HANDOFF_login_sso.md · ANALYSIS_login_sso.md |
396
- | F-002 Audit refresh | L3 | feature/audit | PAUSED | 2026-06-02 | resume at Phase 4 | ANALYSIS_audit_refresh.md (no volatile state) |
409
+ | F-002 Audit refresh | L3 | feature/audit | PAUSED | 2026-06-02 | resume at Phase 4 | HANDOFF_audit_refresh.md · ANALYSIS_audit_refresh.md |
397
410
 
398
411
  ## Project-wide notes
399
- <!-- one or two lines: release pending, environment quirks that affect everyone -->
412
+
413
+ <!-- from audit/project_notes.md: release pending, environment quirks that affect everyone -->
400
414
  ```
401
415
 
402
416
  ## ai_docs/audit/reviews/REVIEW_LOG.md
@@ -431,21 +445,47 @@ records the realization actually used — fresh subagent, one-shot client run, o
431
445
  honest; writing nothing, or implying independence you did not have, is the failure
432
446
  this column exists to prevent. `findings_real` is how many raised findings survived
433
447
  triage: over time it is the only evidence of whether the gate earns its cost.
434
-
435
- ## ai_docs/audit/HANDOFF_[feature].md volatile resume logistics (ephemeral)
436
-
437
- **Resume logistics ONLY; the ANALYSIS Diary keeps the durable narrative (DRY).**
438
- The boundary: Diary = what happened and why (decisions, state of the work survives
439
- forever); this file = how to pick the work back up (branch/worktree, uncommitted
440
- state, environment notes, the next concrete command — worthless once resumed).
441
- Created only when a session pauses the feature WITH volatile state to record;
442
- **DELETED at the feature's closure**, in the same step that flips the ANALYSIS to
443
- COMPLETEDanything in it worth keeping was in the wrong file.
448
+ Concurrent reviews: `init` writes a `.gitattributes` stanza giving this file
449
+ `merge=union`a **built-in** driver (no per-clone `git config`, unlike
450
+ `merge=ours`, which silently does nothing until every clone configures it).
451
+ Rows are date-stamped and their order carries no meaning, so a union merge keeps
452
+ both sides instead of asking a human to choose. It is defence in depth: without
453
+ git, or without the stanza, the outcome is today's one conflict you resolve by
454
+ hand, never a lost row.
455
+
456
+
457
+ ## ai_docs/audit/HANDOFF_[feature].md one open workstream, its own file
458
+
459
+ **The authored home of that workstream's registry row**, and the only one: the
460
+ registry is generated from these files. **One exists for every OPEN workstream**, with
461
+ or without volatile state — a workstream whose file is missing has no row, and a
462
+ workstream with no row is invisible to the next cold agent. **DELETED at the feature's
463
+ closure**, in the same step that flips the ANALYSIS to COMPLETED: deleting it *is*
464
+ removing the row.
465
+
466
+ **The DRY boundary, restated because the file is no longer rare.** What used to keep
467
+ narrative out of it was that it barely existed; now it always does. So: the ANALYSIS
468
+ Diary keeps **what happened and why** (decisions, state of the work — survives
469
+ forever), and this file keeps **the row plus the resume logistics** — how to pick the work back up (branch,
470
+ worktree, uncommitted state, the next concrete command — worthless once resumed).
471
+ Prose that would still be worth reading after closure is in the wrong file, because
472
+ this one is deleted.
473
+
474
+ The frontmatter IS the row. `workstream:` is what marks the file as a source: without
475
+ it the file is still a perfectly good volatile note, and nothing generates.
444
476
 
445
477
  ```markdown
478
+ ---
479
+ workstream: F-001 SSO login
480
+ level: L3
481
+ branch: feature/sso-login (worktree ../wt-sso)
482
+ status: PROGRESS
483
+ since: 2026-06-10
484
+ next: wire the callback tests
485
+ details: ANALYSIS_login_sso.md
486
+ updated: 2026-06-11
487
+ ---
446
488
  # HANDOFF: [feature] (ephemeral — deleted at closure)
447
- Updated: 2026-06-11 (UTC)
448
- Branch: feature/sso-login (worktree ../wt-sso)
449
489
 
450
490
  ## Resume state
451
491
  <!-- uncommitted files, half-run migrations, env vars, running services -->
@@ -457,6 +497,19 @@ Branch: feature/sso-login (worktree ../wt-sso)
457
497
  <!-- traps discovered this session that bite on resume (locks, CRLF, flaky test) -->
458
498
  ```
459
499
 
500
+ `updated:` is the date this file last changed, and the newest one across all sources
501
+ becomes the registry's `Date:` header — which is why no writer ever edits that header
502
+ and why two concurrent writers no longer collide on it. `details:` holds the *other*
503
+ pointers (the ANALYSIS, a review log entry); the generator prepends this file's own
504
+ name, so nothing points at itself by hand.
505
+
506
+ ## ai_docs/audit/project_notes.md — the registry's project-wide notes (source)
507
+
508
+ Plain lines, no frontmatter, appended verbatim to the generated registry under
509
+ `## Project-wide notes`. Release pending, environment quirks, anything true for
510
+ everyone rather than for one workstream. It exists so that generating the registry
511
+ cannot destroy notes that belong to no workstream.
512
+
460
513
  ## ai_docs/strategic/architecture.md and existing_features.md
461
514
 
462
515
  Canonical docs: they open with the header (`description:`/`status:`) so they enter the `INDEX.md` manifest cleanly.