@antoneeo/agentic-sdlc-skill 1.39.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  Tutte le modifiche significative a questa skill saranno documentate in questo file.
4
4
 
5
+ ## [1.41.0] - 2026-10-17
6
+
7
+ ### Changed
8
+ - F-073: one written form for a dated entry, `**YYYY-MM-DD — label.**`, is taught for Diary entries, spike outcomes and project notes that record an event, and shared readers (`entry_openers`, `request_entries`, `adr_date`, `frontmatter_date`) let a chronology list those events the same way the validator reads them. Three new advisories, which never fail a build, not even `--strict`: undated text in the Diary of an L3 analysis started on or after 2026-10-18; a HANDOFF `next:` longer than 240 characters on an L3 file updated on or after that date (`next:` now holds only the next step, and the state of the work goes to the Diary); and, in `check` only, an open L3 unit whose id another branch or tag uses for a different analysis (one `git grep` over local refs, silent without git). The doctrine now says to choose a new unit's id after fetching, and that a session sharing a checkout with another session works in its own git worktree.
9
+
10
+ ## [1.40.0] - 2026-10-17
11
+
12
+ ### Changed
13
+ - F1: closing the last open workstream now empties the generated registry `audit/handoff.md` instead of leaving its stale row behind: with zero `HANDOFF_*.md` sources `index` rewrites it empty (keeping `project_notes.md`), and `validate` reports a generated registry that still has rows but no sources as an error.
14
+ - F-069: `graph`, `check`, `stale` and the session-start count are faster on a mature knowledge base, with byte-identical output. Citations of superseded claim ids are found with one pass per living document instead of one scan per (document, id) pair, and one check run reads and resolves each cited extraction once instead of once per claim row. On a synthetic ledger of 2183 rows and 1000 living documents, `graph` drops from 14 s to 4 s. No new flag.
15
+
5
16
  ## [1.39.0] - 2026-10-13
6
17
 
7
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-sdlc-skill",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
4
  "description": "Documentation-First SDLC protocol with triage, Vision governance and optional devPNT integration.",
5
5
  "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antoneeo/agentic-sdlc-skill",
3
- "version": "1.39.0",
3
+ "version": "1.41.0",
4
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
5
  "keywords": [
6
6
  "claude-code",
package/scripts/init.js CHANGED
@@ -203,16 +203,16 @@ in the skill's \`routing.md\`: it decides which lens's method and validation rul
203
203
  that unit. L1 never reaches it. Never refer to a document whose meaning differs by lens
204
204
  ("threat model", "vision", \`principles.md\`, \`handoff.md\`) by its bare name.
205
205
 
206
- Select the owning lens by the unit's purpose, then apply only its risk scale.
207
- The project's always-on pointer stays intact: consult the selected skill rather than
208
- merging multiple mandatory ladders. All lenses already carry shared project memory
209
- (\`memory.md\`); another installation is not needed for cross-domain recall.
206
+ Select the owning lens by the unit's purpose, then apply only its risk scale.
207
+ The project's always-on pointer stays intact: consult the selected skill rather than
208
+ merging multiple mandatory ladders. All lenses already carry shared project memory
209
+ (\`memory.md\`); another installation is not needed for cross-domain recall.
210
210
  This file is NOT auto-loaded by any client: it is a note for you, deliberately additive.
211
- Delete this note once the routing pointer is integrated.
211
+ Delete this note once the routing pointer is integrated.
212
212
  `, 'multi-lens routing note');
213
213
  if (wrote && protocolPreexisting) {
214
214
  console.log(' ⚠️ A protocol pointer already existed and was NOT overwritten.');
215
- console.log(' Integrate the routing pointer from AGENTIC_MULTI_LENS.md; keep one risk scale per unit.');
215
+ console.log(' Integrate the routing pointer from AGENTIC_MULTI_LENS.md; keep one risk scale per unit.');
216
216
  }
217
217
  }
218
218
 
@@ -14,17 +14,17 @@ python "<skill_dir>/scripts/sdlc_check.py" check
14
14
 
15
15
  ## 2. Check in CI (recommended for teams)
16
16
 
17
- Copy all three validator files into the repository: `scripts/sdlc_check.py`, `scripts/sdlc_core.py`, and `scripts/knowledge.py`, keeping them side by side. Then add to the pipeline:
17
+ Copy all three validator files into the repository: `scripts/sdlc_check.py`, `scripts/sdlc_core.py`, and `scripts/knowledge.py`, keeping them side by side. Then add to the pipeline:
18
18
 
19
19
  ```
20
20
  python tools/sdlc_check.py validate --strict
21
21
  ```
22
22
 
23
- The validator ships as **three files**: the domain entry point, `sdlc_core.py` and `knowledge.py`. Keep all three together. The core alone still runs but omits shared-memory/KB checks and domain-specific overlays; it is not an equivalent CI gate. `check` includes the knowledge surface when present.
23
+ The validator ships as **three files**: the domain entry point, `sdlc_core.py` and `knowledge.py`. Keep all three together. The core alone still runs but omits shared-memory/KB checks and domain-specific overlays; it is not an equivalent CI gate. `check` includes the knowledge surface when present.
24
24
 
25
25
  Effect: an unregenerated index, invalid frontmatter, a missing security section or incoherent states **block the pipeline** instead of relying on the agent's memory. `--strict` also fails on warnings and on a missing `ai_docs/`, so a wrong working directory cannot produce a green pipeline. This works because documents travel in the same PR as the code (Phase 5 rule).
26
26
 
27
- Note: the copy in the repo is the authoritative one for CI; update it when you update the skill — all three files, together.
27
+ Note: the copy in the repo is the authoritative one for CI; update it when you update the skill — all three files, together.
28
28
 
29
29
  **Projects whose docs root is not `ai_docs/`.** Pass `--docs-dir <name>` (e.g. `--docs-dir mkt_docs`) on any subcommand, or set `AGENTIC_SDLC_DOCS_DIR`. Without either, the validator walks up from the working directory and takes the nearest root it recognizes. If it finds two side by side — the shape of a half-finished migration — it refuses and names both rather than validating half a project and printing a verdict. `ai_docs/` remains the default and the recommended root: the parameter exists so a legacy tree can be read and migrated, not so a second one can be kept.
30
30
 
@@ -155,7 +155,7 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
155
155
  **Usage notes:**
156
156
  - The hook assumes the working directory is the project root (standard Claude Code hook behavior); it also accepts `--root <path>`.
157
157
  - **Hybrid/devPNT projects**: add `--hybrid` — the hook then appends a one-line pointer to run `devpnt_mcp_get_bootstrap` for the Master Plan / Knowledge Layer and does not replicate them; the filesystem orientation (router + handoff + README) still emits.
158
- - Like the CI gate (§2), if you copied the validator into the repo, the hook references that copy — keep all three files current when you update the skill.
158
+ - Like the CI gate (§2), if you copied the validator into the repo, the hook references that copy — keep all three files current when you update the skill.
159
159
 
160
160
  ### Per-turn reminder: `remind` (UserPromptSubmit -- wired by default)
161
161
 
@@ -215,7 +215,7 @@ It aggregates the test files (`test_plan.py` + `test_session_start.py` + `test_s
215
215
 
216
216
  **Optional CI** (same shape as §2, not mandatory): add a `run:` step invoking the `unittest discover` command above.
217
217
 
218
- **T10 note:** if you copied the validator (all three files) and the `test_*.py` battery into the repo for CI, that copy is authoritative — keep it current when you update the skill.
218
+ **T10 note:** if you copied the validator (all three files) and the `test_*.py` battery into the repo for CI, that copy is authoritative — keep it current when you update the skill.
219
219
 
220
220
  ## `benefit` is a report, never a CI gate
221
221
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: agentic-sdlc
3
- version: 1.39.0
3
+ version: 1.41.0
4
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.
5
5
  author: Antonio Pinto (https://github.com/Antoneeo)
6
6
  copyright: (c) 2026 Antonio Pinto
@@ -90,7 +90,7 @@ Triage decides IF documentation is due; this table decides WHICH document each e
90
90
  | `solutions/MERGE_REVIEW_[feature].md` | Every L3, after the closure review: the integrator's brief — what to review (`sdlc_check.py review-map`), what was verified by which gate, what only a human can judge (`templates.md`). The PR description links it. | 5 |
91
91
  | `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 |
92
92
  | `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 |
93
- | `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 |
93
+ | `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. Its `next:` is the next concrete step in one sentence; what happened, and the state of the work, go to the Diary as a dated entry. 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 |
94
94
  | `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 |
95
95
  | `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 |
96
96
  | `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 |
@@ -204,7 +204,7 @@ Hybrid L3:
204
204
  ### 4. Development and Testing
205
205
 
206
206
  - Implement only after the documentation gate required by the level. **Flip the ANALYSIS frontmatter `PLANNED` → `IN_PROGRESS` when implementation starts** — that flip is what `gate` and the handoff registry key on, and nothing else performs it.
207
- - Isolate the work: run an L3 change on its own branch. In Hybrid, prefer a git worktree from the start — a running devPNT server locks `.devpnt/*.db` and blocks in-place branch switches/merges in the primary worktree.
207
+ - Isolate the work: run an L3 change on its own branch. In Hybrid, prefer a git worktree from the start — a running devPNT server locks `.devpnt/*.db` and blocks in-place branch switches/merges in the primary worktree. A session that may run while another session uses the same checkout works in its **own git worktree**, and never switches branch, stashes, resets or runs `index` in a checkout another session may be using. Parallel work also shares ids: choose a new unit's id **after fetching**, as the first id free on every branch and tag the checkout can see (`check` names a collision while the unit is open).
208
208
  - Modify surgically, consistently with the plan.
209
209
  - Implementation work follows the TDD discipline in `tdd.md` (RED/GREEN/REFACTOR — the L2/L3 default; record the reason when it does not apply).
210
210
  - Before implementing (L2/L3; L1 exempt), **consult the guide router** — the verdict already declared with the triage level (`## Operative Guides`). Re-run the lookup here only if the work has moved to a topic the first one did not cover, and say so if the verdict changes.
@@ -34,11 +34,11 @@ proof that it caused this incident.
34
34
  Do not repeat an invalidated hypothesis unless new evidence reopens it.
35
35
 
36
36
  3. **Test and trace the mechanism.** Reproduce and isolate the smallest faithful
37
- failing case when feasible; bisect inputs, paths or changes when informative.
38
- Skip a reproduction already known to omit the incident's causal conditions;
39
- choose a faithful alternative or disclose the gap. Stop diagnostic checks when
40
- further results cannot change the causal or fix decision; this does not replace
41
- the regression and recovery verification in step 5.
37
+ failing case when feasible; bisect inputs, paths or changes when informative.
38
+ Skip a reproduction already known to omit the incident's causal conditions;
39
+ choose a faithful alternative or disclose the gap. Stop diagnostic checks when
40
+ further results cannot change the causal or fix decision; this does not replace
41
+ the regression and recovery verification in step 5.
42
42
  If the incident is intermittent or inaccessible, use captured traces and
43
43
  targeted diagnostic observations instead of declaring investigation impossible.
44
44
  Add minimal instrumentation when evidence is insufficient. Prefer read-only
@@ -125,6 +125,21 @@ def kb_unquote(value):
125
125
  return v
126
126
 
127
127
 
128
+ def frontmatter_date(meta):
129
+ """A note's or a sidecar's `date:` as `YYYY-MM-DD`, or None (F-073).
130
+ Quotes stripped, a datetime suffix tolerated, an impossible date refused --
131
+ the one parser behind orient's recency probe and any chronology reader."""
132
+ m = re.match(r"(\d{4}-\d{2}-\d{2})", kb_unquote((meta or {}).get("date")))
133
+ if not m:
134
+ return None
135
+ try:
136
+ import datetime as _dt
137
+ _dt.date.fromisoformat(m.group(1))
138
+ except ValueError:
139
+ return None
140
+ return m.group(1)
141
+
142
+
128
143
  def kb_parse_scope(text):
129
144
  """'-' | 'from X' | 'until X' | 'from X until Y' | 'if <cond>' ->
130
145
  ('always',) | ('window', from, until) | ('if', cond). Raises on junk."""
@@ -336,6 +351,17 @@ def kb_check_claims(root):
336
351
  if rel not in fm_cache:
337
352
  fm_cache[rel] = _note_frontmatter(root, rel)
338
353
  return fm_cache[rel]
354
+ # F-069: the same per-run memo for the source paths themselves -- each
355
+ # distinct path is resolved and stat'ed once, and each cited extraction
356
+ # read once (loc_cache, owned here, dropped when this call returns).
357
+ src_cache = {}
358
+ loc_cache = {}
359
+
360
+ def _resolved(path_s):
361
+ if path_s not in src_cache:
362
+ t = sdlc_core.confine_under(root, path_s)
363
+ src_cache[path_s] = (t, t is not None and t.is_file())
364
+ return src_cache[path_s]
339
365
  per_file_rows = []
340
366
  for p in sorted(topics.glob("*.md")):
341
367
  rel = "topics/" + p.name
@@ -357,16 +383,16 @@ def kb_check_claims(root):
357
383
  errors.append("%s: source %r has no locator" % (where, src))
358
384
  continue
359
385
  path_s, loc = src.rsplit("#", 1)
360
- target = sdlc_core.confine_under(root, path_s)
386
+ target, is_file = _resolved(path_s)
361
387
  if target is None:
362
388
  errors.append("%s: source %r escapes the docs root" % (where, src))
363
389
  continue
364
- if not target.is_file():
390
+ if not is_file:
365
391
  errors.append("%s: source %r does not resolve — a claim whose "
366
392
  "origin cannot be reopened is model knowledge"
367
393
  % (where, src))
368
394
  continue
369
- kb_check_locator(target, loc, where, errors)
395
+ kb_check_locator(target, loc, where, errors, loc_cache)
370
396
  # --- provenance ---
371
397
  prov = row["prov"]
372
398
  # Resolved ONCE, for every provenance: the non-GIVEN classes read it
@@ -556,8 +582,14 @@ def kb_check_claims(root):
556
582
  return errors, warnings, notes
557
583
 
558
584
 
559
- def kb_check_locator(target, loc, where, errors):
560
- """A locator must address an existing span of the stored bytes."""
585
+ def kb_check_locator(target, loc, where, errors, cache=None):
586
+ """A locator must address an existing span of the stored bytes.
587
+
588
+ `cache` is an optional dict the CALLER owns for one run (F-069): keyed by
589
+ the path actually read, it holds only what the checks compare against --
590
+ page lengths, a line count, or False for a missing extraction -- never the
591
+ text. Findings are still appended per call, so per row. Without it, every
592
+ call reads the file, as before."""
561
593
  m = LOC_PAGE_RE.match(loc)
562
594
  if m:
563
595
  page, a, b = int(m.group(1)), int(m.group(2)), int(m.group(3))
@@ -566,17 +598,24 @@ def kb_check_locator(target, loc, where, errors):
566
598
  return
567
599
  # offsets address the stored extraction (<stem>.txt beside the original)
568
600
  ext = target if target.suffix == ".txt" else target.with_suffix(".txt")
569
- if not ext.is_file():
601
+ key = ("p", ext)
602
+ if cache is None or key not in cache:
603
+ lengths = ([len(pg) for pg in sdlc_core.read_text(ext).split("\f")]
604
+ if ext.is_file() else False)
605
+ if cache is not None:
606
+ cache[key] = lengths
607
+ else:
608
+ lengths = cache[key]
609
+ if lengths is False:
570
610
  errors.append("%s: no stored extraction %s for locator %r — offsets "
571
611
  "must address kept bytes" % (where, ext.name, loc))
572
612
  return
573
- pages = sdlc_core.read_text(ext).split("\f")
574
- if page < 1 or page > len(pages):
613
+ if page < 1 or page > len(lengths):
575
614
  errors.append("%s: locator %r addresses page %d of %d" %
576
- (where, loc, page, len(pages)))
577
- elif b > len(pages[page - 1]):
615
+ (where, loc, page, len(lengths)))
616
+ elif b > lengths[page - 1]:
578
617
  errors.append("%s: locator %r spans past the end of page %d "
579
- "(%d chars)" % (where, loc, page, len(pages[page - 1])))
618
+ "(%d chars)" % (where, loc, page, lengths[page - 1]))
580
619
  return
581
620
  m = LOC_LINE_RE.match(loc)
582
621
  if m:
@@ -584,7 +623,13 @@ def kb_check_locator(target, loc, where, errors):
584
623
  if a > b or a < 1:
585
624
  errors.append("%s: locator %r has an invalid line range" % (where, loc))
586
625
  return
587
- n = sdlc_core.read_text(target).count("\n") + 1
626
+ key = ("L", target)
627
+ if cache is None or key not in cache:
628
+ n = sdlc_core.read_text(target).count("\n") + 1
629
+ if cache is not None:
630
+ cache[key] = n
631
+ else:
632
+ n = cache[key]
588
633
  if b > n:
589
634
  errors.append("%s: locator %r spans past line %d" % (where, loc, n))
590
635
  return
@@ -1085,12 +1130,18 @@ KB_GENERATED_RE = re.compile(r"GENERATED by \S+ index - do not edit by hand")
1085
1130
  KB_HEX12_RE = re.compile(r"^[0-9a-f]{12}$")
1086
1131
 
1087
1132
 
1088
- def _kb_hex_bounded(text, cid):
1089
- """True when cid appears NOT embedded in a longer hex run: a git hash
1090
- sharing a substring must never count as a citation (either side, either
1091
- case). cid is pre-validated 12-hex, so it is regex-safe by construction."""
1092
- return re.search(r"(?<![0-9a-fA-F])%s(?![0-9a-fA-F])" % cid,
1093
- text) is not None
1133
+ # A citation of a claim id is a run of exactly its 12 lowercase hex characters
1134
+ # NOT embedded in a longer hex run: a git hash sharing a substring must never
1135
+ # count as a citation (either side, either case). Two such bounded runs cannot
1136
+ # overlap, so findall returns every one of them.
1137
+ KB_HEX_TOKEN_RE = re.compile(r"(?<![0-9a-fA-F])[0-9a-f]{12}(?![0-9a-fA-F])")
1138
+
1139
+
1140
+ def _kb_hex_tokens(text):
1141
+ """The ids a text cites, extracted in one pass (F-069). Callers test
1142
+ membership instead of scanning the text once per id: that per-(document,
1143
+ id) scan made the time cycle grow with superseded ids x living documents."""
1144
+ return frozenset(KB_HEX_TOKEN_RE.findall(text))
1094
1145
 
1095
1146
 
1096
1147
  def _kb_df_entries(value):
@@ -1172,19 +1223,23 @@ def kb_time_cycle(docs):
1172
1223
  if (meta.get("status") or "").strip() == "SUPERSEDED":
1173
1224
  continue
1174
1225
  living.append((rel, text))
1175
- for rel, text in living:
1176
- for old in sorted(superseded):
1177
- if _kb_hex_bounded(text, old):
1178
- succs = superseded[old]
1179
- missing = tuple(x for x in succs
1180
- if not _kb_hex_bounded(text, x))
1181
- if missing:
1182
- cited.append((rel, old, missing, succs))
1226
+ # One token pass per living document, and none at all on a KB with no
1227
+ # supersession and no validity window (the orient line runs this at every
1228
+ # session start).
1229
+ tokens = ({rel: _kb_hex_tokens(text) for rel, text in living}
1230
+ if superseded or windows else {})
1231
+ for rel, _text in living:
1232
+ cites = tokens[rel] if tokens else frozenset()
1233
+ for old in cites.intersection(superseded):
1234
+ succs = superseded[old]
1235
+ missing = tuple(x for x in succs if x not in cites)
1236
+ if missing:
1237
+ cited.append((rel, old, missing, succs))
1183
1238
  today = _dt.date.today().isoformat()
1184
1239
  for topic_rel, rid, until in sorted(windows):
1185
1240
  if until <= today:
1186
- by = tuple(rel for rel, text in living
1187
- if rel != topic_rel and _kb_hex_bounded(text, rid))
1241
+ by = tuple(rel for rel, _text in living
1242
+ if rel != topic_rel and rid in tokens[rel])
1188
1243
  expired.append((topic_rel, rid, until, by))
1189
1244
  rev = {}
1190
1245
  node_meta = {}
@@ -1905,14 +1960,13 @@ def kb_cmd_orient(argv):
1905
1960
  # it -- the file was already fully read, so the cap bought
1906
1961
  # only failure. load_frontmatter is the authoritative
1907
1962
  # reader (fence-bounded, so a column-0 date: in the body
1908
- # still cannot misdate the note); kb_unquote covers quoted
1909
- # dates; the prefix match tolerates a datetime suffix.
1963
+ # still cannot misdate the note); frontmatter_date covers
1964
+ # quoted dates and tolerates a datetime suffix.
1910
1965
  meta = sdlc_core.load_frontmatter(
1911
1966
  sdlc_core.read_text(p).splitlines()) or {}
1912
- m = re.match(r"(\d{4}-\d{2}-\d{2})",
1913
- kb_unquote(meta.get("date")))
1914
- if m:
1915
- stamp = _dt.date.fromisoformat(m.group(1))
1967
+ day = frontmatter_date(meta)
1968
+ if day:
1969
+ stamp = _dt.date.fromisoformat(day)
1916
1970
  except Exception:
1917
1971
  pass
1918
1972
  if stamp is None: