@antoneeo/agentic-sdlc-skill 1.20.3 → 1.21.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 +231 -0
- package/README.md +1 -0
- package/gemini-extension.json +1 -1
- package/package.json +50 -50
- package/scripts/init.js +28 -0
- package/skills/agentic-sdlc-skill/SKILL.md +7 -5
- package/skills/agentic-sdlc-skill/scripts/sdlc_core.py +199 -3
- package/skills/agentic-sdlc-skill/templates.md +86 -33
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,237 @@
|
|
|
2
2
|
|
|
3
3
|
Tutte le modifiche significative a questa skill saranno documentate in questo file.
|
|
4
4
|
|
|
5
|
+
## [1.21.1 / kb 1.4.1 / mkt 0.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.21.0 / kb 1.4.0 / mkt 0.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
|
+
|
|
70
|
+
## [kb 1.3.0] - 2026-08-03
|
|
71
|
+
|
|
72
|
+
F-031 — from the field: *"the ingestion agent takes shortcuts, and a lot of information
|
|
73
|
+
is lost. The graph works only if all of a file's information is poured into it."* An
|
|
74
|
+
agent handed a 200-page manual emitted a few dozen claims and reported done. Nothing it
|
|
75
|
+
did broke a rule — and that is the finding. `distillation.md` carried a floor ("the
|
|
76
|
+
extractor invents nothing") and no target, so an agent optimizing against it stops the
|
|
77
|
+
moment nothing it wrote is false. **Nothing the field agent emitted was wrong.** Third
|
|
78
|
+
appearance of one class: a rule that describes a good output and never says which
|
|
79
|
+
outputs must exist.
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
- **A north star at the top of `distillation.md`**, above the rules rather than beside
|
|
83
|
+
them: *not one assertion the source makes may be lost, and not one it does not make
|
|
84
|
+
may appear.* One sentence, deliberately — two rules a paragraph apart get optimized
|
|
85
|
+
whichever was read last. The unit is the **assertion**, not the byte: layout and page
|
|
86
|
+
furniture are not assertions, exhaustive means **read** and never *a row per page*.
|
|
87
|
+
- **`extracted_through:`** on the artifact's own sidecar (`p=<n>`, `L<n>`, `complete`) —
|
|
88
|
+
required once any claim cites it. It is what makes "I am finished" falsifiable, and it
|
|
89
|
+
makes the rows and the field check each other: claims with no coverage recorded error;
|
|
90
|
+
a claim addressing past the declared coverage, or coverage past the end of the stored
|
|
91
|
+
bytes, is a contradiction and errors; coverage short of the end warns, because partial
|
|
92
|
+
work is legal mid-ingestion. An artifact nobody has extracted from owes nothing.
|
|
93
|
+
- **Bounded reading windows** — 30 pages by default, *the plan states the window used*
|
|
94
|
+
(a hard number baked into doctrine ages badly across models and page densities). One
|
|
95
|
+
plan task per window: the existing `PLAN_[topic].md` ledger is the register an
|
|
96
|
+
ingestion resumes from across sessions, so **no second register was built** — one
|
|
97
|
+
would have been the work board this method refuses.
|
|
98
|
+
- **A coverage cell in `corpus/INDEX.md`**, printed for **every** artifact including the
|
|
99
|
+
finished ones. A list of only what is behind is the dashboard the Vision rejects, and
|
|
100
|
+
a test fails the suite if the report ever becomes one.
|
|
101
|
+
- Cold-run scenario `long_source_is_exhausted_not_sampled.md`: sampling a long source and
|
|
102
|
+
reporting it ingested is the FAIL — and so is manufacturing a row per page.
|
|
103
|
+
|
|
104
|
+
The limit is stated wherever the field is written, as with `original_sha256`: **nothing
|
|
105
|
+
proves a page was read.** A field advanced without extracting is invisible to any
|
|
106
|
+
checker, precisely because a page that asserts nothing legitimately yields no rows — that
|
|
107
|
+
direction belongs to the ingestion review. What changed is that the shortcut must now be
|
|
108
|
+
written down to pass, and a written claim can be reopened by anyone who cares to.
|
|
109
|
+
|
|
110
|
+
### Upgrading an existing corpus
|
|
111
|
+
`check` will error on every `corpus/given/` artifact that has claims and no
|
|
112
|
+
`extracted_through:`. That is the migration, and it is the point: state how far each
|
|
113
|
+
source was actually read (`complete` if it was finished). The message names the artifact,
|
|
114
|
+
the first row citing it, and the three accepted forms. `corpus/INDEX.md` gains the
|
|
115
|
+
coverage cell, so run `sdlc_check.py index` once — until then `validate` reports it
|
|
116
|
+
out of alignment, as it does for any generated index.
|
|
117
|
+
|
|
118
|
+
## [kb 1.2.0] - 2026-08-03
|
|
119
|
+
|
|
120
|
+
F-030 — knowledge built in one project could not leave it. A practitioner who grounded
|
|
121
|
+
82 claims against a vendor's manuals started the next project with nothing and paid the
|
|
122
|
+
whole ingestion again from the same sources. The Core Problem this methodology exists
|
|
123
|
+
against is that understanding evaporates; it was evaporating at the **project boundary**.
|
|
124
|
+
|
|
125
|
+
### Added
|
|
126
|
+
- **`export --out <dir>`** bundles a subgraph together with **the bytes its claims
|
|
127
|
+
cite** — a closure, not a selection. A claim whose source cannot be reopened is model
|
|
128
|
+
knowledge arriving by another route, so the artifacts, sidecars and extractions travel
|
|
129
|
+
with it. The closure also pulls in the other half of any `CONTESTED` set, because the
|
|
130
|
+
symmetry check refuses a set that lost members — and says which topics it added, never
|
|
131
|
+
silently.
|
|
132
|
+
- **`import <dir>`** merges a bundle **additively**: never overwrites a node, never
|
|
133
|
+
deletes, and computes the whole plan before writing a byte (an import that half-applies
|
|
134
|
+
leaves a tree whose checks fail and whose owner cannot tell what landed). Refuses on a
|
|
135
|
+
path escaping the docs root, an artifact whose name matches an existing one with
|
|
136
|
+
different bytes, a directory carrying no `MANIFEST.md`, and a broken conflict set.
|
|
137
|
+
Placing an incoming concept stays the placement pass's job (`taxonomy.md`) — the import
|
|
138
|
+
reports the skipped node and decides nothing.
|
|
139
|
+
- **`prov: IMPORTED`** (owner ruling, 2026-08-03). Knowledge crosses a project boundary;
|
|
140
|
+
authority does not. A ruling from another project keeps its text, span and original
|
|
141
|
+
`basis:` verbatim, must declare `imported_from:`, and **cannot supersede a local row**
|
|
142
|
+
— the validator errors on it. Re-ratification is one honest act: your own note, your
|
|
143
|
+
own basis, `prov: RULING`. Relabelling it `DERIVED` was rejected because the row would
|
|
144
|
+
then lie about where it came from.
|
|
145
|
+
- **`portability.md`**, the doctrine the two commands cite.
|
|
146
|
+
|
|
147
|
+
The feature is small because of one property it did not have to build: `kb_claim_id`
|
|
148
|
+
hashes `path#locator#qty` with the **text excluded**, so the same artifact cited at the
|
|
149
|
+
same span mints the same id in every project. Cross-project de-duplication is therefore
|
|
150
|
+
mechanical rather than a judgement call, and importing the same bundle twice is a
|
|
151
|
+
provable no-op.
|
|
152
|
+
|
|
153
|
+
## [kb 1.1.1] - 2026-08-03
|
|
154
|
+
|
|
155
|
+
Three inconsistencies the same field practitioner found by reading 1.1.0 against
|
|
156
|
+
their own corpus — all of the class 1.1.0 exists to remove: the doctrine and the
|
|
157
|
+
machinery disagreeing, where an agent reads one and the checker obeys the other.
|
|
158
|
+
|
|
159
|
+
### Fixed
|
|
160
|
+
- **`SKILL.md` never named `anchor`.** The command shipped working and listed in
|
|
161
|
+
`--help`, while the support-file line still said "the knowledge overlay: `graph`,
|
|
162
|
+
`corpus`, `claim-id`" — so an agent reading only the doctrine never learned it
|
|
163
|
+
exists, which is how the practitioner found it (from the prose, by accident). New
|
|
164
|
+
kb invariant derives the expected list from `INTERCEPTED` and fails when SKILL.md
|
|
165
|
+
omits any of it; deriving is the point, a second hand-maintained list would be the
|
|
166
|
+
same defect again.
|
|
167
|
+
- **Two rules gave opposite outcomes on a binary corpus.** The Write Triggers row for
|
|
168
|
+
`corpus/given/*` still read "verbatim copy … non-text originals **also** get their
|
|
169
|
+
stored canonical extraction", contradicting the extraction-as-artifact rule added to
|
|
170
|
+
`distillation.md` §1 in 1.1.0. The row now states both forms and names §1 as the
|
|
171
|
+
owner of the rule.
|
|
172
|
+
- **`anchor` only worked from inside the docs root**, an asymmetry with `graph`,
|
|
173
|
+
`corpus` and `check`, which take `--root`. Those scan a tree and this one takes a
|
|
174
|
+
path, so the fix is to make the path resolve rather than to document where to stand:
|
|
175
|
+
a path that does not resolve from the current directory is retried under the docs
|
|
176
|
+
root, which means it can be given exactly as a claim's `source` cell carries it.
|
|
177
|
+
|
|
178
|
+
## [kb 1.1.0] - 2026-08-03
|
|
179
|
+
|
|
180
|
+
Scoped to `@antoneeo/kb-agentic-skill`. `agentic-sdlc-skill` (1.20.3) and
|
|
181
|
+
`mkt-agentic-sdlc` (0.3.1) are unchanged in what they ship: the shared invariant
|
|
182
|
+
added here lives in `test_skill_invariants.py`, which no distribution packages.
|
|
183
|
+
|
|
184
|
+
F-029 — six defects from the first full field application of `kb-agentic` by a
|
|
185
|
+
practitioner other than its author (233 MB of manuals, 51 corpus artifacts, 22 topic
|
|
186
|
+
nodes, 82 offset-verified claims). **None was an adherence failure**: in every one the
|
|
187
|
+
agent did what it was told and was wrong anyway, which makes them agent-UX defects.
|
|
188
|
+
Analysis: `ai_docs/solutions/ANALYSIS_kb_field_report.md`.
|
|
189
|
+
|
|
190
|
+
### Fixed
|
|
191
|
+
- **kb's triage levels were undecidable.** L1 and L2 carried the *identical* bound
|
|
192
|
+
("at most 1-2 files"), so no request could be classified L2 and anything touching
|
|
193
|
+
three files fell to L3 by "when in doubt, go higher". Root cause: kb never performed
|
|
194
|
+
the restatement the Vision requires of every sibling — the file counts were the code
|
|
195
|
+
lens's units, and they contradicted the escalation triggers two lines below them.
|
|
196
|
+
Restated in knowledge units by owner ruling (2026-08-02): one claim row → propagation
|
|
197
|
+
of an already-settled fact → a new knowledge unit. The one limit: propagation that
|
|
198
|
+
changes what a claim *asserts* is not propagation. New shared invariant asserts no two
|
|
199
|
+
graded levels state the same criteria, lens-agnostically (`L1..L3` here, `E1..E3` in
|
|
200
|
+
marketing) — mutation-tested RED before the fix.
|
|
201
|
+
- **The claim model recorded powers and skipped gates**, making every ledger
|
|
202
|
+
systematically optimistic on a corpus whose purpose is to deflate over-promising. A
|
|
203
|
+
query returned three verified rows saying "yes, supported" while omitting that the
|
|
204
|
+
feature ships disabled and needs a second construct — true, and a plan that fails on
|
|
205
|
+
site. `distillation.md` §3 now requires the gates alongside the powers, with the guard
|
|
206
|
+
that keeps it from becoming fabrication: the rule is *ask*, never *produce*.
|
|
207
|
+
- **`--help` hid the knowledge overlay.** Forward-by-default handed `--help` to the
|
|
208
|
+
spine, whose usage lists nine commands and none of kb's, so a user concluded the
|
|
209
|
+
overlay was not installed. Intercepted at `argv[0]` only, and the spine's own usage is
|
|
210
|
+
still rendered — forward-by-default is intact, with a regression test that proves a
|
|
211
|
+
non-intercepted command still reaches the spine.
|
|
212
|
+
- **kb's eval battery tested the wrong skill.** Six scenarios, none covering kb's own
|
|
213
|
+
method, two byte-identical to the code lens's and exercising an architect pass kb does
|
|
214
|
+
not ship — while extraction, placement, reconciliation, the corpus letter and locators
|
|
215
|
+
had no cold-run coverage at all. Rewritten in kb's units, three new scenarios added,
|
|
216
|
+
and a new invariant refuses any scenario citing a support file its distribution lacks.
|
|
217
|
+
The ANALYSIS template's example rows were the code lens's verbatim (Python paths in a
|
|
218
|
+
knowledge template); replaced with topic-graph rows.
|
|
219
|
+
|
|
220
|
+
### Added
|
|
221
|
+
- **`sdlc_check.py anchor <path> <phrase>`** — the half `claim-id` never had: it turns a
|
|
222
|
+
prose citation into a *verified* span. The check has always demanded resolved locators
|
|
223
|
+
and nothing produced them, so every one was hand-authored against a machine verifier.
|
|
224
|
+
Whitespace in the phrase matches as `\s+`, because a PDF extraction breaks phrases
|
|
225
|
+
mid-line and a probe that pretty-prints collapsed whitespace shows the phrase intact
|
|
226
|
+
while the checker sees the break. Refuses ambiguity and refuses what it cannot anchor,
|
|
227
|
+
and re-verifies every locator with `kb_check_locator` before emitting it — the tool can
|
|
228
|
+
never produce a span its own validator would reject.
|
|
229
|
+
- **Extraction-as-artifact** for large binary corpora (`distillation.md` §1): the stored
|
|
230
|
+
extraction may be the corpus artifact on its own, `sha256:` covering it, with the
|
|
231
|
+
original recorded as `original_path:` + `original_sha256:` instead of copied in. The
|
|
232
|
+
code already allowed this; the doctrine said the extraction came "additionally", so a
|
|
233
|
+
faithful reader copied gigabytes for nothing. The recorded fields state their limit
|
|
234
|
+
where they are defined: `original_sha256` is never checked.
|
|
235
|
+
|
|
5
236
|
## [1.20.3] - 2026-08-02
|
|
6
237
|
|
|
7
238
|
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.
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@antoneeo/agentic-sdlc-skill",
|
|
3
|
-
"version": "1.
|
|
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.1",
|
|
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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agentic-sdlc
|
|
3
|
+
version: 1.21.1
|
|
3
4
|
description: Documentation-First SDLC protocol with risk-proportional triage, Vision as a guide, a complete Standalone mode and optional symbiosis with devPNT. Use for features, significant bugs, refactors, audits and documented maintenance.
|
|
4
5
|
author: Antonio Pinto (https://github.com/Antoneeo)
|
|
5
6
|
copyright: (c) 2026 Antonio Pinto
|
|
@@ -64,9 +65,10 @@ Triage decides IF documentation is due; this table decides WHICH document each e
|
|
|
64
65
|
| `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
66
|
| `solutions/SPIKE_[topic].md` | Closing any Spike — including a failed one (a negative outcome is still an outcome). | — |
|
|
66
67
|
| `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) |
|
|
68
|
-
| `audit/
|
|
69
|
-
| `audit/
|
|
68
|
+
| `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 |
|
|
69
|
+
| `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 |
|
|
70
|
+
| `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 |
|
|
71
|
+
| `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
72
|
| `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
73
|
| `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
74
|
| `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 +190,7 @@ gate) instead of requiring an IN_PROGRESS ANALYSIS.
|
|
|
188
190
|
|
|
189
191
|
### 1. Audit and Alignment
|
|
190
192
|
|
|
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` (
|
|
193
|
+
- 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
194
|
- 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
195
|
- 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
196
|
- 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 +257,7 @@ Hybrid L3:
|
|
|
255
257
|
- 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
258
|
- **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
259
|
- Update only the documents actually impacted.
|
|
258
|
-
- **Update the workstream registry (`audit/handoff.md`)** — mandatory at every L3 closure:
|
|
260
|
+
- **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
261
|
- **Aligned indexes (Poka-Yoke)**: if you created, moved or removed canonical documents (`vision/`, `reference/`, `architecture/`, `functional/`, `strategic/`):
|
|
260
262
|
- regenerate the manifest with `sdlc_check.py index` (writes `ai_docs/INDEX.md`) — never write it by hand;
|
|
261
263
|
- 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 (
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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.
|