@antoneeo/kb-agentic-skill 1.3.0 → 1.4.1

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,71 @@
2
2
 
3
3
  Every significant change to this skill is recorded here.
4
4
 
5
+ ## [1.4.1] - 2026-08-03
6
+
7
+ ### Added
8
+ - **`version:` in every `SKILL.md` frontmatter.** An installed skill carries no
9
+ `package.json` and no `gemini-extension.json` — only doctrine and scripts — so nothing
10
+ in it said which build it was. Answering "is that fix in your copy?" took `npm view`
11
+ plus a shasum comparison; from a user's side it was unanswerable. Now the first thing
12
+ in the operating contract says it.
13
+ - **A battery invariant asserting every bump point agrees** (`SKILL.md` ↔ `package.json`
14
+ ↔ `gemini-extension.json`). A hand-maintained version string rots, and this repository
15
+ has the scar: the third bump point was skipped for two whole releases with nothing to
16
+ catch it. The fourth arrives with its guard attached, and `GUIDE_release.md` step 1
17
+ now says FOUR.
18
+
19
+ ## [1.4.0] - 2026-08-03
20
+
21
+ F-028 — several people on one project. `templates.md` had claimed the workstream
22
+ registry was "Parallel-safe by construction" since F-019, and nothing had ever exercised
23
+ it. Two workstreams opened from one base conflict **twice in one file**: on the row
24
+ insert, and on the file-global `Date:` header. Row-level ownership cannot save a file
25
+ that has a file-level field.
26
+
27
+ ### Changed
28
+ - **`audit/handoff.md` is now GENERATED** from one `HANDOFF_[unit].md` per open
29
+ workstream, whose frontmatter IS the row. Two writers on two workstreams touch two
30
+ different files, and the `Date:` header is derived — from the newest `updated:` VALUE
31
+ in the sources, never a filesystem timestamp (git does not preserve mtimes, and an
32
+ mtime-derived header would make the file regenerate differently in every fresh clone).
33
+ - **`HANDOFF_[unit].md` is now written for every OPEN workstream**, with or without
34
+ volatile state — no file, no row. It still carries the resume logistics, and deleting
35
+ it at closure *is* what removes the row. The DRY boundary against the ANALYSIS Diary
36
+ is restated in the template, because what used to keep narrative out of that file was
37
+ its rarity, and the rarity is gone.
38
+ - Project-wide notes move to their own source, `audit/project_notes.md`, so generating
39
+ the registry cannot destroy notes that belong to no workstream.
40
+
41
+ ### Added
42
+ - **`validate` errors when the registry disagrees with its sources** — which is what
43
+ turns a merge resolved carelessly from permanent into loud. Resolution is mechanical:
44
+ re-run `index`. The generated view can still conflict; the authored truth does not.
45
+ - **`index` refuses to write while anything in the file is unaccounted for**, and names
46
+ it. Converting one row at a time is the state that loses the others, so conversion is
47
+ per project. A project with no sources is untouched and sees no new finding.
48
+ - **`init` writes a `.gitattributes` stanza** giving the append-only review log
49
+ `merge=union` — a **built-in** driver, unlike `merge=ours`, which silently does
50
+ nothing until every clone runs `git config` and leaves the file wrong even then.
51
+ Create-only; a user's own `.gitattributes` is never clobbered.
52
+ - `scripts/test_merge_safety.py` (shared, ×3): the experiment that found the defect,
53
+ kept as a regression test — two workstreams from one base, merged, with the assertion
54
+ that only the generated view may conflict and that regenerating loses no state. Plus
55
+ the mixed-state, fresh-mtime, ordering, cap and duplicate-id guards, each
56
+ mutation-verified.
57
+ - A warning when two files claim the same workstream: the collision this design does
58
+ **not** fix (two people opening the same work under two names) must not pass as two
59
+ ordinary rows.
60
+
61
+ The mechanism is files and a generator: **it works with no VCS at all.** The
62
+ `.gitattributes` stanza is defence in depth — without it the outcome is today's, one
63
+ conflict resolved by hand, never a lost row.
64
+
65
+ ### Upgrading
66
+ Nothing happens until you convert: with no `HANDOFF_*.md` sources, `index` and
67
+ `validate` behave exactly as before. When you convert, convert the whole registry at
68
+ once — `index` will tell you what is still unaccounted for.
69
+
5
70
  ## [1.3.0] - 2026-08-03
6
71
 
7
72
  Field defect: an agent handed a 200-page manual emits a few dozen claims and reports
package/README.md CHANGED
@@ -1,88 +1,89 @@
1
- # KB Agentic Skill for Claude Code, Gemini CLI, Google Antigravity & Codex
2
-
3
- `kb-agentic` turns an AI agent into the keeper of a second brain built **on the documents you supply** — not on what the model remembers. It supports Claude Code, Codex, Gemini CLI, Google Antigravity 2.0, Cursor/Windsurf-style project instructions, and optional devPNT governance.
4
-
5
- The sibling of [`@antoneeo/agentic-sdlc-skill`](https://www.npmjs.com/package/@antoneeo/agentic-sdlc-skill), transplanted from source code to knowledge: same process, one different fidelity discipline.
6
-
7
- ## What it does
8
-
9
- Two axes are kept apart on purpose — **abstraction** (a topic made of topics: the graph edges) and **certainty** (how strongly the corpus supports a statement: the claim rows).
10
-
11
- 1. **Intake — the source becomes untouchable.** Every document enters verbatim under `corpus/given/`, content-addressed by a raw-byte digest. A new version is appended with `supersedes:`, never overwritten. Non-text files get a **stored canonical extraction** (`.txt`, registered extractor) — those are the bytes locators point at, so an id stays valid across re-ingestion. On a large binary corpus the extraction **is** the artifact: the digest moves onto it (immutability still enforced, on the bytes that actually matter) and the original stays where it lives, recorded as `original_path:` + `original_sha256:`. 233 MB of manuals do not enter your repository to protect bytes no locator addresses — and the two recorded fields are honest about their limit: they let a human re-verify, they check nothing on their own. Spoken input is a note with `origin: elicited`; a synthesis carries `derived_from:`; your decision carries `basis:`. A note with none of the three is model knowledge dressed as a source, and the validator rejects it.
12
-
13
- 2. **Extraction — the unit is the claim.** Rows of `id | claim | valid | qty | about | source | prov | state`. The **id hashes the location and the quantity, never the text**, so an LLM rephrasing mints no new identity. The locator (`p=17@412-509`) is verified: the validator opens the extraction and checks the span exists — and `anchor <path> <phrase>` produces it for you, matching spaces as `\s+` because a PDF extraction breaks phrases mid-line. **Gates are extracted alongside powers**: for every row saying what something can do, the source is asked what must hold first — default-off, licence tier, version floor, dependency — because "yes, supported" without the gate is a plan that fails on site. The rule is *ask*, never *produce*: a source that states no gate yields no row. Validity scopes are half-open ("until March" and "from March" do not conflict). Quantities are typed — mixed kinds or currencies **refuse to sum**. What the source does not assert becomes a `gaps:` line, never a claim.
14
-
15
- **The source is exhausted, not sampled.** "Invents nothing" is a floor, and an extractor that stops when nothing it wrote is false stops on page twenty of a two-hundred-page manual with every row correct — which is exactly what a 200-page manual produces in the field. So a long source is read in **bounded windows** (30 pages by default, one plan task each, the ledger holding your place across sessions), and every window closes by advancing `extracted_through:` on the artifact's sidecar. That field is what makes "I am finished" falsifiable: claims with no coverage recorded are an error, a claim addressing a page past the declared coverage is a contradiction, and coverage short of the end is reported until it reaches it. Its limit is stated where it is written — **nothing proves a page was read**; what changes is that the shortcut must now be written down to pass. And exhaustive means read, never *a row per page*: a page that asserts nothing yields nothing.
16
-
17
- 3. **Placement — five verdicts, after querying the graph.** Descent through the generated index following every parent (polyhierarchy). EXISTS → reconcile; INADEQUATE → child; **MISSING only after the graph was actually asked**; GENERALIZES → escalate (a new root stops at you); UNPLACED → quarantine. Similar-but-maybe-different becomes a sibling **with the distinguishing line written** — if you cannot write it, it is the same concept. Cycles are refused at write time; merged nodes leave a tombstone with `redirect_to:`, never a deletion.
18
-
19
- 4. **Reconciliation — the machine detects and holds, it never decides.** Five outcomes: new / confirmation (the source is appended to the row — the base strengthens, it does not lengthen) / refinement (the old row goes `SUPERSEDED`, its text intact) / coexistence (disjoint scopes) / conflict → the whole set goes `CONTESTED`, **symmetrically**: flipping one cell by hand fails the check. Only new information resolves it — a later source, or **your ruling with a `basis:`**, the fact you know and the corpus does not. No basis, no ruling: a preference is not a fact. A ruling is challengeable — a later document reopens the case with your basis beside it.
20
-
21
- 5. **Escalation in one batch at the end of the run**, in legal form (the claims, the reopenable sources, the dates, why the machine cannot decide). Ingestion never stops to interrogate you.
22
-
23
- Deliberately absent: any per-node coverage or completion state. `gaps:` says what a node lacks; nothing collects it into a dashboard. A **source** does record how far it has been read, but on its own sidecar and nowhere else — the corpus index prints that fact for every artifact, including the finished ones, because a list of only what is behind is the dashboard this method refuses.
24
-
25
- ## Key features
26
-
27
- - **Risk-proportional triage, measured in knowledge and never in files**: one claim row → propagating a fact already settled → a new knowledge unit (a source ingested, a node created or superseded, the hierarchy moved). Carrying one settled fact into eight documents is small; one claim that re-parents a node is not. A **Write Triggers** table maps each knowledge event to exactly one destination.
28
- - **Portable knowledge**: `export` bundles a subgraph together with the bytes its claims cite — a closure, not a selection, because a claim whose source cannot be reopened is model knowledge arriving by another route. `import` merges it into another project **additively**: it never overwrites a node and never deletes, and claims already present are recognised by id rather than by comparing text, since the same artifact cited at the same span mints the same id in every project. Knowledge crosses the project boundary; **authority does not** — an imported ruling arrives as `prov: IMPORTED`, keeps its original `basis:`, and cannot settle a local disagreement until you re-ratify it.
29
- - **Vision-guided governance**: Standalone projects use `ai_docs/vision/`; Hybrid projects use devPNT `M-VISION` as the milestone north star. `DRAFT` informs, `APPROVED` binds, promotion is the user's alone.
30
- - **Independent review, twice**: the design before it is implemented, the result before it is declared done — fresh-context subagent > one-shot run > a declared self-pass, 3 rounds max, one log line each, and a PASS is invalid on "found nothing".
31
- - **Question discipline**: a question is legal only when the agent searched first, names the search with its result, and names the decision it unblocks; otherwise it proceeds on a declared assumption, batched.
32
- - **Operative guides + agent-global KB**: distil user-provided indications into source-faithful `GUIDE_*.md` (`source_kind: document`) verbatim snapshot plus hash, so drift is detected mechanically.
33
- - **Mechanical checks**: `check`, `validate`, `index`, `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, plus the spine's `stale`/`mark`/`gate`/`plan`/`orient`/`migrate`. The graph and corpus checks verify spans against the stored extraction, recompute every id, refuse cycles and unreachable nodes, and enforce `CONTESTED` symmetry.
34
- - **Installed support files**: Claude, Codex, Gemini and Google Antigravity receive the full skill folder — `SKILL.md`, `templates.md`, `taxonomy.md`, `distillation.md`, `reconciliation.md`, `guides.md`, `vision.md`, `elicitation.md`, `review.md`, `dispatch.md`, `routing.md`, `ENFORCEMENT.md`, and the validator's two files, `scripts/sdlc_check.py` + `scripts/sdlc_core.py` (the core is the family's shared spine copy both, or neither).
35
-
36
- ## Installation
37
-
38
- ```bash
39
- npm install -g @antoneeo/kb-agentic-skill@latest
40
- ```
41
-
42
- That is enough — the package's `postinstall` runs the installer. If your npm blocks
43
- install scripts (`--ignore-scripts`, some CI/pnpm setups), run it by hand:
44
-
45
- ```bash
46
- kb-agentic-install-skill
47
- ```
48
-
49
- > The command is on your PATH only after a **global** (`-g`) install; after a local
50
- > `npm i`, invoke it as `npx kb-agentic-install-skill`.
51
-
52
- The installer copies `skills/kb-agentic-skill/` recursively into native skill locations:
53
-
54
- - Claude Code: `~/.claude/skills/kb-agentic/`
55
- - Codex: `~/.codex/skills/kb-agentic/`
56
- - Gemini CLI: `~/.gemini/skills/kb-agentic/`
57
- - Google Antigravity: `~/.gemini/config/skills/kb-agentic/` (override the home with `ANTIGRAVITY_HOME`)
58
-
59
- Restart the relevant agent, or reload skills where the CLI supports it.
60
-
61
- Initialize a project:
62
-
63
- ```bash
64
- kb-agentic-init
65
- ```
66
-
67
- Run it inside a project to create `ai_docs/`, Vision documents, strategic docs, audit plan, and agent protocol files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`).
68
-
69
- ## The family: three lenses, one spine
70
-
71
- | Package | Faithful to | Unit of work |
72
- |---|---|---|
73
- | [`@antoneeo/agentic-sdlc-skill`](https://www.npmjs.com/package/@antoneeo/agentic-sdlc-skill) | this repository's code | feature |
74
- | `@antoneeo/kb-agentic-skill` (this one) | the documents you supply | topic |
75
- | [`@antoneeo/mkt-agentic-sdlc-skill`](https://www.npmjs.com/package/@antoneeo/mkt-agentic-sdlc-skill) | market evidence | engagement |
76
-
77
- Triage, the Vision Gate, the review gates, the guide router, question discipline and the validator spine are byte-identical across the three. When two live in the same project, `routing.md` decides which lens owns a given piece of work, and any of the three validators gives the same verdict on the same tree.
78
-
79
- ## Standalone vs Hybrid
80
-
81
- - **Standalone** — `ai_docs/` is the source of truth: vision, topics, corpus, audit, handoff.
82
- - **Hybrid with devPNT** — devPNT governs `M-VISION`, Master Plan, Action Plan and versioned artifacts; `ai_docs/` stays as readable context, fallback and shadow. Divergence between your request, the local Vision and the M-VISION is surfaced before any work.
83
-
84
- ## Created By
85
-
86
- Created by **Antonio Pinto** ([GitHub](https://github.com/Antoneeo)).
87
-
88
- MIT (c) 2026 Antonio Pinto.
1
+ # KB Agentic Skill for Claude Code, Gemini CLI, Google Antigravity & Codex
2
+
3
+ `kb-agentic` turns an AI agent into the keeper of a second brain built **on the documents you supply** — not on what the model remembers. It supports Claude Code, Codex, Gemini CLI, Google Antigravity 2.0, Cursor/Windsurf-style project instructions, and optional devPNT governance.
4
+
5
+ The sibling of [`@antoneeo/agentic-sdlc-skill`](https://www.npmjs.com/package/@antoneeo/agentic-sdlc-skill), transplanted from source code to knowledge: same process, one different fidelity discipline.
6
+
7
+ ## What it does
8
+
9
+ Two axes are kept apart on purpose — **abstraction** (a topic made of topics: the graph edges) and **certainty** (how strongly the corpus supports a statement: the claim rows).
10
+
11
+ 1. **Intake — the source becomes untouchable.** Every document enters verbatim under `corpus/given/`, content-addressed by a raw-byte digest. A new version is appended with `supersedes:`, never overwritten. Non-text files get a **stored canonical extraction** (`.txt`, registered extractor) — those are the bytes locators point at, so an id stays valid across re-ingestion. On a large binary corpus the extraction **is** the artifact: the digest moves onto it (immutability still enforced, on the bytes that actually matter) and the original stays where it lives, recorded as `original_path:` + `original_sha256:`. 233 MB of manuals do not enter your repository to protect bytes no locator addresses — and the two recorded fields are honest about their limit: they let a human re-verify, they check nothing on their own. Spoken input is a note with `origin: elicited`; a synthesis carries `derived_from:`; your decision carries `basis:`. A note with none of the three is model knowledge dressed as a source, and the validator rejects it.
12
+
13
+ 2. **Extraction — the unit is the claim.** Rows of `id | claim | valid | qty | about | source | prov | state`. The **id hashes the location and the quantity, never the text**, so an LLM rephrasing mints no new identity. The locator (`p=17@412-509`) is verified: the validator opens the extraction and checks the span exists — and `anchor <path> <phrase>` produces it for you, matching spaces as `\s+` because a PDF extraction breaks phrases mid-line. **Gates are extracted alongside powers**: for every row saying what something can do, the source is asked what must hold first — default-off, licence tier, version floor, dependency — because "yes, supported" without the gate is a plan that fails on site. The rule is *ask*, never *produce*: a source that states no gate yields no row. Validity scopes are half-open ("until March" and "from March" do not conflict). Quantities are typed — mixed kinds or currencies **refuse to sum**. What the source does not assert becomes a `gaps:` line, never a claim.
14
+
15
+ **The source is exhausted, not sampled.** "Invents nothing" is a floor, and an extractor that stops when nothing it wrote is false stops on page twenty of a two-hundred-page manual with every row correct — which is exactly what a 200-page manual produces in the field. So a long source is read in **bounded windows** (30 pages by default, one plan task each, the ledger holding your place across sessions), and every window closes by advancing `extracted_through:` on the artifact's sidecar. That field is what makes "I am finished" falsifiable: claims with no coverage recorded are an error, a claim addressing a page past the declared coverage is a contradiction, and coverage short of the end is reported until it reaches it. Its limit is stated where it is written — **nothing proves a page was read**; what changes is that the shortcut must now be written down to pass. And exhaustive means read, never *a row per page*: a page that asserts nothing yields nothing.
16
+
17
+ 3. **Placement — five verdicts, after querying the graph.** Descent through the generated index following every parent (polyhierarchy). EXISTS → reconcile; INADEQUATE → child; **MISSING only after the graph was actually asked**; GENERALIZES → escalate (a new root stops at you); UNPLACED → quarantine. Similar-but-maybe-different becomes a sibling **with the distinguishing line written** — if you cannot write it, it is the same concept. Cycles are refused at write time; merged nodes leave a tombstone with `redirect_to:`, never a deletion.
18
+
19
+ 4. **Reconciliation — the machine detects and holds, it never decides.** Five outcomes: new / confirmation (the source is appended to the row — the base strengthens, it does not lengthen) / refinement (the old row goes `SUPERSEDED`, its text intact) / coexistence (disjoint scopes) / conflict → the whole set goes `CONTESTED`, **symmetrically**: flipping one cell by hand fails the check. Only new information resolves it — a later source, or **your ruling with a `basis:`**, the fact you know and the corpus does not. No basis, no ruling: a preference is not a fact. A ruling is challengeable — a later document reopens the case with your basis beside it.
20
+
21
+ 5. **Escalation in one batch at the end of the run**, in legal form (the claims, the reopenable sources, the dates, why the machine cannot decide). Ingestion never stops to interrogate you.
22
+
23
+ Deliberately absent: any per-node coverage or completion state. `gaps:` says what a node lacks; nothing collects it into a dashboard. A **source** does record how far it has been read, but on its own sidecar and nowhere else — the corpus index prints that fact for every artifact, including the finished ones, because a list of only what is behind is the dashboard this method refuses.
24
+
25
+ ## Key features
26
+
27
+ - **Risk-proportional triage, measured in knowledge and never in files**: one claim row → propagating a fact already settled → a new knowledge unit (a source ingested, a node created or superseded, the hierarchy moved). Carrying one settled fact into eight documents is small; one claim that re-parents a node is not. A **Write Triggers** table maps each knowledge event to exactly one destination.
28
+ - **Portable knowledge**: `export` bundles a subgraph together with the bytes its claims cite — a closure, not a selection, because a claim whose source cannot be reopened is model knowledge arriving by another route. `import` merges it into another project **additively**: it never overwrites a node and never deletes, and claims already present are recognised by id rather than by comparing text, since the same artifact cited at the same span mints the same id in every project. Knowledge crosses the project boundary; **authority does not** — an imported ruling arrives as `prov: IMPORTED`, keeps its original `basis:`, and cannot settle a local disagreement until you re-ratify it.
29
+ - **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.
30
+ - **Vision-guided governance**: Standalone projects use `ai_docs/vision/`; Hybrid projects use devPNT `M-VISION` as the milestone north star. `DRAFT` informs, `APPROVED` binds, promotion is the user's alone.
31
+ - **Independent review, twice**: the design before it is implemented, the result before it is declared done fresh-context subagent > one-shot run > a declared self-pass, 3 rounds max, one log line each, and a PASS is invalid on "found nothing".
32
+ - **Question discipline**: a question is legal only when the agent searched first, names the search with its result, and names the decision it unblocks; otherwise it proceeds on a declared assumption, batched.
33
+ - **Operative guides + agent-global KB**: distil user-provided indications into source-faithful `GUIDE_*.md` (`source_kind: document`) verbatim snapshot plus hash, so drift is detected mechanically.
34
+ - **Mechanical checks**: `check`, `validate`, `index`, `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, plus the spine's `stale`/`mark`/`gate`/`plan`/`orient`/`migrate`. The graph and corpus checks verify spans against the stored extraction, recompute every id, refuse cycles and unreachable nodes, and enforce `CONTESTED` symmetry.
35
+ - **Installed support files**: Claude, Codex, Gemini and Google Antigravity receive the full skill folder — `SKILL.md`, `templates.md`, `taxonomy.md`, `distillation.md`, `reconciliation.md`, `guides.md`, `vision.md`, `elicitation.md`, `review.md`, `dispatch.md`, `routing.md`, `ENFORCEMENT.md`, and the validator's two files, `scripts/sdlc_check.py` + `scripts/sdlc_core.py` (the core is the family's shared spine — copy both, or neither).
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install -g @antoneeo/kb-agentic-skill@latest
41
+ ```
42
+
43
+ That is enough the package's `postinstall` runs the installer. If your npm blocks
44
+ install scripts (`--ignore-scripts`, some CI/pnpm setups), run it by hand:
45
+
46
+ ```bash
47
+ kb-agentic-install-skill
48
+ ```
49
+
50
+ > The command is on your PATH only after a **global** (`-g`) install; after a local
51
+ > `npm i`, invoke it as `npx kb-agentic-install-skill`.
52
+
53
+ The installer copies `skills/kb-agentic-skill/` recursively into native skill locations:
54
+
55
+ - Claude Code: `~/.claude/skills/kb-agentic/`
56
+ - Codex: `~/.codex/skills/kb-agentic/`
57
+ - Gemini CLI: `~/.gemini/skills/kb-agentic/`
58
+ - Google Antigravity: `~/.gemini/config/skills/kb-agentic/` (override the home with `ANTIGRAVITY_HOME`)
59
+
60
+ Restart the relevant agent, or reload skills where the CLI supports it.
61
+
62
+ Initialize a project:
63
+
64
+ ```bash
65
+ kb-agentic-init
66
+ ```
67
+
68
+ Run it inside a project to create `ai_docs/`, Vision documents, strategic docs, audit plan, and agent protocol files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`).
69
+
70
+ ## The family: three lenses, one spine
71
+
72
+ | Package | Faithful to | Unit of work |
73
+ |---|---|---|
74
+ | [`@antoneeo/agentic-sdlc-skill`](https://www.npmjs.com/package/@antoneeo/agentic-sdlc-skill) | this repository's code | feature |
75
+ | `@antoneeo/kb-agentic-skill` (this one) | the documents you supply | topic |
76
+ | [`@antoneeo/mkt-agentic-sdlc-skill`](https://www.npmjs.com/package/@antoneeo/mkt-agentic-sdlc-skill) | market evidence | engagement |
77
+
78
+ Triage, the Vision Gate, the review gates, the guide router, question discipline and the validator spine are byte-identical across the three. When two live in the same project, `routing.md` decides which lens owns a given piece of work, and any of the three validators gives the same verdict on the same tree.
79
+
80
+ ## Standalone vs Hybrid
81
+
82
+ - **Standalone** — `ai_docs/` is the source of truth: vision, topics, corpus, audit, handoff.
83
+ - **Hybrid with devPNT** — devPNT governs `M-VISION`, Master Plan, Action Plan and versioned artifacts; `ai_docs/` stays as readable context, fallback and shadow. Divergence between your request, the local Vision and the M-VISION is surfaced before any work.
84
+
85
+ ## Created By
86
+
87
+ Created by **Antonio Pinto** ([GitHub](https://github.com/Antoneeo)).
88
+
89
+ MIT (c) 2026 Antonio Pinto.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kb-agentic-skill",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antoneeo/kb-agentic-skill",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
5
  "keywords": [
6
6
  "claude-code",
package/scripts/init.js CHANGED
@@ -105,6 +105,34 @@ const writeIfNotExists = (relPath, content, description) => {
105
105
 
106
106
  seedFiles.forEach(([relPath, content]) => writeIfNotExists(relPath, content));
107
107
 
108
+ const EOL = String.fromCharCode(10);
109
+ // 5b. Merge hygiene for the append-only review log (F-028, defence in depth).
110
+ // `merge=union` is a BUILT-IN driver: no per-clone `git config`, unlike
111
+ // `merge=ours`, which silently does nothing until every clone configures it.
112
+ // Rows are date-stamped, so interleaving them loses no information and their
113
+ // order carries none. Create-only, and appended rather than rewritten: a user's
114
+ // own .gitattributes is never clobbered.
115
+ const gitattributes = () => {
116
+ const marker = 'ai_docs/audit/reviews/REVIEW_LOG.md';
117
+ const stanza = [
118
+ '',
119
+ '# agentic-sdlc: the review log is append-only and its rows are date-stamped,',
120
+ '# so a union merge keeps both sides instead of asking a human to pick one.',
121
+ marker + ' merge=union',
122
+ '',
123
+ ].join(EOL);
124
+ const filePath = path.join(cwd, '.gitattributes');
125
+ if (!fs.existsSync(path.join(cwd, '.git'))) return; // not a git repo: nothing to do
126
+ const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
127
+ if (current.includes(marker)) {
128
+ console.log('SKIP .gitattributes already covers the review log.');
129
+ return;
130
+ }
131
+ fs.writeFileSync(filePath, current + stanza, 'utf8');
132
+ console.log('OK .gitattributes: review log set to merge=union (concurrent reviews merge cleanly).');
133
+ };
134
+ gitattributes();
135
+
108
136
  // 6. Client discovery and protocol pointers
109
137
  console.log('\n--- Environment Analysis ---');
110
138
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: kb-agentic
3
+ version: 1.4.1
3
4
  description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs and knowledge management.
4
5
  author: Antonio Pinto (https://github.com/Antoneeo)
5
6
  copyright: (c) 2026 Antonio Pinto
@@ -74,8 +75,10 @@ Triage decides IF documentation is due; this table decides WHICH document each e
74
75
  | `solutions/ANALYSIS_[topic].md` | Every L3, after elicitation and before any drafting. On topic match with an existing analysis, update that one instead of a new file. | 3 |
75
76
  | `solutions/SPIKE_[topic].md` | Closing any Spike — including a negative outcome. | — |
76
77
  | `vision/features/VISION_[feature].md` | Multi-milestone topic at analysis time, or retroactive trigger (second ANALYSIS on same theme). | 3 |
77
- | `audit/handoff.md` (workstream registry) | One row per OPEN workstream (topic, level, status, since, next step) refresh at every L3 closure and session end. 20 lines. | 5 / session end |
78
- | `audit/HANDOFF_[topic].md` | Session ends with that topic unfinished AND there is volatile resume state. Ephemeral, deleted at closure. | 4 / 5 / session end |
78
+ | `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. Inventory for lookup, never a work board. | 5 / session end |
79
+ | `audit/HANDOFF_[topic].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. Carries the resume logistics too; the ANALYSIS Diary keeps the durable narrative (DRY). DELETED at closure — deleting it *is* removing the row. | 4 / 5 / session end |
80
+ | `audit/project_notes.md` | A note true for the whole project rather than one workstream. Appended verbatim to the generated registry, so regenerating cannot destroy notes that belong to no workstream. | 5 / session end |
81
+ | `audit/handoff.md` — converting an existing project | **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 exists nothing generates and nothing errors. | 5 / session end |
79
82
  | `audit/audit_plan.md` (Standalone) | Bootstrap, and whenever a mapped area changes state (`sdlc_check.py mark`). | 1 |
80
83
  | `reference/GUIDE_[topic].md` (`source_kind: document`) | Origin+purpose test (`guides.md`), or a proactive proposal accepted by user. | 4 / 5 |
81
84
  | `reference/GUIDE_[topic].md` (`source_kind: code`/`domain`) | Recognized high-complexity domain/concept with no CURRENT guide $\rightarrow$ duty to write autonomously. | 4 / 5 |
@@ -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:
@@ -83,7 +83,7 @@ Must-reads for this project, in order. The full manifest of canonical docs is
83
83
  1. `reference/INDEX.md` — the guide router: which guide already governs the work you are about to do (generated).
84
84
  2. `vision/project_vision.md` — why the project exists (check its Status first).
85
85
  3. `strategic/architecture.md` — how it is built.
86
- 4. `audit/handoff.md` — where work stopped last session (if present).
86
+ 4. `audit/handoff.md` — where work stopped last session (generated from the `HANDOFF_*.md` beside it; never edited by hand).
87
87
 
88
88
  Directory purposes: `vision/` (project direction), `strategic/` (architecture and
89
89
  feature catalog), `reference/` (operative guides), `solutions/` (per-feature
@@ -389,36 +389,50 @@ States: PENDING (to analyze) | ANALYZED (analyzed, with reference) | SKIPPED (wi
389
389
  | vendor/ | SKIPPED | - | vendored code |
390
390
  ```
391
391
 
392
- ## ai_docs/audit/handoff.md — the workstream registry
393
-
394
- One row per OPEN workstream, ≤ 20 lines. **Parallel-safe by construction**: closing
395
- one milestone removes one row and never touches another's resume point — the defect
396
- this replaces was a single narrative slot where the last session to close overwrote
397
- everyone else's handoff. It is an **inventory for lookup** (like the generated
398
- manifest), not a work board: no assignment, no due dates, no execution ordering.
399
-
400
- Updated at every L3 closure (row removed) AND at session end with work still
401
- IN_PROGRESS (row refreshed) see Write Triggers.
402
-
403
- **Coming from a pre-1.17 project** (narrative handoff with `## Active features` /
404
- `## Next step` / `## Session notes`): nothing is broken and nothing is urgent — the
405
- validator only checks the `Date:` header and its age, and the orientation hook reads
406
- the file verbatim. Read it as a one-row registry, and convert it the next time the
407
- write trigger fires: each `## Active features` bullet becomes a row, `## Next step`
408
- becomes that row's next step, `## Session notes` becomes `## Project-wide notes`.
409
- Migrating a repository that is not being worked on buys nothing.
392
+ ## ai_docs/audit/handoff.md — the workstream registry (GENERATED)
393
+
394
+ One row per OPEN workstream. **Never written by hand**: `sdlc_check.py index` builds
395
+ it from the `HANDOFF_[topic].md` files, and `validate` errors when the two disagree.
396
+ It is an **inventory for lookup** (like the generated manifest), not a work board: no
397
+ assignment, no due dates, no execution ordering, no holder.
398
+
399
+ **Why generated, and not just one row per workstream.** Row-per-workstream alone was
400
+ tried and was not enough: two workstreams opened from one base still conflicted twice
401
+ in this file, because a file-global `Date:` header defeats row-level ownership no
402
+ matter how few rows each writer touches. So the truth moved into the per-workstream
403
+ file, and the header is derived (the newest `updated:` in the sources — a value, never
404
+ a filesystem timestamp, which git does not preserve). Two writers on two workstreams
405
+ now touch two different files. The generated view can still conflict at merge; that
406
+ conflict is resolved **mechanically** by re-running `index`, never by hand, and
407
+ `validate` refuses CLEAN until the file matches its sources.
408
+
409
+ Project-wide notes have their own source, `ai_docs/audit/project_notes.md`, appended
410
+ verbatim under `## Project-wide notes`. (Not `handoff_notes.md`: the `HANDOFF_*.md`
411
+ glob is case-insensitive on Windows and would collect it as a workstream.)
412
+
413
+ **Converting an existing project** — lazily, at the first write, and **all at once**.
414
+ Converting one row at a time is the state that loses the others: the next `index`
415
+ would regenerate from the one source and drop the rest. `index` refuses to write while
416
+ anything in the file is unaccounted for, and names it. A pre-1.17 narrative handoff
417
+ (`## Active features` / `## Next step` / `## Session notes`) is the same conversion:
418
+ each bullet becomes a `HANDOFF_[topic].md`, `## Session notes` becomes
419
+ `project_notes.md`. A project with no sources yet is not touched and reports nothing —
420
+ migrating a repository nobody is working on buys nothing.
410
421
 
411
422
  ```markdown
412
423
  # Handoff — workstream registry
413
424
  Date: 2026-06-11 (UTC)
414
425
 
426
+ <!-- GENERATED by sdlc_check.py index - do not edit by hand. Source of truth: the HANDOFF_*.md files in ai_docs/audit/. -->
427
+
415
428
  | Workstream | Level | Branch | Status | Since | Next step | Details |
416
429
  |---|---|---|---|---|---|---|
417
430
  | F-001 SSO login | L3 | feature/sso-login | PROGRESS | 2026-06-10 | wire callback tests | HANDOFF_login_sso.md · ANALYSIS_login_sso.md |
418
- | F-002 Audit refresh | L3 | feature/audit | PAUSED | 2026-06-02 | resume at Phase 4 | ANALYSIS_audit_refresh.md (no volatile state) |
431
+ | F-002 Audit refresh | L3 | feature/audit | PAUSED | 2026-06-02 | resume at Phase 4 | HANDOFF_audit_refresh.md · ANALYSIS_audit_refresh.md |
419
432
 
420
433
  ## Project-wide notes
421
- <!-- one or two lines: release pending, environment quirks that affect everyone -->
434
+
435
+ <!-- from audit/project_notes.md: release pending, environment quirks that affect everyone -->
422
436
  ```
423
437
 
424
438
  ## ai_docs/audit/reviews/REVIEW_LOG.md
@@ -453,21 +467,47 @@ records the realization actually used — fresh subagent, one-shot client run, o
453
467
  honest; writing nothing, or implying independence you did not have, is the failure
454
468
  this column exists to prevent. `findings_real` is how many raised findings survived
455
469
  triage: over time it is the only evidence of whether the gate earns its cost.
456
-
457
- ## ai_docs/audit/HANDOFF_[topic].md volatile resume logistics (ephemeral)
458
-
459
- **Resume logistics ONLY; the ANALYSIS Diary keeps the durable narrative (DRY).**
460
- The boundary: Diary = what happened and why (decisions, state of the work survives
461
- forever); this file = how to pick the work back up (branch/worktree, uncommitted
462
- state, environment notes, the next concrete command — worthless once resumed).
463
- Created only when a session pauses the feature WITH volatile state to record;
464
- **DELETED at the feature's closure**, in the same step that flips the ANALYSIS to
465
- COMPLETEDanything in it worth keeping was in the wrong file.
470
+ Concurrent reviews: `init` writes a `.gitattributes` stanza giving this file
471
+ `merge=union`a **built-in** driver (no per-clone `git config`, unlike
472
+ `merge=ours`, which silently does nothing until every clone configures it).
473
+ Rows are date-stamped and their order carries no meaning, so a union merge keeps
474
+ both sides instead of asking a human to choose. It is defence in depth: without
475
+ git, or without the stanza, the outcome is today's one conflict you resolve by
476
+ hand, never a lost row.
477
+
478
+
479
+ ## ai_docs/audit/HANDOFF_[topic].md one open workstream, its own file
480
+
481
+ **The authored home of that workstream's registry row**, and the only one: the
482
+ registry is generated from these files. **One exists for every OPEN workstream**, with
483
+ or without volatile state — a workstream whose file is missing has no row, and a
484
+ workstream with no row is invisible to the next cold agent. **DELETED at the feature's
485
+ closure**, in the same step that flips the ANALYSIS to COMPLETED: deleting it *is*
486
+ removing the row.
487
+
488
+ **The DRY boundary, restated because the file is no longer rare.** What used to keep
489
+ narrative out of it was that it barely existed; now it always does. So: the ANALYSIS
490
+ Diary keeps **what happened and why** (decisions, state of the work — survives
491
+ forever), and this file keeps **the row plus the resume logistics** — how to pick the work back up (branch,
492
+ worktree, uncommitted state, the next concrete command — worthless once resumed).
493
+ Prose that would still be worth reading after closure is in the wrong file, because
494
+ this one is deleted.
495
+
496
+ The frontmatter IS the row. `workstream:` is what marks the file as a source: without
497
+ it the file is still a perfectly good volatile note, and nothing generates.
466
498
 
467
499
  ```markdown
500
+ ---
501
+ workstream: F-001 SSO login
502
+ level: L3
503
+ branch: feature/sso-login (worktree ../wt-sso)
504
+ status: PROGRESS
505
+ since: 2026-06-10
506
+ next: wire the callback tests
507
+ details: ANALYSIS_login_sso.md
508
+ updated: 2026-06-11
509
+ ---
468
510
  # HANDOFF: [topic] (ephemeral — deleted at closure)
469
- Updated: 2026-06-11 (UTC)
470
- Branch: feature/sso-login (worktree ../wt-sso)
471
511
 
472
512
  ## Resume state
473
513
  <!-- uncommitted files, half-run migrations, env vars, running services -->
@@ -479,6 +519,19 @@ Branch: feature/sso-login (worktree ../wt-sso)
479
519
  <!-- traps discovered this session that bite on resume (locks, CRLF, flaky test) -->
480
520
  ```
481
521
 
522
+ `updated:` is the date this file last changed, and the newest one across all sources
523
+ becomes the registry's `Date:` header — which is why no writer ever edits that header
524
+ and why two concurrent writers no longer collide on it. `details:` holds the *other*
525
+ pointers (the ANALYSIS, a review log entry); the generator prepends this file's own
526
+ name, so nothing points at itself by hand.
527
+
528
+ ## ai_docs/audit/project_notes.md — the registry's project-wide notes (source)
529
+
530
+ Plain lines, no frontmatter, appended verbatim to the generated registry under
531
+ `## Project-wide notes`. Release pending, environment quirks, anything true for
532
+ everyone rather than for one workstream. It exists so that generating the registry
533
+ cannot destroy notes that belong to no workstream.
534
+
482
535
  ## ai_docs/strategic/architecture.md and existing_features.md
483
536
 
484
537
  Canonical docs: they open with the header (`description:`/`status:`) so they enter the `INDEX.md` manifest cleanly.