@antoneeo/agentic-sdlc-skill 1.43.1 → 1.48.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,65 @@
2
2
 
3
3
  Tutte le modifiche significative a questa skill saranno documentate in questo file.
4
4
 
5
+ ## [1.48.0] - 2026-10-23
6
+
7
+ ### Changed
8
+ - `guides.md`: a document snapshot is verbatim except for credentials and personal or confidential data the guide does not need, which become `[REDACTED: <kind>]` before the snapshot is written; the guide lists the kinds in `redacted_at_intake:`.
9
+ - The session-start orientation (`orient`) now fits under Claude Code's 10,000-character hook limit, even in a project with many documents. Above that limit Claude Code saves the output to a file and shows the agent only the first 2,000 characters, so the guide router never reached the agent. The output is now at most 9,500 characters, including the knowledge layer's lines, and in this order: the triage line, the guide router (one line per guide), the workstream registry (one line per workstream, then the project-wide notes), then the reading guide (README). The manifest (INDEX) and the full files are listed as pointers. A section that does not fit is cut at a line, with a pointer to its file.
10
+ - The per-turn `remind` line also tells the agent, above L1 and when sibling lenses are installed, to choose the lens with `routing.md` and declare it. The line's limit rises from 500 to 600 characters (`vision/rulings.md` r32).
11
+ - `docs_site.py` adds `.docs-site/` to the project's `.gitignore` on its first run in a git repository, so the site's copy of the documents is not committed. It used to print a note only. An entry already in the file is never changed; when git still does not ignore the folder, the note is printed as before.
12
+ - `SKILL.md` names the docs site in the words users use ("docs site", "minisito", "documentation site", "project site"), so a request for it runs `docs_site.py` instead of building a page by hand.
13
+
14
+ ### Added
15
+ - `check` refuses, in every lens, a line shaped like a credential (a password, token or key with a real value, a private-key header, a password in a URL, a bearer token) in a `corpus/given/` file or in the snapshot of a `source_kind: document` guide. The message names the file, the lines and the kinds, never the value. A human clears a false positive with `credential_scan: reviewed — <reason>` (in the sidecar, or in the guide). A project whose stored sources already hold such a line fails `check` until each is redacted or reviewed (`vision/rulings.md` r35a).
16
+ - `adopt`: a dry run listing the document guides that rest on `.sources/` snapshots and what moving each into `corpus/given/` involves. It writes nothing.
17
+ - `check` warns when the same text is stored both as a guide snapshot and as a corpus artifact.
18
+ - `check` also warns on a `corpus/given/` file with no sidecar, accepts a withdrawn artifact's tombstone sidecar (no bytes, no digest), and, when an `original_path:` is a URI, names `original_ref:` for non-local identifiers. A password inside a URL in that warning is masked.
19
+ - Shared claim checks (`check`): a claim's `about` cell may hold several relations separated by `; `, and one reserved predicate, `not-yet-honored-by -> ANALYSIS_<name>`, marks a specification claim the code does not yet honor. `check` refuses the marker when that analysis is missing from `solutions/` or cites neither the claim id nor the id of a row the claim succeeds. `export` drops the markers and says so in one line. Nothing lists or counts claims by the marker (`vision/rulings.md` r33). `memory.md`: a marked claim is cited as not yet true of the code, naming its analysis.
20
+ - Shared claim checks (`check`): the `not-yet-honored-by` marker now works in Hybrid projects (devPNT present). It may name `SHADOW_<doc_key>`; `check` reads the newest `solutions/SHADOW_<doc_key>_vX.Y.md` (versions compared number by number), which must exist and cite the claim id or a predecessor's. A missing shadow is an error that says to export it, and names a file of that key whose name lacks the `_vX.Y` suffix. `check` behaves the same with or without `--hybrid`; export drops both target forms (`vision/rulings.md` r36). New shared helper `sdlc_core.newest_shadow`. `memory.md`: a marked claim is cited naming its target. `SKILL.md` Write Triggers: the marker row no longer says "(Standalone)"; in Hybrid the claim id goes in the E-ISP/E-TDD and its exported shadow.
21
+ - Write Triggers: a Standalone L3 that defers fixing code which contradicts a `topics/` claim marks the claim and cites its id in its ANALYSIS. The L2/L3 change that makes the code honor the claim removes the marker; L1 is exempt.
22
+ - `verdict --hook`, wired by default on Claude Code: an Edit, Write, MultiEdit or NotebookEdit is refused, with one line saying how to unblock, when the session declared no guide-router verdict (or `Level: L1`), or never read a guide its verdict names. Silent in projects without guides, for subagents and whenever it cannot tell. Remove the hook entry to opt out (`ENFORCEMENT.md` §3, `vision/rulings.md` r34).
23
+ - `orient` adds a note when one client runs it from two settings entries, which means each session gets the orientation twice. Claude Code and Codex are counted separately, an identical command counts once, and an entry whose validator is gone is not counted.
24
+ - `orient` adds a line naming the sibling lenses installed beside the skill or in the user's Claude skills directory, and says to choose the lens with `routing.md`. The family list is read from `routing.md` §0, as the installer does.
25
+ - `routing.md` §2 gains worked verdicts for four mixed requests: an interface spec written for another team, the requirements another team must build, a guide whose source spec changed, and code that diverges from a supplied spec.
26
+ - The L3 ANALYSIS template gains a `## Guide check` section: two lines answering the Phase-3 question, whether a CURRENT guide covers what the change touches, and whether understanding it forced a model of a complex component that no guide covers. `validate` adds an advisory when an L3 analysis started on or after 2026-10-24 lacks the section or leaves it unfilled; older analyses are not checked.
27
+
28
+ ### Fixed
29
+ - Uninstalling now also removes the per-turn `remind` hook entry. Left behind, it pointed at a deleted file and Python exited 2, which blocks the prompt.
30
+
31
+ ## [1.47.0] - 2026-10-19
32
+
33
+ Includes everything listed under [1.46.0], which was tagged but never published to npm.
34
+
35
+ ### Added
36
+ - `mark <paths> --note "<what was done>"` writes a dated entry `**YYYY-MM-DD — <what was done>.**` at the top of `audit/project_notes.md`. A release or work outside an open unit used to leave no dated event, because the date had to be copied there by hand. Without `--note`, and with no open unit, `mark` says so.
37
+ - `validate`: an advisory for a dated entry in `project_notes.md` whose bold does not pair; the chronology then showed the label running into the body.
38
+ - Docs site chronology: a Diary entry's label carries its unit id ("F-074 — closed"), so it reads on its own.
39
+ - `stale` no longer counts files inside the docs root (`ai_docs/`, `mkt_docs/`) as drift of any area, and `mark` refuses to record it: `validate` already checks the docs root, and every document edit used to leave `check` NOT CLEAN until a redundant re-mark.
40
+
41
+ ## [1.46.0] - 2026-10-19
42
+
43
+ ### Added
44
+ - Docs site: `docs_site.py --zip <file>` also writes the site as one .zip to share; unzipped, `index.html` opens on any system. A zip made by hand with Windows PowerShell 5.1 `Compress-Archive` stores `data\core.js`, and on Linux or macOS the page then finds no data; `--zip` always writes `/` names. It refuses a target inside the docs root or the site folder. The `SKILL.md` line names it.
45
+
46
+ ## [1.45.0] - 2026-10-19
47
+
48
+ Includes everything listed under [1.44.0], which was tagged but never published to npm.
49
+
50
+ ### Added
51
+ - Docs site: each card in Open work and each feature in Features links its merge review and its end-to-end test script, when they exist. They were reachable only inside a feature's chain page; who merges looks in Open work while the work is open, who tests looks in Features after it closed.
52
+
53
+ ### Changed
54
+ - Docs site: Features are listed newest first by their declared start date; those without a start date come last.
55
+
56
+ ## [1.44.0] - 2026-10-18
57
+
58
+ ### Changed
59
+ - F-071: a review weighs effort against what the owner asked for. In a design review, each outcome the owner named (the opening request, any entry widening scope, an approved north star) is a row of the conformance statement, saying where the design serves it and what share of the design goes to it; an outcome no section serves, or one served by a minor share while a secondary concern takes the bulk, is a finding. `review.md` §Reviewing.
60
+ - F-071: a review runs where something is left to judge. At level 2 the closure review is due only on a security-sensitive area the lens's Rule Zero names, or at the owner's request; otherwise it does not run and logs no row. After a design or closure PASS, a delta made only of version and release metadata, regenerated indexes and audit-mark records, tool-verified copies of a shared file, and CHANGELOG lines adding no claim is not reviewed: its own check runs and a `NOT DUE` row records it. README or overview prose, an E2E script, a rulings row, doctrine, behaviour, tests and any correction of a review finding are still reviewed. `review.md` §When a review is due.
61
+ - `sdlc_check.py benefit` counts `NOT DUE` rows apart, in no review or findings figure.
62
+ - F-071 (this lens): when the owner's words for an outcome name a comparative effect (an agent understands better, a task gets faster), the probe that would show it is built before code, asks the question the outcome names, and is shown red on the baseline; how the author frames the unit does not exempt it. `SKILL.md` Execute-Before-Specify.
63
+
5
64
  ## [1.43.1] - 2026-10-18
6
65
 
7
66
  ### Fixed
package/README.md CHANGED
@@ -35,7 +35,7 @@ Vendoring requires all three files: the entry point, `sdlc_core.py`, `knowledge.
35
35
  - **Independent review, twice**: the design is reviewed before it is implemented and the diff before the work is declared done — by somebody other than its author, who receives the whole review mandate in a fixed block copied verbatim, never a paraphrase of it. Three rungs of independence (fresh-context subagent > one-shot run > a declared self-pass, legal only when no higher rung is usable: absent, or permission-gated and declined — a gated rung is asked about, never silently skipped, and the log says which), capped at 3 rounds, one log line per review, and a PASS is invalid on "found nothing" — it must state where each constraint is satisfied.
36
36
  - **Question discipline**: a real doubt is asked when it emerges, before its answer is written into any document — grouped, with the pros and cons of each option. A choice whose pros clearly win is taken without asking, and the weighing is written next to it; a fact is settled by evidence or asked, never weighed. A question is legal only when the agent searched first, names the search with its result, and names the decision it unblocks — never a stream of "shall I proceed?".
37
37
  - **Installed support files**: Claude, Codex, Gemini, and Google Antigravity receive the full skill folder, including `templates.md`, `architect.md`, `guides.md`, `vision.md`, `tdd.md`, `debugging.md`, `elicitation.md`, `review.md`, `dispatch.md`, `routing.md`, `hybrid.md`, `ENFORCEMENT.md`, `memory.md`, and the validator's three files, `scripts/sdlc_check.py` + `scripts/sdlc_core.py` + `scripts/knowledge.py` — plus the docs site generator `scripts/docs_site.py` and its page `docs_site/`, and the `LICENSE` and `NOTICE` it ships under.
38
- - **Docs site** (`scripts/docs_site.py --root <project> [--open]`): a static, offline, read-only website of what the agent reads in the project — reading guide, Vision, rules, documents, open work with each next step, analyses and, with a knowledge base, the topic map, each fact with its exact source passage, and the source documents. Written outside `ai_docs/`; nothing is ranked or recommended.
38
+ - **Docs site** (`scripts/docs_site.py --root <project> [--open] [--zip <file>]`): a static, offline, read-only website of what the agent reads in the project — reading guide, Vision, rules, documents, open work with each next step, analyses and, with a knowledge base, the topic map, each fact with its exact source passage, and the source documents. Written to `.docs-site/` (added to `.gitignore` on the first run), outside `ai_docs/`; nothing is ranked or recommended.
39
39
  - **Mechanical checks**: optional validator for document structure, generated feature history, stale audit areas, and protected-path gates — `check`, `validate`, `index`, `stale`, `mark`, `benefit`, `gate`, `plan`, `orient`, `migrate`, `review-map` (code lens: classes the files of a change for the merge review brief). `benefit` reports what the review gates have already caught, beside what the doctrine costs to read — it always exits 0 and carries no verdict, because a measurement that can fail a build becomes a target.
40
40
 
41
41
  ## Installation
@@ -74,8 +74,8 @@ agentic-sdlc-init
74
74
  Run it inside a project to create `ai_docs/`, Vision stubs awaiting your interview, strategic docs, audit plan, and agent protocol files (`AGENTS.md` — also the Antigravity CLI surface, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`).
75
75
 
76
76
  It also **wires the SessionStart orientation hook** into `.claude/`, so an agent opening
77
- the project is handed the reading guide, the generated manifest, the guide router and the
78
- last handoff before it does anything — instead of only when it remembers to look. That
77
+ the project is handed the triage line, the guide router, the workstream registry and the
78
+ reading guide (with pointers to the manifest) before it does anything — instead of only when it remembers to look. That
79
79
  wiring used to be a manual step documented in `ENFORCEMENT.md`; being manual, it was
80
80
  skipped, and a project could run fully governed with an agent that never met the process.
81
81
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-sdlc-skill",
3
- "version": "1.43.1",
3
+ "version": "1.48.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.43.1",
3
+ "version": "1.48.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/lib.js CHANGED
@@ -587,10 +587,23 @@ function wireGlobalOrientHook(options) {
587
587
  // already removed the hook, the line stays and the opt-out survives uninstall,
588
588
  // reinstall, and sibling-lens uninstalls.
589
589
  function removeGlobalOrientHooks(removedRoots) {
590
+ return removeGlobalHookEntries(removedRoots, {
591
+ event: 'SessionStart',
592
+ re: / orient(\s|$)/,
593
+ markerTargets: orientMarkerTargets,
594
+ markerRemove: orientMarkerRemove,
595
+ });
596
+ }
597
+
598
+ // The pruning loop shared by every hook this installer wires (F-076): only the
599
+ // `spec.event` entries whose command matches `spec.re` AND whose validator
600
+ // token resolves INTO a removed root are ours; the marker line is cleared only
601
+ // when an entry was actually removed. Returns the targets touched.
602
+ function removeGlobalHookEntries(removedRoots, spec) {
590
603
  const targets = [];
591
604
  const claude = CLIENTS.find((c) => c.key === 'claude');
592
605
  if (claude) targets.push(path.join(claude.home, 'settings.json'));
593
- for (const t of orientMarkerTargets()) {
606
+ for (const t of spec.markerTargets()) {
594
607
  if (!targets.some((x) => pathsEqual(x, t))) targets.push(t);
595
608
  }
596
609
  const removedFrom = [];
@@ -599,17 +612,17 @@ function removeGlobalOrientHooks(removedRoots) {
599
612
  const state = orientSettingsState(target);
600
613
  if (!state.ok || !fs.existsSync(target)) continue;
601
614
  const hooks = state.settings.hooks;
602
- if (!hooks || !Array.isArray(hooks.SessionStart)) continue;
615
+ if (!hooks || !Array.isArray(hooks[spec.event])) continue;
603
616
  let removedHere = false;
604
617
  const ours = (h) => {
605
618
  const cmd = h && typeof h.command === 'string' ? h.command : '';
606
- if (!(ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && / orient(\s|$)/.test(cmd))) return false;
619
+ if (!(ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && spec.re.test(cmd))) return false;
607
620
  const tok = orientHookValidator(cmd);
608
621
  if (tok === null || !path.isAbsolute(tok)) return false; // cannot attribute: keep
609
622
  return removedRoots.some((root) => pathInside(tok, root));
610
623
  };
611
624
  const kept = [];
612
- for (const group of hooks.SessionStart) {
625
+ for (const group of hooks[spec.event]) {
613
626
  if (!group || typeof group !== 'object') { kept.push(group); continue; }
614
627
  if (Array.isArray(group.hooks)) {
615
628
  const inner = group.hooks.filter((h) => {
@@ -626,13 +639,13 @@ function removeGlobalOrientHooks(removedRoots) {
626
639
  }
627
640
  }
628
641
  if (!removedHere) continue;
629
- state.settings.hooks.SessionStart = kept;
630
- if (!kept.length) delete state.settings.hooks.SessionStart;
642
+ state.settings.hooks[spec.event] = kept;
643
+ if (!kept.length) delete state.settings.hooks[spec.event];
631
644
  if (state.settings.hooks && !Object.keys(state.settings.hooks).length) {
632
645
  delete state.settings.hooks;
633
646
  }
634
647
  fs.writeFileSync(target, JSON.stringify(state.settings, null, 2) + String.fromCharCode(10), 'utf8');
635
- orientMarkerRemove(target);
648
+ spec.markerRemove(target);
636
649
  removedFrom.push(target);
637
650
  } catch (e) { /* fail-open per target: never abort an uninstall */ }
638
651
  }
@@ -647,94 +660,142 @@ function removeGlobalOrientHooks(removedRoots) {
647
660
  // already there -- which is the state of every existing installation.
648
661
  // Owner-accepted per-turn cost: `ai_docs/vision/rulings.md` r19.
649
662
 
650
- const REMIND_HOOK_TIMEOUT = 5;
663
+ // F-076 generalized the pair below to every constant-command hook this
664
+ // installer wires: one spec per hook, the remind names kept as wrappers.
665
+ const REMIND_HOOK = {
666
+ event: 'UserPromptSubmit', sub: 'remind', re: / remind(\s|$)/,
667
+ marker: 'remind-hook-wired', timeout: 5,
668
+ };
669
+ // F-076: the guide-verdict check. `; exit 0` because a validator that cannot
670
+ // run `verdict` -- missing (npm 7+ may never run the uninstaller) or older than
671
+ // it (a downgrade) -- exits 2, and exit 2 on PreToolUse blocks every edit.
672
+ // Probed in Git Bash and PowerShell 5.1. Owner-accepted: rulings.md r34.
673
+ const VERDICT_HOOK = {
674
+ event: 'PreToolUse', sub: 'verdict --hook', suffix: '; exit 0', re: / verdict(\s|$)/,
675
+ marker: 'verdict-hook-wired', timeout: 10, matcher: 'Edit|Write|MultiEdit|NotebookEdit',
676
+ };
651
677
 
678
+ function commandHookCommand(spec, python, validator) {
679
+ return python + ' "' + validator + '" ' + spec.sub + (spec.suffix || '');
680
+ }
652
681
  function remindHookCommand(python, validator) {
653
- return python + ' "' + validator + '" remind';
682
+ return commandHookCommand(REMIND_HOOK, python, validator);
654
683
  }
655
684
 
656
- function findRemindHook(settings) {
657
- const groups = settings && settings.hooks && settings.hooks.UserPromptSubmit;
685
+ function findCommandHook(settings, spec) {
686
+ const groups = settings && settings.hooks && settings.hooks[spec.event];
658
687
  if (!Array.isArray(groups)) return null;
659
688
  for (const group of groups) {
660
689
  if (!group || typeof group !== 'object') continue;
661
690
  const inner = Array.isArray(group.hooks) ? group.hooks : [group];
662
691
  for (const h of inner) {
663
692
  const cmd = h && typeof h.command === 'string' ? h.command : '';
664
- if (ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && / remind(\s|$)/.test(cmd)) {
693
+ if (ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && spec.re.test(cmd)) {
665
694
  return cmd;
666
695
  }
667
696
  }
668
697
  }
669
698
  return null;
670
699
  }
700
+ function findRemindHook(settings) {
701
+ return findCommandHook(settings, REMIND_HOOK);
702
+ }
671
703
 
672
- // Writes the hook into `target` (an already-chosen settings file) unless it is
673
- // there or the user opted out. Returns a code, never throws: an installer must
674
- // not die on someone's settings file.
675
- function remindMarkerPath() {
704
+ // One marker file per hook: removal is a standing opt-out for THAT hook only.
705
+ function markerPathOf(spec) {
676
706
  const root = process.env.AGENTIC_SDLC_KB_ROOT
677
707
  || path.join(os.homedir(), '.agentic-sdlc');
678
- return path.join(root, 'remind-hook-wired');
708
+ return path.join(root, spec.marker);
679
709
  }
680
- function remindMarkerTargets() {
710
+ function markerTargetsOf(spec) {
681
711
  try {
682
- return fs.readFileSync(remindMarkerPath(), 'utf8')
712
+ return fs.readFileSync(markerPathOf(spec), 'utf8')
683
713
  .split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
684
714
  } catch (e) {
685
715
  return [];
686
716
  }
687
717
  }
688
- function remindMarkerHas(target) {
689
- return remindMarkerTargets().some((t) => pathsEqual(t, target));
718
+ function markerHasOf(spec, target) {
719
+ return markerTargetsOf(spec).some((t) => pathsEqual(t, target));
720
+ }
721
+ function markerAddOf(spec, target) {
722
+ try {
723
+ if (markerHasOf(spec, target)) return;
724
+ fs.mkdirSync(path.dirname(markerPathOf(spec)), { recursive: true });
725
+ fs.appendFileSync(markerPathOf(spec), target + String.fromCharCode(10), 'utf8');
726
+ } catch (e) { /* fail-open */ }
690
727
  }
691
- function remindMarkerAdd(target) {
728
+ function markerRemoveOf(spec, target) {
692
729
  try {
693
- if (remindMarkerHas(target)) return;
694
- fs.mkdirSync(path.dirname(remindMarkerPath()), { recursive: true });
695
- fs.appendFileSync(remindMarkerPath(), target + String.fromCharCode(10), 'utf8');
730
+ const rest = markerTargetsOf(spec).filter((t) => !pathsEqual(t, target));
731
+ if (rest.length) {
732
+ fs.writeFileSync(markerPathOf(spec), rest.join(String.fromCharCode(10)) + String.fromCharCode(10), 'utf8');
733
+ } else {
734
+ fs.rmSync(markerPathOf(spec), { force: true });
735
+ }
696
736
  } catch (e) { /* fail-open */ }
697
737
  }
738
+ function remindMarkerHas(target) { return markerHasOf(REMIND_HOOK, target); }
739
+ function remindMarkerAdd(target) { markerAddOf(REMIND_HOOK, target); }
698
740
 
699
- function wireRemindHookInto(target, python, validator) {
741
+ // Writes the hook into `target` (an already-chosen settings file) unless it is
742
+ // there or the user opted out. Returns a code, never throws: an installer must
743
+ // not die on someone's settings file. Order matters: the hook being PRESENT is
744
+ // `already`, and only its ABSENCE plus our marker means the user removed it --
745
+ // a standing opt-out. Marker-first would refuse to re-check a hook someone
746
+ // still has.
747
+ function wireCommandHookInto(spec, target, python, validator) {
700
748
  if (!python) return { code: 'no-python' };
701
749
  if (!validator || ORIENT_UNSAFE_IN_PATH.test(validator)) return { code: 'unsafe-path', validator };
702
750
  const state = orientSettingsState(target);
703
751
  if (!state.ok) return { code: 'malformed', target, why: state.why };
704
- const settings0 = state.settings;
705
- // Order matters, and the first version had it wrong too: the hook being
706
- // PRESENT is `already`, and only its ABSENCE plus our marker means the user
707
- // removed it -- a standing opt-out. Marker-first would refuse to re-check a
708
- // hook someone still has.
709
- const present = findRemindHook(settings0);
752
+ const settings = state.settings;
753
+ const present = findCommandHook(settings, spec);
710
754
  if (present) return { code: 'already', target, command: present };
711
- if (remindMarkerHas(target)) return { code: 'opted-out', target };
712
- const settings = settings0;
713
- const existing = null;
714
- if (existing) return { code: 'already', target, command: existing };
715
- const command = remindHookCommand(python, validator);
755
+ if (markerHasOf(spec, target)) return { code: 'opted-out', target };
756
+ const command = commandHookCommand(spec, python, validator);
716
757
  if (!settings.hooks) settings.hooks = {};
717
- if (!Array.isArray(settings.hooks.UserPromptSubmit)) settings.hooks.UserPromptSubmit = [];
718
- settings.hooks.UserPromptSubmit.push({
719
- hooks: [{ type: 'command', command, timeout: REMIND_HOOK_TIMEOUT }],
720
- });
758
+ if (!Array.isArray(settings.hooks[spec.event])) settings.hooks[spec.event] = [];
759
+ const group = { hooks: [{ type: 'command', command, timeout: spec.timeout }] };
760
+ if (spec.matcher) group.matcher = spec.matcher;
761
+ settings.hooks[spec.event].push(group);
721
762
  try {
722
763
  fs.mkdirSync(path.dirname(target), { recursive: true });
723
764
  fs.writeFileSync(target, JSON.stringify(settings, null, 2) + String.fromCharCode(10), 'utf8');
724
765
  } catch (e) {
725
766
  return { code: 'write-failed', target, command, error: e.message };
726
767
  }
727
- remindMarkerAdd(target);
768
+ markerAddOf(spec, target);
728
769
  return { code: 'wired', target, command };
729
770
  }
771
+ function wireRemindHookInto(target, python, validator) {
772
+ return wireCommandHookInto(REMIND_HOOK, target, python, validator);
773
+ }
730
774
 
731
-
732
- // The machine-global twin of wireGlobalOrientHook, same target and validator.
733
- function wireGlobalRemindHook(options) {
775
+ // The machine-global twins of wireGlobalOrientHook, same target and validator.
776
+ function wireGlobalCommandHook(spec, options) {
734
777
  const client = options.client;
735
778
  const validator = path.join(skillTarget(client), 'scripts', ORIENT_ENTRY_POINT);
736
779
  if (!fs.existsSync(validator)) return { code: 'no-validator', validator };
737
- return wireRemindHookInto(path.join(client.home, 'settings.json'), options.python, validator);
780
+ return wireCommandHookInto(spec, path.join(client.home, 'settings.json'), options.python, validator);
781
+ }
782
+ function wireGlobalRemindHook(options) { return wireGlobalCommandHook(REMIND_HOOK, options); }
783
+ function wireGlobalVerdictHook(options) { return wireGlobalCommandHook(VERDICT_HOOK, options); }
784
+
785
+ // Uninstall twin for the constant-command hooks: one {target, event} per
786
+ // removal, so the uninstaller can say what it removed.
787
+ function removeGlobalCommandHooks(removedRoots) {
788
+ const removed = [];
789
+ for (const spec of [REMIND_HOOK, VERDICT_HOOK]) {
790
+ const targets = removeGlobalHookEntries(removedRoots, {
791
+ event: spec.event,
792
+ re: spec.re,
793
+ markerTargets: () => markerTargetsOf(spec),
794
+ markerRemove: (target) => markerRemoveOf(spec, target),
795
+ });
796
+ for (const target of targets) removed.push({ target, event: spec.event });
797
+ }
798
+ return removed;
738
799
  }
739
800
 
740
801
 
@@ -750,6 +811,8 @@ module.exports = {
750
811
  wireOrientHook,
751
812
  wireRemindHookInto,
752
813
  wireGlobalRemindHook,
814
+ wireGlobalVerdictHook,
815
+ removeGlobalCommandHooks,
753
816
  remindMarkerHas,
754
817
  remindMarkerAdd,
755
818
  remindHookCommand,
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const {
5
5
  SKILL_SOURCE, CLIENTS, clientDetected, skillTarget, copyRecursive,
6
- wireGlobalOrientHook, wireGlobalRemindHook, detectPython,
6
+ wireGlobalOrientHook, wireGlobalRemindHook, wireGlobalVerdictHook, detectPython,
7
7
  } = require('./lib');
8
8
 
9
9
  function installSkill(client) {
@@ -51,6 +51,16 @@ for (const client of CLIENTS) {
51
51
  console.log(` (${rr.target}). One constant line per prompt; remove that hook`);
52
52
  console.log(' entry to opt out — it will not be re-added.');
53
53
  }
54
+ // F-076: the guide-verdict check -- refuses an edit when the session
55
+ // declared no guide-router verdict, or never read a guide it names.
56
+ // Silent on projects without guides; owner-accepted per-edit cost:
57
+ // ai_docs/vision/rulings.md r34.
58
+ const rv = wireGlobalVerdictHook({ client, python: detectPython() });
59
+ if (rv.code === 'wired') {
60
+ console.log('🔗 Guide-verdict check wired for every project on this machine');
61
+ console.log(` (${rv.target}). Edits wait for the Rule-Zero router verdict in projects`);
62
+ console.log(' with guides; remove that hook entry to opt out — it will not be re-added.');
63
+ }
54
64
  const r = wireGlobalOrientHook({ client, python: detectPython() });
55
65
  if (r.code === 'wired') {
56
66
  console.log('🔗 Session orientation wired for every project on this machine');
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
- const { CLIENTS, skillTarget, removeGlobalOrientHooks } = require('./lib');
4
+ const {
5
+ CLIENTS, skillTarget, removeGlobalOrientHooks, removeGlobalCommandHooks,
6
+ } = require('./lib');
5
7
 
6
8
  function removeSkill(client) {
7
9
  const target = skillTarget(client);
@@ -27,4 +29,18 @@ try {
27
29
  }
28
30
  } catch (e) { /* never abort an uninstall for a convenience */ }
29
31
 
32
+ // F-076: the per-turn reminder and the guide-verdict check name the same skill
33
+ // dirs; left behind they exit 2 (Python cannot open the file), which blocks the
34
+ // prompt or the edit. The verdict entry is also wired `; exit 0`, because npm 7+
35
+ // may not run this script at all.
36
+ const COMMAND_HOOK_NAMES = {
37
+ UserPromptSubmit: 'per-turn reminder',
38
+ PreToolUse: 'guide-verdict check',
39
+ };
40
+ try {
41
+ for (const { target, event } of removeGlobalCommandHooks(CLIENTS.map(skillTarget))) {
42
+ console.log(`🧹 Removed the ${COMMAND_HOOK_NAMES[event] || event} hook entry (${target}).`);
43
+ }
44
+ } catch (e) { /* never abort an uninstall for a convenience */ }
45
+
30
46
  CLIENTS.forEach(removeSkill);
@@ -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
 
@@ -59,9 +59,60 @@ Semantics: exit code 2 + message on stderr ⇒ the write is blocked and the mess
59
59
  - The hook assumes the working directory is the project root (standard behavior of Claude Code hooks).
60
60
  - **Hybrid/devPNT projects**: add `--hybrid` to the gate command. Governed designs live in the devPNT DB, so the gate also unlocks when an approved E-TDD shadow (`ai_docs/solutions/SHADOW_*tdd*.md`, exported before implementation — see `hybrid.md` §Shadow discipline) is present. Without the flag the gate would block legitimate governed work. The flag is deliberately explicit: never auto-detected.
61
61
 
62
+ ### Verdict check: `verdict` (PreToolUse -- wired by default)
63
+
64
+ Rule Zero makes the agent declare the level with the guide-router verdict and read the
65
+ guide it names before acting. A field agent edited without doing either and said nothing
66
+ had stopped it (F-076). `sdlc_check.py verdict --hook` reads the session transcript
67
+ Claude Code hands every hook and refuses an Edit, Write, MultiEdit or NotebookEdit when:
68
+
69
+ - the session shows no line of the agent's own text with `router:` (the verdict) and no
70
+ `Level: L1` (or another lens's level 1), or
71
+ - a guide the verdict names, present in `reference/`, never appeared in the input of a
72
+ tool call other than an edit (Read, Bash `cat`, Grep, a subagent brief all count).
73
+
74
+ A verdict whose text after `router:` begins with `no match` or `absent` names no guide.
75
+ The refusal is one line, shown to the agent, saying how to unblock. It is silent — the
76
+ edit proceeds — in a project without guides, for a file outside the project, for a
77
+ subagent's edit (one the client marks with `agent_id`, or one whose transcript is the
78
+ subagent's own), and whenever it cannot tell (no or oversized transcript, unknown format,
79
+ ambiguous docs root, any error). It never approves an edit: compliance leaves the user's
80
+ normal permission flow in place.
81
+
82
+ Cost (owner-accepted, `ai_docs/vision/rulings.md` r34, the "no ceremony ratchet"
83
+ Non-Goal on r19's precedent): ~0.2–0.5 s per edit and zero tokens when compliant; one
84
+ refusal of at most 250 characters and one retry when not. L1 pays nothing beyond writing
85
+ `Level: L1`, which Rule Zero already asks for. Residuals: edits through a subagent and
86
+ writes made with Bash are not checked (a subagent's edit the client does not mark is judged
87
+ on the orchestrator's verdict); a verdict anywhere earlier in the session counts.
88
+
89
+ Installing the package wires it machine-wide on Claude Code; **removing that hook entry
90
+ is a standing opt-out.** The command ends in `; exit 0`: a validator that cannot run
91
+ `verdict` — gone after an uninstall, or older after a downgrade — would exit 2, and exit
92
+ 2 on PreToolUse blocks every edit. To wire it by hand:
93
+
94
+ ```json
95
+ {
96
+ "hooks": {
97
+ "PreToolUse": [
98
+ {
99
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
100
+ "hooks": [
101
+ {
102
+ "type": "command",
103
+ "command": "python \"C:\\Users\\<user>\\.claude\\skills\\agentic-sdlc\\scripts\\sdlc_check.py\" verdict --hook; exit 0",
104
+ "timeout": 10
105
+ }
106
+ ]
107
+ }
108
+ ]
109
+ }
110
+ }
111
+ ```
112
+
62
113
  ## 4. SessionStart hook (orientation, recommended default)
63
114
 
64
- Emits the `ai_docs/` orientation — reading guide (`README.md`), manifest (`INDEX.md`), guide router (`reference/INDEX.md`) and last `handoff.md` — plus the Rule-Zero triage reminder to stdout at session start, so the agent begins already oriented instead of reading them only if it remembers to. It is **fail-open**: a missing, unreadable or oversized doc is skipped, the output is size-capped, and it always exits 0 — a broken or empty `ai_docs/` never blocks the session. It is **zero-execution** (it reads and prints, never runs anything).
115
+ Emits the `ai_docs/` orientation to stdout at session start, in the order an arriving agent needs it: the Rule-Zero triage line, the lens choice when sibling lenses are installed, the guide router (`reference/INDEX.md`, one line per guide), the workstream registry (`audit/handoff.md`, one line per workstream, then the project-wide notes), the reading guide (`README.md`), then pointers to the manifest (`INDEX.md`) and to the full files — so the agent begins already oriented instead of reading them only if it remembers to. The whole output stays under 9,500 characters: Claude Code injects a hook's output up to 10,000 and, above that, only a 2,000-character preview, which is how the router once never reached a session (F-075). A section that does not fit is cut at a line with a pointer to its file, and a note says when one client runs the hook from two settings entries. It is **fail-open**: a missing or unreadable doc is skipped, and it always exits 0 — a broken or empty `ai_docs/` never blocks the session. It is **zero-execution** (it reads and prints, never runs anything).
65
116
 
66
117
  **Wire it on every project that has `ai_docs/` and a Python interpreter.** It was opt-in until v1.16.0 and the field result was the defect this level exists to prevent: the guide router stayed unread unless the user asked for it by hand, so guides were written and never consulted. Prompt-level placement (Rule Zero declares the router verdict; Phase 1 reads the router) carries the process on its own — this hook is the backstop that survives long contexts, compaction and a session that never enters Phase 1 explicitly. Skip it only where Python is unavailable, and know what you are trading.
67
118
 
@@ -155,7 +206,7 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
155
206
  **Usage notes:**
156
207
  - The hook assumes the working directory is the project root (standard Claude Code hook behavior); it also accepts `--root <path>`.
157
208
  - **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.
209
+ - 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
210
 
160
211
  ### Per-turn reminder: `remind` (UserPromptSubmit -- wired by default)
161
212
 
@@ -166,13 +217,13 @@ a release ran against a `GUIDE_release.md` the session never consulted, because
166
217
  ONE constant line at every prompt, and its first duty is that one: decide whether the
167
218
  skill governs the work in front of you and, if it does, load it once.
168
219
 
169
- Cost: 478 characters, ~80 tokens per prompt, paid on every turn including
220
+ Cost: 566 characters, ~95 tokens per prompt, paid on every turn including
170
221
  trivial ones. That is admissible only because the owner accepted it explicitly
171
222
  (`ai_docs/vision/rulings.md` r19, the "no ceremony ratchet" Non-Goal's second door) --
172
223
  and the acceptance was for a FLAT cost. Hence the contract, guarded by
173
224
  `scripts/test_remind.py`: the line is **constant and reads nothing** (so nothing repo-
174
225
  or session-controlled rides into the agent's context through it, and the cost cannot
175
- grow with the session), it is **one line under 500 characters**, and it **can never
226
+ grow with the session), it is **one line under 600 characters** (r32 raised r19's 500 to carry the lens choice), and it **can never
176
227
  break a prompt** (argv ignored whole, always exit 0). A trivial turn pays a yes/no
177
228
  judgement, never a load.
178
229
 
@@ -215,7 +266,7 @@ It aggregates the test files (`test_plan.py` + `test_session_start.py` + `test_s
215
266
 
216
267
  **Optional CI** (same shape as §2, not mandatory): add a `run:` step invoking the `unittest discover` command above.
217
268
 
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.
269
+ **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
270
 
220
271
  ## `benefit` is a report, never a CI gate
221
272