@antoneeo/kb-agentic-skill 1.4.7 → 1.5.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,146 @@
2
2
 
3
3
  Every significant change to this skill is recorded here.
4
4
 
5
+ ## [1.5.0] - 2026-08-25
6
+
7
+ F-036 — the orientation hook installs itself.
8
+
9
+ ### Fixed
10
+ - **`ENFORCEMENT.md` 4 said "wire it on every project" and nothing ever did.** The
11
+ SessionStart orientation hook was real, tested and fail-open, but installing it was a
12
+ manual step, so it was skipped — and a session that never enters Phase 1 explicitly
13
+ then never meets the guide router at all. Field result: an agent worked a governed
14
+ project without invoking the process. `init` now wires the hook itself.
15
+ - **The invariant that looked like it guarded this guarded only prose** — it asserted
16
+ the documentation section exists (`assertIn("## 4. SessionStart hook", t)`) and that
17
+ `orient` runs. It gains a companion asserting the shipped installer actually CALLS the
18
+ writer. Mutation-tested: the first draft asserted the bare symbol and passed on a file
19
+ that imports the writer without invoking it, which is exactly the disabled-installer
20
+ case it exists to catch.
21
+
22
+ ### Fixed (also)
23
+ - **ENFORCEMENT 4/2's own worked hook examples named a skill directory that two of
24
+ the three lenses do not install.** kb and mkt both printed
25
+ `.claude/skills/agentic-sdlc/scripts/...`; kb installs under `kb-agentic` and mkt
26
+ under `mkt-agentic-sdlc`, so anyone who copied the snippet got a hook that runs,
27
+ prints `can't open file`, and emits nothing — the wired-and-dead defect this release
28
+ detects, sitting in the instructions that produce it. Four paths corrected, and a new
29
+ invariant derives the expected directory and validator from what the distribution
30
+ actually ships and fails if an example names anything else. Mutation-tested.
31
+
32
+ ### Added
33
+ - **A portability rule the doctrine did not have.** The hook command names a validator,
34
+ and where that validator lives decides which settings file may carry it: a repo that
35
+ vendors the validator gets a repo-relative command in the shared, committed
36
+ `.claude/settings.json`; a normal project gets an absolute path, which goes to the
37
+ git-ignored `.claude/settings.local.json` instead — committing it would hand every
38
+ teammate a hook naming a directory they do not have. `init` picks the file and adds
39
+ the local one to `.gitignore`. ENFORCEMENT 4's own worked example showed an absolute
40
+ path and did not mention the distinction; it now does.
41
+ - **A wired-but-dead hook is detected and reported instead of counted as done.** Found
42
+ in the field: a repository whose hook named a sibling lens's path, so it ran every
43
+ session, printed `can't open file`, and emitted nothing. A bare "is a hook present?"
44
+ check answers "already wired" to that and makes the silence permanent, so the existing
45
+ command's validator path is checked on disk. Never rewritten — it may be hand-tuned —
46
+ but never passed off as working either.
47
+
48
+ ### Notes
49
+ - `init` declines rather than guessing: no Python, skill not installed, a settings file
50
+ that is not valid JSON (never rewritten — a merge would discard what is in it), or a
51
+ skill path containing a double quote. Each case prints the snippet to paste.
52
+ - Only Claude Code's hook shape is wired. Codex and Gemini keep the manual snippet: this
53
+ repository has no fixture pinning their schema, and writing a hook file in a shape
54
+ nobody has verified is how the wired-but-dead defect above was born.
55
+ - A blocking `PreToolUse` gate would be real enforcement rather than a nudge, and it is
56
+ refused by the Vision's no-ceremony-ratchet Non-Goal. Recorded in the analysis so the
57
+ next person to have the idea finds the ruling instead of re-deriving it.
58
+
59
+ ## [1.4.8] - 2026-08-25
60
+
61
+ F-035 — second field report from the F-029 practitioner. Three defects in the corpus
62
+ letter's own enforcement: the promise is that every provenance is a real file, and two
63
+ of the three mechanisms that should hold it did not run.
64
+
65
+ ### Fixed
66
+ - **`prov:` below `GIVEN` was structurally impossible for any claim citing
67
+ `corpus/given/`.** `_note_frontmatter` read frontmatter from the cited file itself. A
68
+ corpus artifact is bytes and carries none — but it *is* a file, so the helper returned
69
+ `{}` rather than `None`, which the caller cannot tell apart from "resolved, field
70
+ absent", and the required-field error fired unconditionally. `DERIVED`, `RULING` and
71
+ `IMPORTED` were unreachable on any `given/` artifact, so every such claim was forced
72
+ to `GIVEN` whatever its real extraction chain. The helper now reads the artifact's
73
+ `.meta.md` **sidecar** when one exists and the cited file otherwise — sidecar-first,
74
+ not "non-`.md`", so a verbatim `.md` source stored in `given/` resolves the same way
75
+ as a `.txt` extraction while a `corpus/notes/*.md` note still resolves to its own
76
+ frontmatter. It also returns the path it actually read, and the three findings now
77
+ name that file instead of saying "note" about a sidecar.
78
+ - **`original_path:` was never verified.** Zero reads of it in either script: a folder
79
+ of originals could be moved and sixteen sidecars go dangling behind a green run. It is
80
+ now checked for resolution — a **warning**, never an error, because a bundle carries
81
+ artifacts and sidecars and never the originals, so after an import it dangles
82
+ legitimately. Absolute paths are tested as written; a relative one is tried against
83
+ the docs root's parent and against the docs root, and warns only if neither resolves.
84
+ `original_sha256` stays unverified for the reason already stated — we do not hold the
85
+ bytes — and `distillation.md` now states the two limits separately, because they were
86
+ never in the same position.
87
+ - **The duplicate-id error named the wrong defect.** "uniqueness is global across
88
+ topics/" describes a copied row; the reporter's two rows were hand-written and
89
+ different, and had collided in `kb_claim_id`, which hashes `path#locator#qty` and
90
+ excludes the text on purpose. The check now branches on the **claim text** — same text
91
+ is a copied row, different text is the collision — and the collision message explains
92
+ why the id cannot separate them and prescribes the two legitimate repairs (widen a
93
+ locator, or merge the rows), explicitly ruling out editing the qty to break the tie.
94
+ The id function itself is unchanged and its two constants are now pinned by tests:
95
+ `portability.md`'s cross-project de-duplication rests on them
96
+ (`ADR_2026-08-25_claim_id_collision.md`).
97
+
98
+ ### Added
99
+ - **A `GIVEN` row whose artifact declares a weaker chain now warns.** `provenance:` was
100
+ written into every sidecar and read by nothing; this is its first consumer. A
101
+ conformant corpus (`provenance: GIVEN`, or no field) is silent. **Scope, stated so it
102
+ is not mistaken for more:** it reads the row's first source and it reads the *field* —
103
+ a sidecar declaring `provenance: GIVEN` while its prose says "transcribed from a
104
+ photograph" stays silent, because prose is not machine-readable. The pair's larger
105
+ half is the fix above, which is what makes the honest declaration possible at all.
106
+
107
+ ### Fixed (second review round)
108
+ - **`original_path` classification was wrong on Windows, and the documented example was
109
+ the trigger.** `Path.is_absolute()` is False for a rooted-but-driveless path — exactly
110
+ the `/vault/manuals/xyz.pdf` form these templates print — so it was joined under the
111
+ docs root and silently rewritten onto the docs root's *drive*. That produced a warning
112
+ quoting a path nobody wrote (twice, since both candidates collapsed to it), could hide
113
+ a genuinely dangling pointer behind whatever happened to sit there, and warned falsely
114
+ whenever the corpus and the original lived on different drives. Rootedness is now
115
+ decided by `ntpath`/`posixpath`, the candidate list is de-duplicated, and the
116
+ backslash normalisation is an *additional* candidate rather than a rewrite (a
117
+ backslash is a legal character in a POSIX filename).
118
+ - **The pointer probe could crash the run.** `Path.exists()` re-raises `PermissionError`
119
+ and `ENAMETOOLONG`; `original_path` points outside the docs root by design, so the
120
+ validator must survive whatever is out there. It is now `is_file()` (a directory is not
121
+ a document) inside an `OSError` guard.
122
+ - **`_note_frontmatter` could read one file outside the docs root.** A source cell of
123
+ the form `#L1-2` — no path before the locator — made `confine_under` return the docs
124
+ root itself, and the sidecar name was then built from `base.parent`, i.e.
125
+ `<docs-root>.meta.md`. Both that case and a sidecar orphaned by a deleted artifact now
126
+ return "unresolved", which the source loop already reports.
127
+ - **The collision message asserted something it had not checked.** It claimed the two
128
+ rows "cite the same span with the same qty" — true only for a *computed* id. Two rows
129
+ sharing a hand-typed or stale id while citing different spans now get their own
130
+ message, naming the real repair (`claim-id --fill`).
131
+ - Frontmatter resolution is memoized per cited path: it now runs for every row, `GIVEN`
132
+ included, and a ledger citing one artifact from eighty rows was re-reading its sidecar
133
+ eighty times.
134
+ - The worked sidecar example carried trailing `# comment`s. The frontmatter reader is a
135
+ line regex and does not strip them, so the comment landed inside the value — harmless
136
+ while nothing read `original_path`, and a guaranteed false warning once something did.
137
+
138
+ ### Known limits, stated
139
+ - `ELICITED` is accepted with **no required field at all** — it is named in the
140
+ provenance branch but has no check below it. Found while fixing the above, deliberately
141
+ not repaired here: adding one is a new gate, not a repair.
142
+ - `corpus` and `graph` still have no `--errors-only`; on a corpus with many in-progress
143
+ artifacts the coverage warnings bury a lone error. Ranked last by the reporter.
144
+
5
145
  ## [1.4.7] - 2026-08-06
6
146
 
7
147
  ### Fixed
package/README.md CHANGED
@@ -8,7 +8,7 @@ The sibling of [`@antoneeo/agentic-sdlc-skill`](https://www.npmjs.com/package/@a
8
8
 
9
9
  Two axes are kept apart on purpose — **abstraction** (a topic made of topics: the graph edges) and **certainty** (how strongly the corpus supports a statement: the claim rows).
10
10
 
11
- 1. **Intake — the source becomes untouchable.** Every document enters verbatim under `corpus/given/`, content-addressed by a raw-byte digest. A new version is appended with `supersedes:`, never overwritten. Non-text files get a **stored canonical extraction** (`.txt`, registered extractor) — those are the bytes locators point at, so an id stays valid across re-ingestion. On a large binary corpus the extraction **is** the artifact: the digest moves onto it (immutability still enforced, on the bytes that actually matter) and the original stays where it lives, recorded as `original_path:` + `original_sha256:`. 233 MB of manuals do not enter your repository to protect bytes no locator addresses — and the two recorded fields are honest about their limit: they let a human re-verify, they check nothing on their own. Spoken input is a note with `origin: elicited`; a synthesis carries `derived_from:`; your decision carries `basis:`. A note with none of the three is model knowledge dressed as a source, and the validator rejects it.
11
+ 1. **Intake — the source becomes untouchable.** Every document enters verbatim under `corpus/given/`, content-addressed by a raw-byte digest. A new version is appended with `supersedes:`, never overwritten. Non-text files get a **stored canonical extraction** (`.txt`, registered extractor) — those are the bytes locators point at, so an id stays valid across re-ingestion. On a large binary corpus the extraction **is** the artifact: the digest moves onto it (immutability still enforced, on the bytes that actually matter) and the original stays where it lives, recorded as `original_path:` + `original_sha256:`. 233 MB of manuals do not enter your repository to protect bytes no locator addresses — and the two recorded fields are honest about their limits, which differ: `original_sha256` checks nothing on its own (we do not hold the bytes), while `original_path` is verified to still resolve, and warns when it does not — a folder you reorganised should not leave sixteen sidecars pointing at nothing behind a green run. Spoken input is a note with `origin: elicited`; a synthesis carries `derived_from:`; your decision carries `basis:`. A note with none of the three is model knowledge dressed as a source, and the validator rejects it.
12
12
 
13
13
  2. **Extraction — the unit is the claim.** Rows of `id | claim | valid | qty | about | source | prov | state`. The **id hashes the location and the quantity, never the text**, so an LLM rephrasing mints no new identity. The locator (`p=17@412-509`) is verified: the validator opens the extraction and checks the span exists — and `anchor <path> <phrase>` produces it for you, matching spaces as `\s+` because a PDF extraction breaks phrases mid-line. **Gates are extracted alongside powers**: for every row saying what something can do, the source is asked what must hold first — default-off, licence tier, version floor, dependency — because "yes, supported" without the gate is a plan that fails on site. The rule is *ask*, never *produce*: a source that states no gate yields no row. Validity scopes are half-open ("until March" and "from March" do not conflict). Quantities are typed — mixed kinds or currencies **refuse to sum**. What the source does not assert becomes a `gaps:` line, never a claim.
14
14
 
@@ -1,6 +1,6 @@
1
- {
2
- "name": "kb-agentic-skill",
3
- "version": "1.4.7",
4
- "description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
- "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
- }
1
+ {
2
+ "name": "kb-agentic-skill",
3
+ "version": "1.5.0",
4
+ "description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
+ "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
+ }
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "@antoneeo/kb-agentic-skill",
3
- "version": "1.4.7",
4
- "description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
- "keywords": [
6
- "claude-code",
7
- "claude-skill",
8
- "gemini-cli",
9
- "skill",
10
- "knowledge-base",
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
- "kb-agentic-init": "scripts/init.js",
21
- "kb-agentic-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/kb-agentic-skill/SKILL.md",
29
- "skills/kb-agentic-skill/templates.md",
30
- "skills/kb-agentic-skill/taxonomy.md",
31
- "skills/kb-agentic-skill/guides.md",
32
- "skills/kb-agentic-skill/vision.md",
33
- "skills/kb-agentic-skill/distillation.md",
34
- "skills/kb-agentic-skill/reconciliation.md",
35
- "skills/kb-agentic-skill/elicitation.md",
36
- "skills/kb-agentic-skill/review.md",
37
- "skills/kb-agentic-skill/dispatch.md",
38
- "skills/kb-agentic-skill/routing.md",
39
- "skills/kb-agentic-skill/portability.md",
40
- "skills/kb-agentic-skill/ENFORCEMENT.md",
41
- "skills/kb-agentic-skill/scripts/sdlc_check.py",
42
- "skills/kb-agentic-skill/scripts/sdlc_core.py",
43
- "gemini-extension.json",
44
- "README.md",
45
- "CHANGELOG.md",
46
- "scripts/lib.js",
47
- "scripts/init.js",
48
- "scripts/postinstall.js",
49
- "scripts/preuninstall.js"
50
- ]
51
- }
1
+ {
2
+ "name": "@antoneeo/kb-agentic-skill",
3
+ "version": "1.5.0",
4
+ "description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-skill",
8
+ "gemini-cli",
9
+ "skill",
10
+ "knowledge-base",
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
+ "kb-agentic-init": "scripts/init.js",
21
+ "kb-agentic-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/kb-agentic-skill/SKILL.md",
29
+ "skills/kb-agentic-skill/templates.md",
30
+ "skills/kb-agentic-skill/taxonomy.md",
31
+ "skills/kb-agentic-skill/guides.md",
32
+ "skills/kb-agentic-skill/vision.md",
33
+ "skills/kb-agentic-skill/distillation.md",
34
+ "skills/kb-agentic-skill/reconciliation.md",
35
+ "skills/kb-agentic-skill/elicitation.md",
36
+ "skills/kb-agentic-skill/review.md",
37
+ "skills/kb-agentic-skill/dispatch.md",
38
+ "skills/kb-agentic-skill/routing.md",
39
+ "skills/kb-agentic-skill/portability.md",
40
+ "skills/kb-agentic-skill/ENFORCEMENT.md",
41
+ "skills/kb-agentic-skill/scripts/sdlc_check.py",
42
+ "skills/kb-agentic-skill/scripts/sdlc_core.py",
43
+ "gemini-extension.json",
44
+ "README.md",
45
+ "CHANGELOG.md",
46
+ "scripts/lib.js",
47
+ "scripts/init.js",
48
+ "scripts/postinstall.js",
49
+ "scripts/preuninstall.js"
50
+ ]
51
+ }
package/scripts/init.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { execSync } = require('child_process');
6
- const { SKILL_SOURCE, INSTALLED_SKILL_NAME, SELF_LENS, SIBLING_LENSES, CLIENTS, clientDetected, skillTarget, loadTemplates, templateFor } = require('./lib');
6
+ const { SKILL_SOURCE, INSTALLED_SKILL_NAME, SELF_LENS, SIBLING_LENSES, CLIENTS, clientDetected, skillTarget, wireOrientHook, loadTemplates, templateFor } = require('./lib');
7
7
 
8
8
  const cwd = process.cwd();
9
9
 
@@ -149,8 +149,22 @@ const protocolFiles = {
149
149
  // written aside and merged by hand.
150
150
  let protocolPreexisting = false;
151
151
 
152
- for (const client of CLIENTS) {
153
- if (clientDetected(client)) {
152
+ // Probed ONCE and reused by section 6c: re-filtering CLIENTS there would
153
+ // run every client's detection a second time.
154
+ const detectedClients = CLIENTS.filter(clientDetected);
155
+ const PYTHON_CANDIDATES = ['python', 'python3', 'py'];
156
+ const detectedPython = (() => {
157
+ for (const py of PYTHON_CANDIDATES) {
158
+ try {
159
+ execSync(`${py} --version`, { stdio: 'ignore' });
160
+ return py;
161
+ } catch (e) { /* try the next interpreter */ }
162
+ }
163
+ return null;
164
+ })();
165
+
166
+ for (const client of detectedClients) {
167
+ {
154
168
  console.log(`✅ ${client.label} detected.`);
155
169
  const created = writeIfNotExists(protocolFiles[client.key], protocolContent, `${client.label} protocol pointer`);
156
170
  if (!created) protocolPreexisting = true;
@@ -214,15 +228,118 @@ Delete it once the merge is done.
214
228
  }
215
229
  }
216
230
 
231
+
232
+ // 6c. SessionStart orientation hook (F-036).
233
+ // ENFORCEMENT.md §4 asks for this on every project with a docs root and Python.
234
+ // It was a manual step until now, so it was skipped -- and a session that never
235
+ // enters Phase 1 explicitly then never meets the guide router at all.
236
+
237
+ // devPNT projects want `--hybrid`: without it the hook reports audit-plan
238
+ // staleness that devPNT/KL owns, and noise at every session start is how a
239
+ // session-start message stops being read.
240
+ const hybridProject = fs.existsSync(path.join(cwd, '.devpnt'));
241
+
242
+ // Only touch .gitignore when the wiring landed in the machine-specific file:
243
+ // if that file is not ignored, the absolute path reaches teammates anyway and
244
+ // the whole reason for choosing it is undone. Append-only and marker-guarded,
245
+ // exactly like gitattributes() above. Wrapped, because the installer has already
246
+ // written the seed files and must not die here.
247
+ const ignoreLocalSettings = () => {
248
+ const marker = '.claude/settings.local.json';
249
+ if (!fs.existsSync(path.join(cwd, '.git'))) return;
250
+ const filePath = path.join(cwd, '.gitignore');
251
+ try {
252
+ const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
253
+ // Line-wise, not substring: a `!.claude/settings.local.json` un-ignore would
254
+ // satisfy `includes()` and leave the machine-specific path committed.
255
+ if (current.split(/\r?\n/).some((l) => l.trim() === marker)) return;
256
+ fs.writeFileSync(filePath, current + [
257
+ '',
258
+ '# agentic-sdlc: the orientation hook here names a path on THIS machine.',
259
+ marker,
260
+ '',
261
+ ].join(EOL), 'utf8');
262
+ console.log(`OK .gitignore: ${marker} ignored (it names a machine-specific path).`);
263
+ console.log(' If it is already tracked, run: git rm --cached ' + marker);
264
+ } catch (e) {
265
+ console.log(`⚠️ Could not update .gitignore (${e.message}). Add "${marker}" by hand.`);
266
+ }
267
+ };
268
+
269
+ const claudeClient = detectedClients.find((c) => c.key === 'claude');
270
+ const otherClients = detectedClients.filter((c) => c.key !== 'claude');
271
+ if (!claudeClient) {
272
+ console.log('ℹ️ Claude Code not detected: SessionStart orientation hook not wired.');
273
+ } else {
274
+ const r = wireOrientHook({
275
+ cwd, client: claudeClient, python: detectedPython,
276
+ hybrid: hybridProject, docsLabel: 'ai_docs',
277
+ });
278
+ switch (r.code) {
279
+ case 'wired':
280
+ console.log(`🪝 Wired the SessionStart orientation hook in .claude/${r.file}.`);
281
+ if (r.local) {
282
+ console.log(' It names a path on THIS machine, so it went to the git-ignored');
283
+ console.log(' file: each teammate runs init once to get their own.');
284
+ ignoreLocalSettings();
285
+ }
286
+ break;
287
+ case 'already':
288
+ console.log(`⏭️ SessionStart orientation hook already wired (.claude/${r.file}).`);
289
+ if (r.file === 'settings.local.json') ignoreLocalSettings();
290
+ break;
291
+ case 'broken':
292
+ console.log(`⚠️ The SessionStart orientation hook in .claude/${r.file} is BROKEN —`);
293
+ console.log(' its validator does not resolve, so it has been emitting nothing:');
294
+ console.log(` ${r.existing}`);
295
+ console.log(' Not overwritten (it may be hand-tuned). Correct it to:');
296
+ console.log(` ${r.command}`);
297
+ break;
298
+ case 'unverifiable':
299
+ console.log(`ℹ️ A SessionStart orientation hook exists in .claude/${r.file} but names`);
300
+ console.log(' no validator this installer recognises, so it was left alone:');
301
+ console.log(` ${r.existing}`);
302
+ break;
303
+ case 'malformed':
304
+ console.log(`⚠️ .claude/${r.file} has a shape this installer will not rewrite`);
305
+ console.log(` (${r.why}): left untouched. Add the hook by hand — command:`);
306
+ console.log(` ${JSON.stringify(r.command)}`);
307
+ break;
308
+ case 'write-failed':
309
+ console.log(`⚠️ Could not write .claude/${r.file} (${r.error}). Add by hand:`);
310
+ console.log(` ${JSON.stringify(r.command)}`);
311
+ break;
312
+ case 'no-python':
313
+ console.log('ℹ️ Python not found: SessionStart orientation hook not wired.');
314
+ break;
315
+ case 'no-validator':
316
+ console.log('ℹ️ Skill not installed yet: orientation hook not wired. Install it');
317
+ console.log(' (below), then re-run init.');
318
+ break;
319
+ case 'unsafe-path':
320
+ console.log('⚠️ The validator path contains a character that cannot be placed in a');
321
+ console.log(` hook command safely: ${r.validator}`);
322
+ console.log(' Refusing to build one. Wire it by hand (ENFORCEMENT.md §4).');
323
+ break;
324
+ }
325
+ }
326
+ for (const c of otherClients) {
327
+ // Said out loud rather than skipped in silence: ENFORCEMENT.md §4's manual
328
+ // snippet is these clients' only route, and a silent skip is what let the
329
+ // "documented default nobody installs" defect live in the first place.
330
+ console.log(`ℹ️ ${c.label}: hook not wired (only Claude Code's shape is verified) —`);
331
+ console.log(' wire it by hand from ENFORCEMENT.md §4 if that client supports it.');
332
+ }
333
+
217
334
  // 7. Generate ai_docs/INDEX.md
218
335
  const validator = path.join(SKILL_SOURCE, 'scripts', 'sdlc_check.py');
219
336
  let indexed = false;
220
- for (const py of ['python', 'python3', 'py']) {
337
+ // detectedPython is probed once, above: one answer for the hook and for this.
338
+ if (detectedPython) {
221
339
  try {
222
- execSync(`${py} "${validator}" index --root "${cwd}"`, { stdio: 'ignore' });
340
+ execSync(`${detectedPython} "${validator}" index --root "${cwd}"`, { stdio: 'ignore' });
223
341
  console.log('📇 Generated ai_docs/INDEX.md (document manifest).');
224
342
  indexed = true;
225
- break;
226
343
  } catch (e) { /* try next */ }
227
344
  }
228
345
  if (!indexed) {