@antoneeo/kb-agentic-skill 1.7.0 → 1.9.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 +42 -0
- package/README.md +11 -2
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/init.js +22 -11
- package/scripts/lib.js +232 -1
- package/scripts/postinstall.js +34 -2
- package/scripts/preuninstall.js +14 -1
- package/skills/kb-agentic-skill/ENFORCEMENT.md +63 -0
- package/skills/kb-agentic-skill/SKILL.md +2 -2
- package/skills/kb-agentic-skill/scripts/sdlc_check.py +26 -0
- package/skills/kb-agentic-skill/scripts/sdlc_core.py +138 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
Every significant change to this skill is recorded here.
|
|
4
4
|
|
|
5
|
+
## [1.9.0] - 2026-08-28
|
|
6
|
+
|
|
7
|
+
F-042 -- the install wires the hook; the incomprehensible question dies.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Installing or updating the package wires the session-orientation hook
|
|
11
|
+
machine-wide** (Claude Code with Python; user-level settings). The npm install
|
|
12
|
+
is the consent -- no question is ever asked. Removal is a standing opt-out: a
|
|
13
|
+
per-target marker remembers every settings file ever wired and no update
|
|
14
|
+
re-adds a removed hook; deleting the marker re-enables. `npm uninstall`
|
|
15
|
+
surgically removes only the entries attributable to the removed skill, across
|
|
16
|
+
every world the marker remembers, so no dead hook is left behind. `init` now
|
|
17
|
+
reports "covered by the machine-global hook" instead of wiring a duplicate,
|
|
18
|
+
and still reports a dead or unverifiable global wrapper while wiring the
|
|
19
|
+
project.
|
|
20
|
+
- **The check note is global-aware and speaks user language.** Detection now
|
|
21
|
+
also reads the user-level settings (`CLAUDE_CONFIG_DIR` honored;
|
|
22
|
+
`AGENTIC_SDLC_USER_SETTINGS` is the test seam), so a fixed machine stops
|
|
23
|
+
being nagged; the two notes are reworded consequence-first ("agent sessions
|
|
24
|
+
start without automatic orientation...") so the agent relaying them can ask a
|
|
25
|
+
question a normal user understands.
|
|
26
|
+
|
|
27
|
+
## [1.8.0] - 2026-08-28
|
|
28
|
+
|
|
29
|
+
F-041 -- the mid-session drift field report, both items.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- **`check` notes an unwired or dead session-orientation hook.** `init` wires the
|
|
33
|
+
hook at init time only; pre-F-036 and agent-bootstrapped projects never got it,
|
|
34
|
+
silently. `check` now prints, right after its summary line, a one-line `[note]` when no hook is detectable
|
|
35
|
+
in `.claude/settings.json` / `.claude/settings.local.json` / `.codex/hooks.json`,
|
|
36
|
+
and a distinct note when a hook is wired but its validator does not resolve
|
|
37
|
+
(any-resolves aggregation across entries; BOM-tolerant reads; fail-open on
|
|
38
|
+
malformed config). Informational only: never the exit code, never a `validate`
|
|
39
|
+
warning -- CI stays green because the wired hook legitimately lives in the
|
|
40
|
+
git-ignored local settings file.
|
|
41
|
+
- **`remind` -- an opt-in per-turn one-line reminder (UserPromptSubmit).** orient
|
|
42
|
+
fires once; mid-session drift had no mechanical guard. One constant, zero-read
|
|
43
|
+
line re-arms the kb minimum (triage levels, the topics/INDEX.md recall trigger,
|
|
44
|
+
the capture moment) at every prompt. Never wired by `init`, never a default:
|
|
45
|
+
ENFORCEMENT par.4 documents the snippet and the ~50 tokens/prompt cost.
|
|
46
|
+
|
|
5
47
|
## [1.7.0] - 2026-08-28
|
|
6
48
|
|
|
7
49
|
F-040 — the capture moment (second-brain unit 2): the day's decisions reach the
|
package/README.md
CHANGED
|
@@ -71,7 +71,12 @@ kb-agentic-init
|
|
|
71
71
|
|
|
72
72
|
Run it inside a project to create `ai_docs/`, Vision documents, strategic docs, audit plan, and agent protocol files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cursorrules`).
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Installing or updating the npm package **wires the session-orientation hook
|
|
75
|
+
machine-wide** (user-level `~/.claude/settings.json`, Claude Code with Python):
|
|
76
|
+
every project with a docs root greets its agent already oriented, and removing
|
|
77
|
+
the entry is a standing opt-out that no update ever overrides. `init` still
|
|
78
|
+
wires per-project where that is the better shape (a vendored validator, a
|
|
79
|
+
shared team file), so an agent opening
|
|
75
80
|
the project is handed the reading guide, the generated manifest, the guide router and the
|
|
76
81
|
last handoff before it does anything — instead of only when it remembers to look. That
|
|
77
82
|
wiring used to be a manual step documented in `ENFORCEMENT.md`; being manual, it was
|
|
@@ -82,7 +87,11 @@ it: a repo that vendors it gets a repo-relative command in the shared `.claude/s
|
|
|
82
87
|
(portable — commit it); otherwise the path is machine-specific and goes to the git-ignored
|
|
83
88
|
`.claude/settings.local.json`, and each teammate runs `init` once. Re-running `init` never
|
|
84
89
|
duplicates the hook, and a hook whose validator no longer resolves is reported as **broken**
|
|
85
|
-
with the correction rather than counted as installed.
|
|
90
|
+
with the correction rather than counted as installed. `check` prints a one-line
|
|
91
|
+
note when a machine has no orientation hook at project or user level (or a dead
|
|
92
|
+
one), and an opt-in per-turn reminder exists for long sessions: `sdlc_check.py
|
|
93
|
+
remind`, one constant line via UserPromptSubmit -- documented in `ENFORCEMENT.md`
|
|
94
|
+
par.4, never wired by default.
|
|
86
95
|
|
|
87
96
|
## The family: three lenses, one spine
|
|
88
97
|
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kb-agentic-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Knowledge-Base & Document-First protocol with risk triage, Vision governance, signal distillation and optional devPNT integration.",
|
|
5
5
|
"author": "Antonio Pinto (https://github.com/Antoneeo)"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/kb-agentic-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Knowledge-Base & Document-First protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, signal distillation and optional devPNT integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/scripts/init.js
CHANGED
|
@@ -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, wireOrientHook, loadTemplates, templateFor } = require('./lib');
|
|
6
|
+
const { SKILL_SOURCE, INSTALLED_SKILL_NAME, SELF_LENS, SIBLING_LENSES, CLIENTS, clientDetected, skillTarget, wireOrientHook, detectPython, loadTemplates, templateFor } = require('./lib');
|
|
7
7
|
|
|
8
8
|
const cwd = process.cwd();
|
|
9
9
|
|
|
@@ -152,16 +152,8 @@ let protocolPreexisting = false;
|
|
|
152
152
|
// Probed ONCE and reused by section 6c: re-filtering CLIENTS there would
|
|
153
153
|
// run every client's detection a second time.
|
|
154
154
|
const detectedClients = CLIENTS.filter(clientDetected);
|
|
155
|
-
|
|
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
|
-
})();
|
|
155
|
+
// One probe, shared with postinstall's global wiring (F-042): lib owns it.
|
|
156
|
+
const detectedPython = detectPython();
|
|
165
157
|
|
|
166
158
|
for (const client of detectedClients) {
|
|
167
159
|
{
|
|
@@ -276,8 +268,21 @@ if (!claudeClient) {
|
|
|
276
268
|
hybrid: hybridProject, docsLabel: 'ai_docs',
|
|
277
269
|
});
|
|
278
270
|
switch (r.code) {
|
|
271
|
+
case 'global':
|
|
272
|
+
// F-042: a resolving machine-global hook already orients every project.
|
|
273
|
+
console.log('⏭️ Covered by the machine-global orientation hook — no project-level');
|
|
274
|
+
console.log(` hook needed (${r.target}).`);
|
|
275
|
+
break;
|
|
279
276
|
case 'wired':
|
|
280
277
|
console.log(`🪝 Wired the SessionStart orientation hook in .claude/${r.file}.`);
|
|
278
|
+
if (r.globalNote === 'dead') {
|
|
279
|
+
console.log('⚠️ Note: a machine-global orientation hook exists but its validator');
|
|
280
|
+
console.log(' does not resolve — it emits nothing. Fix or remove it in your user');
|
|
281
|
+
console.log(' settings (ENFORCEMENT.md §4).');
|
|
282
|
+
} else if (r.globalNote === 'unverifiable') {
|
|
283
|
+
console.log('ℹ️ Note: your user settings carry an orientation hook this installer');
|
|
284
|
+
console.log(' cannot verify; the project-level hook above covers this project.');
|
|
285
|
+
}
|
|
281
286
|
if (r.local) {
|
|
282
287
|
console.log(' It names a path on THIS machine, so it went to the git-ignored');
|
|
283
288
|
console.log(' file: each teammate runs init once to get their own.');
|
|
@@ -321,6 +326,12 @@ if (!claudeClient) {
|
|
|
321
326
|
console.log(` hook command safely: ${r.validator}`);
|
|
322
327
|
console.log(' Refusing to build one. Wire it by hand (ENFORCEMENT.md §4).');
|
|
323
328
|
break;
|
|
329
|
+
default:
|
|
330
|
+
// A lens port missing a case must be loud, never a silent skip — the
|
|
331
|
+
// exact defect class the manual-snippet note below exists to prevent.
|
|
332
|
+
console.log(`ℹ️ Orientation hook: unhandled result '${r.code}'. Wire it by hand`);
|
|
333
|
+
console.log(' per ENFORCEMENT.md §4.');
|
|
334
|
+
break;
|
|
324
335
|
}
|
|
325
336
|
}
|
|
326
337
|
for (const c of otherClients) {
|
package/scripts/lib.js
CHANGED
|
@@ -358,6 +358,31 @@ function wireOrientHook(options) {
|
|
|
358
358
|
: { code: 'broken', file: name, target: p, existing, command };
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
// F-042: a resolving MACHINE-GLOBAL hook (user-level settings) already
|
|
362
|
+
// orients every project. Checked AFTER the project scan on purpose: a
|
|
363
|
+
// project-level broken/malformed/unverifiable must be reported first — a
|
|
364
|
+
// pre-check would mask a dead project hook behind "covered globally", the
|
|
365
|
+
// wired-and-DEAD state F-041 calls the worst of the three. A dead or
|
|
366
|
+
// unverifiable global hook falls through to the project write (the wrapper
|
|
367
|
+
// may be emitting nothing) and the caller gets `globalNote` to print the
|
|
368
|
+
// correction. A user-level token that is not absolute has no cwd to resolve
|
|
369
|
+
// against, so it is treated as unverifiable.
|
|
370
|
+
let globalNote;
|
|
371
|
+
{
|
|
372
|
+
const userTarget = path.join(client.home, 'settings.json');
|
|
373
|
+
const ustate = orientSettingsState(userTarget);
|
|
374
|
+
if (ustate.ok) {
|
|
375
|
+
const uexisting = findOrientHook(ustate.settings);
|
|
376
|
+
if (uexisting) {
|
|
377
|
+
const ufound = orientHookValidator(uexisting);
|
|
378
|
+
if (ufound !== null && path.isAbsolute(ufound) && fs.existsSync(ufound)) {
|
|
379
|
+
return { code: 'global', target: userTarget, command: uexisting };
|
|
380
|
+
}
|
|
381
|
+
globalNote = (ufound === null || !path.isAbsolute(ufound)) ? 'unverifiable' : 'dead';
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
361
386
|
// A machine-specific command must not reach the shared, committed file.
|
|
362
387
|
const file = vendored ? 'settings.json' : 'settings.local.json';
|
|
363
388
|
const target = path.join(cwd, '.claude', file);
|
|
@@ -384,7 +409,209 @@ function wireOrientHook(options) {
|
|
|
384
409
|
} catch (e) {
|
|
385
410
|
return { code: 'write-failed', file, target, command, error: e.message };
|
|
386
411
|
}
|
|
387
|
-
return { code: 'wired', file, target, command, local: !vendored };
|
|
412
|
+
return { code: 'wired', file, target, command, local: !vendored, globalNote };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// --- F-042: the machine-global hook (install-time wiring) -------------------
|
|
416
|
+
// The F-041 field lesson: the check note asked a question a normal user cannot
|
|
417
|
+
// evaluate. The owner's ruling: the hook is wired by the act the user already
|
|
418
|
+
// performs — installing/updating the package — at the USER level, which the
|
|
419
|
+
// installer knows exactly. `orient` is fail-open (no docs root ⇒ silent), so a
|
|
420
|
+
// global hook is safe by construction on unmanaged projects. Removal is a
|
|
421
|
+
// STANDING opt-out: a per-target marker remembers every settings file ever
|
|
422
|
+
// wired, and no install or update ever re-adds a removed hook. Deleting the
|
|
423
|
+
// marker file is the documented re-enable gesture (ENFORCEMENT §4).
|
|
424
|
+
|
|
425
|
+
// Python detection, shared by init (project wiring + index) and postinstall
|
|
426
|
+
// (global wiring). One list, one probe.
|
|
427
|
+
const PYTHON_CANDIDATES = ['python', 'python3', 'py'];
|
|
428
|
+
function detectPython() {
|
|
429
|
+
for (const py of PYTHON_CANDIDATES) {
|
|
430
|
+
try {
|
|
431
|
+
execSync(`${py} --version`, { stdio: 'ignore' });
|
|
432
|
+
return py;
|
|
433
|
+
} catch (e) { /* try the next interpreter */ }
|
|
434
|
+
}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Case/separator normalization uses the PLATFORM'S OWN path.relative: win32
|
|
439
|
+
// folds case and separators; forcing win32 semantics on POSIX would fold
|
|
440
|
+
// genuinely distinct paths (a false positive). One helper guards BOTH the
|
|
441
|
+
// uninstall attribution and the marker lookup — a naive prefix/string match
|
|
442
|
+
// misses `c:\` vs `C:\`, which on attribution re-manufactures the dead hook
|
|
443
|
+
// and on the marker re-wires against a standing opt-out.
|
|
444
|
+
function pathsEqual(a, b) {
|
|
445
|
+
try {
|
|
446
|
+
return path.relative(String(a), String(b)) === '';
|
|
447
|
+
} catch (e) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function pathInside(child, parent) {
|
|
452
|
+
try {
|
|
453
|
+
const rel = path.relative(String(parent), String(child));
|
|
454
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// The marker lives under the family's agent-global root (the same root the
|
|
461
|
+
// validator's AGENTIC_SDLC_KB_ROOT seam names), read at call time so the test
|
|
462
|
+
// battery's stubbed env applies. Every helper is fail-open: a broken marker
|
|
463
|
+
// must never break an install, and a failed marker write never undoes a wire.
|
|
464
|
+
function orientMarkerPath() {
|
|
465
|
+
const root = process.env.AGENTIC_SDLC_KB_ROOT
|
|
466
|
+
|| path.join(os.homedir(), '.agentic-sdlc');
|
|
467
|
+
return path.join(root, 'orient-hook-wired');
|
|
468
|
+
}
|
|
469
|
+
function orientMarkerTargets() {
|
|
470
|
+
try {
|
|
471
|
+
return fs.readFileSync(orientMarkerPath(), 'utf8')
|
|
472
|
+
.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
473
|
+
} catch (e) {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function orientMarkerHas(target) {
|
|
478
|
+
return orientMarkerTargets().some((t) => pathsEqual(t, target));
|
|
479
|
+
}
|
|
480
|
+
function orientMarkerAdd(target) {
|
|
481
|
+
try {
|
|
482
|
+
if (orientMarkerHas(target)) return;
|
|
483
|
+
fs.mkdirSync(path.dirname(orientMarkerPath()), { recursive: true });
|
|
484
|
+
fs.appendFileSync(orientMarkerPath(), target + String.fromCharCode(10), 'utf8');
|
|
485
|
+
} catch (e) { /* fail-open */ }
|
|
486
|
+
}
|
|
487
|
+
function orientMarkerRemove(target) {
|
|
488
|
+
try {
|
|
489
|
+
const rest = orientMarkerTargets().filter((t) => !pathsEqual(t, target));
|
|
490
|
+
if (rest.length) {
|
|
491
|
+
fs.writeFileSync(orientMarkerPath(), rest.join(String.fromCharCode(10)) + String.fromCharCode(10), 'utf8');
|
|
492
|
+
} else {
|
|
493
|
+
fs.rmSync(orientMarkerPath(), { force: true });
|
|
494
|
+
}
|
|
495
|
+
} catch (e) { /* fail-open */ }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Wire the machine-global hook into client.home/settings.json — exactly ONE
|
|
499
|
+
// file (user level has no local split; the path is machine-local by nature).
|
|
500
|
+
// Codes follow the wireOrientHook contract, plus `opted-out`.
|
|
501
|
+
function wireGlobalOrientHook(options) {
|
|
502
|
+
const client = options.client;
|
|
503
|
+
const python = options.python;
|
|
504
|
+
|
|
505
|
+
const validator = path.join(skillTarget(client), 'scripts', ORIENT_ENTRY_POINT);
|
|
506
|
+
if (!fs.existsSync(validator)) return { code: 'no-validator', validator };
|
|
507
|
+
if (!python) return { code: 'no-python', validator };
|
|
508
|
+
if (ORIENT_UNSAFE_IN_PATH.test(validator)) return { code: 'unsafe-path', validator };
|
|
509
|
+
const command = orientHookCommand(python, validator, false);
|
|
510
|
+
|
|
511
|
+
const target = path.join(client.home, 'settings.json');
|
|
512
|
+
const state = orientSettingsState(target);
|
|
513
|
+
if (!state.ok) return { code: 'malformed', target, why: state.why, command };
|
|
514
|
+
|
|
515
|
+
const existing = findOrientHook(state.settings);
|
|
516
|
+
if (existing) {
|
|
517
|
+
const found = orientHookValidator(existing);
|
|
518
|
+
if (found === null || !path.isAbsolute(found)) {
|
|
519
|
+
// No cwd exists at user level, so an unresolvable token is "cannot
|
|
520
|
+
// tell" — left alone, never remembered as ours.
|
|
521
|
+
return { code: 'unverifiable', target, existing, command };
|
|
522
|
+
}
|
|
523
|
+
if (fs.existsSync(found)) {
|
|
524
|
+
// A hand-wired live hook, once removed, deserves the same respect as
|
|
525
|
+
// one this installer wrote: remember the target.
|
|
526
|
+
orientMarkerAdd(target);
|
|
527
|
+
return { code: 'already', target, command: existing };
|
|
528
|
+
}
|
|
529
|
+
return { code: 'broken', target, existing, command };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (orientMarkerHas(target)) return { code: 'opted-out', target };
|
|
533
|
+
|
|
534
|
+
const settings = state.settings;
|
|
535
|
+
if (!settings.hooks) settings.hooks = {};
|
|
536
|
+
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
537
|
+
settings.hooks.SessionStart.push({
|
|
538
|
+
hooks: [{
|
|
539
|
+
type: 'command',
|
|
540
|
+
command,
|
|
541
|
+
timeout: ORIENT_HOOK_TIMEOUT,
|
|
542
|
+
statusMessage: 'Loading docs orientation...',
|
|
543
|
+
}],
|
|
544
|
+
});
|
|
545
|
+
try {
|
|
546
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
547
|
+
fs.writeFileSync(target, JSON.stringify(settings, null, 2) + String.fromCharCode(10), 'utf8');
|
|
548
|
+
} catch (e) {
|
|
549
|
+
return { code: 'write-failed', target, command, error: e.message };
|
|
550
|
+
}
|
|
551
|
+
orientMarkerAdd(target);
|
|
552
|
+
return { code: 'wired', target, command };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Uninstall symmetry: removing the skill dirs would leave any global hook
|
|
556
|
+
// naming them wired-and-DEAD. So, across EVERY settings target the marker
|
|
557
|
+
// remembers (a hook wired under CLAUDE_CONFIG_DIR=/x must be cleaned from a
|
|
558
|
+
// shell without that env) unioned with the current client.home, remove ONLY
|
|
559
|
+
// the SessionStart entries whose validator token resolves INTO a root being
|
|
560
|
+
// removed. Foreign or unverifiable entries are never touched. The marker line
|
|
561
|
+
// is cleared ONLY when an entry was actually removed — when the user had
|
|
562
|
+
// already removed the hook, the line stays and the opt-out survives uninstall,
|
|
563
|
+
// reinstall, and sibling-lens uninstalls.
|
|
564
|
+
function removeGlobalOrientHooks(removedRoots) {
|
|
565
|
+
const targets = [];
|
|
566
|
+
const claude = CLIENTS.find((c) => c.key === 'claude');
|
|
567
|
+
if (claude) targets.push(path.join(claude.home, 'settings.json'));
|
|
568
|
+
for (const t of orientMarkerTargets()) {
|
|
569
|
+
if (!targets.some((x) => pathsEqual(x, t))) targets.push(t);
|
|
570
|
+
}
|
|
571
|
+
const removedFrom = [];
|
|
572
|
+
for (const target of targets) {
|
|
573
|
+
try {
|
|
574
|
+
const state = orientSettingsState(target);
|
|
575
|
+
if (!state.ok || !fs.existsSync(target)) continue;
|
|
576
|
+
const hooks = state.settings.hooks;
|
|
577
|
+
if (!hooks || !Array.isArray(hooks.SessionStart)) continue;
|
|
578
|
+
let removedHere = false;
|
|
579
|
+
const ours = (h) => {
|
|
580
|
+
const cmd = h && typeof h.command === 'string' ? h.command : '';
|
|
581
|
+
if (!(ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && / orient(\s|$)/.test(cmd))) return false;
|
|
582
|
+
const tok = orientHookValidator(cmd);
|
|
583
|
+
if (tok === null || !path.isAbsolute(tok)) return false; // cannot attribute: keep
|
|
584
|
+
return removedRoots.some((root) => pathInside(tok, root));
|
|
585
|
+
};
|
|
586
|
+
const kept = [];
|
|
587
|
+
for (const group of hooks.SessionStart) {
|
|
588
|
+
if (!group || typeof group !== 'object') { kept.push(group); continue; }
|
|
589
|
+
if (Array.isArray(group.hooks)) {
|
|
590
|
+
const inner = group.hooks.filter((h) => {
|
|
591
|
+
if (ours(h)) { removedHere = true; return false; }
|
|
592
|
+
return true;
|
|
593
|
+
});
|
|
594
|
+
if (inner.length) kept.push({ ...group, hooks: inner });
|
|
595
|
+
else if (inner.length !== group.hooks.length) { /* emptied: prune */ }
|
|
596
|
+
else kept.push(group);
|
|
597
|
+
} else if (ours(group)) {
|
|
598
|
+
removedHere = true; // direct-object shape: prune
|
|
599
|
+
} else {
|
|
600
|
+
kept.push(group);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (!removedHere) continue;
|
|
604
|
+
state.settings.hooks.SessionStart = kept;
|
|
605
|
+
if (!kept.length) delete state.settings.hooks.SessionStart;
|
|
606
|
+
if (state.settings.hooks && !Object.keys(state.settings.hooks).length) {
|
|
607
|
+
delete state.settings.hooks;
|
|
608
|
+
}
|
|
609
|
+
fs.writeFileSync(target, JSON.stringify(state.settings, null, 2) + String.fromCharCode(10), 'utf8');
|
|
610
|
+
orientMarkerRemove(target);
|
|
611
|
+
removedFrom.push(target);
|
|
612
|
+
} catch (e) { /* fail-open per target: never abort an uninstall */ }
|
|
613
|
+
}
|
|
614
|
+
return removedFrom;
|
|
388
615
|
}
|
|
389
616
|
|
|
390
617
|
module.exports = {
|
|
@@ -397,6 +624,10 @@ module.exports = {
|
|
|
397
624
|
clientDetected,
|
|
398
625
|
skillTarget,
|
|
399
626
|
wireOrientHook,
|
|
627
|
+
wireGlobalOrientHook,
|
|
628
|
+
removeGlobalOrientHooks,
|
|
629
|
+
detectPython,
|
|
630
|
+
pathsEqual,
|
|
400
631
|
orientHookCommand,
|
|
401
632
|
copyRecursive,
|
|
402
633
|
loadTemplates,
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
SKILL_SOURCE, CLIENTS, clientDetected, skillTarget, copyRecursive,
|
|
6
|
+
wireGlobalOrientHook, detectPython,
|
|
7
|
+
} = require('./lib');
|
|
5
8
|
|
|
6
9
|
function installSkill(client) {
|
|
7
10
|
if (!fs.existsSync(SKILL_SOURCE)) {
|
|
@@ -29,7 +32,36 @@ for (const client of CLIENTS) {
|
|
|
29
32
|
if (clientDetected(client)) {
|
|
30
33
|
console.log(`✅ Detected: ${client.label}`);
|
|
31
34
|
detected = true;
|
|
32
|
-
installSkill(client);
|
|
35
|
+
const installed = installSkill(client);
|
|
36
|
+
// F-042: wire the machine-global orientation hook — the install IS the
|
|
37
|
+
// consent, so no question is ever asked. Claude Code only (the one client
|
|
38
|
+
// whose hook shape a fixture pins); removal is a standing opt-out the
|
|
39
|
+
// marker remembers. A convenience must never fail an npm install: wrapped.
|
|
40
|
+
if (client.key === 'claude' && installed) {
|
|
41
|
+
try {
|
|
42
|
+
const r = wireGlobalOrientHook({ client, python: detectPython() });
|
|
43
|
+
if (r.code === 'wired') {
|
|
44
|
+
console.log('🔗 Session orientation wired for every project on this machine');
|
|
45
|
+
console.log(` (${r.target}).`);
|
|
46
|
+
console.log(' Agents now start oriented on any project with a docs root; projects');
|
|
47
|
+
console.log(' without one see nothing. To opt out, remove that hook entry — it');
|
|
48
|
+
console.log(' will not be re-added.');
|
|
49
|
+
} else if (r.code === 'broken') {
|
|
50
|
+
console.log('⚠️ A session-orientation hook exists but the validator it names does');
|
|
51
|
+
console.log(' not resolve, so it has been emitting nothing. Correct its command to:');
|
|
52
|
+
console.log(` ${r.command}`);
|
|
53
|
+
} else if (r.code === 'no-python') {
|
|
54
|
+
console.log('ℹ️ Python not found: session orientation not wired (wire it later per');
|
|
55
|
+
console.log(' ENFORCEMENT.md §4).');
|
|
56
|
+
} else if (r.code === 'malformed' || r.code === 'write-failed'
|
|
57
|
+
|| r.code === 'unsafe-path') {
|
|
58
|
+
console.log(`⚠️ Session orientation not wired (${r.code}). Wire it by hand per`);
|
|
59
|
+
console.log(' ENFORCEMENT.md §4.');
|
|
60
|
+
}
|
|
61
|
+
// already / opted-out / unverifiable / no-validator: silent — an
|
|
62
|
+
// install must not nag (the opt-out is the user's standing choice).
|
|
63
|
+
} catch (e) { /* never fail the install for a convenience */ }
|
|
64
|
+
}
|
|
33
65
|
}
|
|
34
66
|
}
|
|
35
67
|
|
package/scripts/preuninstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const { CLIENTS, skillTarget } = require('./lib');
|
|
4
|
+
const { CLIENTS, skillTarget, removeGlobalOrientHooks } = require('./lib');
|
|
5
5
|
|
|
6
6
|
function removeSkill(client) {
|
|
7
7
|
const target = skillTarget(client);
|
|
@@ -14,4 +14,17 @@ function removeSkill(client) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// F-042: BEFORE the skill dirs go, remove the global orientation hooks that
|
|
18
|
+
// name them — otherwise every future session carries a hook whose validator no
|
|
19
|
+
// longer exists, the wired-and-DEAD state the check note calls the worst of
|
|
20
|
+
// the three. Surgical (only entries attributable to the removed dirs; the
|
|
21
|
+
// marker's opt-out lines survive when nothing was removed) and fail-open (an
|
|
22
|
+
// uninstall must never abort on a settings file).
|
|
23
|
+
try {
|
|
24
|
+
const removedFrom = removeGlobalOrientHooks(CLIENTS.map(skillTarget));
|
|
25
|
+
for (const target of removedFrom) {
|
|
26
|
+
console.log(`🧹 Removed the session-orientation hook entry (${target}).`);
|
|
27
|
+
}
|
|
28
|
+
} catch (e) { /* never abort an uninstall for a convenience */ }
|
|
29
|
+
|
|
17
30
|
CLIENTS.forEach(removeSkill);
|
|
@@ -86,6 +86,37 @@ that cannot be placed in a command safely, or a settings file it cannot write. `
|
|
|
86
86
|
prints which case applied, per client — a silent skip is what let "documented default
|
|
87
87
|
that nobody installs" survive in the first place.
|
|
88
88
|
|
|
89
|
+
**Older and agent-bootstrapped projects are silently unwired -- `check` now tells you (F-041).**
|
|
90
|
+
`init` wires the hook at init time only: a project initialized before F-036, or whose
|
|
91
|
+
docs root was bootstrapped by the agent (which runs `index`, never `init`), never
|
|
92
|
+
received it retroactively -- and until F-041 nothing said so. `check` now prints, right after its summary line, a
|
|
93
|
+
one-line `[note]` when no orientation hook is detectable (`.claude/settings.json`,
|
|
94
|
+
`.claude/settings.local.json`, `.codex/hooks.json`), and a DISTINCT note when a hook is
|
|
95
|
+
wired but the validator it names does not resolve (the wired-and-DEAD state below --
|
|
96
|
+
silence there would bless the worst of the three states). The notes are informational
|
|
97
|
+
only: never the exit code, never a `validate` warning -- CI (`validate --strict`) does
|
|
98
|
+
not see them, because the wired hook legitimately lives in the git-ignored
|
|
99
|
+
`settings.local.json`. Known residuals, accepted as honest: a client with no hook
|
|
100
|
+
mechanism (Gemini today) keeps the note -- read it as the manual-reads reminder; a
|
|
101
|
+
`.codex/hooks.json` wired in a shape other than the documented one keeps it too.
|
|
102
|
+
|
|
103
|
+
**Installing the package wires the hook machine-wide (F-042).** On Claude Code
|
|
104
|
+
with Python, `npm i -g` (and every update) wires a SessionStart orient hook into
|
|
105
|
+
the USER-level settings (`$CLAUDE_CONFIG_DIR/settings.json` when set, else
|
|
106
|
+
`~/.claude/settings.json`), covering every project on the machine -- `orient` is
|
|
107
|
+
fail-open, so projects without a docs root see nothing. The install itself is
|
|
108
|
+
the consent: no question is asked. **Removing that hook entry is a standing
|
|
109
|
+
opt-out**: a per-target marker (`<agentic-sdlc root>/orient-hook-wired`,
|
|
110
|
+
default `~/.agentic-sdlc/`) remembers each settings file ever wired, and no
|
|
111
|
+
install or update ever re-adds a removed hook; deleting the marker file is the
|
|
112
|
+
re-enable gesture. `npm uninstall` removes the entries it can attribute to the
|
|
113
|
+
removed skill (and only those), so no hook is left pointing at deleted files.
|
|
114
|
+
Notes: the global command carries no `--hybrid` (a machine-global hook cannot
|
|
115
|
+
know per-project mode -- on Hybrid projects only the devPNT pointer line is
|
|
116
|
+
absent); a project wired per-project AND globally prints its orientation twice
|
|
117
|
+
-- remove one of the two entries; `init` reports "covered by the machine-global
|
|
118
|
+
hook" instead of wiring a project-level duplicate.
|
|
119
|
+
|
|
89
120
|
**Which settings file, and why it is not always the shared one.** The command names a
|
|
90
121
|
validator, and where that validator lives decides where the hook may be written:
|
|
91
122
|
|
|
@@ -133,6 +164,38 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
|
|
|
133
164
|
- **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.
|
|
134
165
|
- Like the CI gate (§2), if you copied `sdlc_check.py` into the repo, the hook references that copy — keep it current when you update the skill.
|
|
135
166
|
|
|
167
|
+
### Per-turn reminder: `remind` (UserPromptSubmit -- opt-in, NEVER a default)
|
|
168
|
+
|
|
169
|
+
`orient` fires once, at session start; a long session drifts after it (a compaction,
|
|
170
|
+
ten turns of other work -- the F-041 field report). `sdlc_check.py remind` prints ONE
|
|
171
|
+
constant line -- the kb minimum: the triage levels, the topic-index recall trigger, the
|
|
172
|
+
capture moment -- for the client to inject at every prompt. Cost: ~50 tokens per
|
|
173
|
+
prompt, which is exactly why `init` never wires it and never will by default: the
|
|
174
|
+
per-turn cost is the project's choice. The line is constant and zero-read, so nothing
|
|
175
|
+
repo-controlled can ride into the agent's context through it, and the vision's
|
|
176
|
+
no-per-turn-graph-queries non-goal stays honored -- the line re-arms the
|
|
177
|
+
entry-into-topic trigger; it never runs the reading itself.
|
|
178
|
+
|
|
179
|
+
Claude Code -- in the project's `.claude/settings.json` (or `settings.local.json` when
|
|
180
|
+
the validator path is machine-specific, same rule as the orientation hook above):
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"hooks": {
|
|
185
|
+
"UserPromptSubmit": [
|
|
186
|
+
{
|
|
187
|
+
"hooks": [
|
|
188
|
+
{
|
|
189
|
+
"type": "command",
|
|
190
|
+
"command": "python \"C:\\Users\\<user>\\.claude\\skills\\kb-agentic\\scripts\\sdlc_check.py\" remind"
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
136
199
|
## 5. Skill eval battery (release gate)
|
|
137
200
|
|
|
138
201
|
**Skill development only.** `test_*.py` and `evals/` are deliberately absent from the npm `files` allowlist — they never reach an installed consumer, so this section applies to whoever builds the skill, not to a project that uses it. (Consumers get `sdlc_check.py`; §1–§4 are theirs.)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: kb-agentic
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.9.0
|
|
4
4
|
description: Knowledge-Base & Document-First protocol with risk-proportional triage, Vision as a guide, Signal Distillation, a complete Standalone mode and optional symbiosis with devPNT. Use for user documentation, knowledge extraction, SOPs, research notes, decision logs, knowledge management — and for answering project questions from the claim ledger and topic graph.
|
|
5
5
|
author: Antonio Pinto (https://github.com/Antoneeo)
|
|
6
6
|
copyright: (c) 2026 Antonio Pinto
|
|
@@ -23,7 +23,7 @@ Support files in the skill directory:
|
|
|
23
23
|
- `review.md`: the review discipline — when a review is due, how to request one, how to receive findings, how to review.
|
|
24
24
|
- `dispatch.md`: opt-in subagent execution of an approved plan.
|
|
25
25
|
- `routing.md`: which lens owns this unit of work. Read ONLY when a sibling lens skill is installed alongside this one; a single-lens install never reads it.
|
|
26
|
-
- `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`). Two files: the core is the family's shared spine; the entry point IS the knowledge overlay — the claim ledger and topic-graph checks live inside it, so the core alone runs none of them. Copy both, or neither.
|
|
26
|
+
- `scripts/sdlc_check.py` + `scripts/sdlc_core.py`: the mechanical validator for the docs root (`check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`, and the knowledge overlay: `graph`, `corpus`, `claim-id`, `anchor`, `export`, `import`, plus `remind` — the opt-in per-turn one-line reminder, ENFORCEMENT §4). Two files: the core is the family's shared spine; the entry point IS the knowledge overlay — the claim ledger and topic-graph checks live inside it, so the core alone runs none of them. Copy both, or neither.
|
|
27
27
|
- `ENFORCEMENT.md`: optional setup for CI and hooks.
|
|
28
28
|
|
|
29
29
|
Read these files only when needed. `SKILL.md` is the operating contract; the support files are progressive resources.
|
|
@@ -1598,6 +1598,26 @@ def kb_cmd_orient(argv):
|
|
|
1598
1598
|
return rc
|
|
1599
1599
|
|
|
1600
1600
|
|
|
1601
|
+
def kb_cmd_remind(_argv):
|
|
1602
|
+
"""F-041 item B: the per-turn one-line reminder (UserPromptSubmit, opt-in).
|
|
1603
|
+
|
|
1604
|
+
A CONSTANT, zero-read line -- the kb minimum an agent needs re-armed every
|
|
1605
|
+
prompt: triage, the recall trigger, the capture moment. Constant by
|
|
1606
|
+
contract: nothing repo-controlled may ride a line injected into the agent's
|
|
1607
|
+
context every turn, and zero reads is what keeps the per-turn cost flat.
|
|
1608
|
+
The vision's non-goal bans per-turn GRAPH queries; this line only re-arms
|
|
1609
|
+
the entry-into-topic criterion, it never runs the descent. Never wired by
|
|
1610
|
+
init and never a default: ENFORCEMENT par.4 documents the manual snippet
|
|
1611
|
+
and the ~50 tokens/prompt cost -- the project chooses. Fail-open like every
|
|
1612
|
+
hook surface: argv is ignored whole, the exit code is always 0."""
|
|
1613
|
+
print("kb-agentic: triage before acting -- L1 quick fact, L2 propagation of "
|
|
1614
|
+
"settled knowledge, L3 new knowledge unit, Spike exploration; declare "
|
|
1615
|
+
"the level. Answering about this project's domain? Read topics/INDEX.md "
|
|
1616
|
+
"under the docs root first and cite claim ids, or state no coverage. "
|
|
1617
|
+
"Decisions made today get recorded in the KB at session close.")
|
|
1618
|
+
return 0
|
|
1619
|
+
|
|
1620
|
+
|
|
1601
1621
|
def kb_cmd_help():
|
|
1602
1622
|
"""The spine's usage, then the overlay's own commands.
|
|
1603
1623
|
|
|
@@ -1619,6 +1639,8 @@ knowledge overlay (kb-agentic) -- also available:
|
|
|
1619
1639
|
anchor <path> <phrase> resolve a phrase to a verified locator span
|
|
1620
1640
|
export --out <dir> bundle a subgraph WITH the bytes its claims cite
|
|
1621
1641
|
import <dir> merge a bundle in additively (never overwrites)
|
|
1642
|
+
remind one-line per-turn reminder (UserPromptSubmit hook,
|
|
1643
|
+
opt-in -- ENFORCEMENT par.4; constant, zero-read)
|
|
1622
1644
|
|
|
1623
1645
|
index / validate / check the spine's behaviour PLUS the claim ledger and
|
|
1624
1646
|
the topic graph""")
|
|
@@ -1636,6 +1658,10 @@ def main(argv=None):
|
|
|
1636
1658
|
# spine runs it on the raw argv, the overlay only appends the topic router.
|
|
1637
1659
|
if argv and argv[0] == "orient":
|
|
1638
1660
|
return kb_cmd_orient(argv)
|
|
1661
|
+
# `remind` too: it is a hook surface (UserPromptSubmit), so a stray flag
|
|
1662
|
+
# must print the line, not an argparse usage error into someone's prompt.
|
|
1663
|
+
if argv and argv[0] == "remind":
|
|
1664
|
+
return kb_cmd_remind(argv)
|
|
1639
1665
|
# Forward-by-default: anything not intercepted goes to the spine untouched.
|
|
1640
1666
|
# Never a hand-copied command tuple - that is how a spine command gets
|
|
1641
1667
|
# silently dropped (mkt_check.py ships that exact defect with `migrate`).
|
|
@@ -1646,6 +1646,143 @@ def cmd_mark(root, paths):
|
|
|
1646
1646
|
return 0
|
|
1647
1647
|
|
|
1648
1648
|
|
|
1649
|
+
# ------------------------------------------------- orientation-hook wiring
|
|
1650
|
+
# F-041: read-only mirror of the installer-side detection in lib.js
|
|
1651
|
+
# (findOrientHook + orientHookValidator). The JS owns WRITING hooks, with its
|
|
1652
|
+
# injection guards; this side only DETECTS, at `check`, so a project whose
|
|
1653
|
+
# sessions start unoriented hears about it in-project (ENFORCEMENT par.4: the
|
|
1654
|
+
# hook is wired at init only -- pre-F-036 and agent-bootstrapped projects never
|
|
1655
|
+
# got it, and a wired-but-dead hook is "the worst of the three states").
|
|
1656
|
+
# The file list is a SUPERSET of the JS one: .codex/hooks.json is documented in
|
|
1657
|
+
# ENFORCEMENT par.4 but never written by init. Pinned equal on both sides:
|
|
1658
|
+
# the entry-point names, and (F-042) the user-settings path derivation --
|
|
1659
|
+
# CLAUDE_CONFIG_DIR when set, else <home>/.claude -- empty-string env falsy
|
|
1660
|
+
# on both sides.
|
|
1661
|
+
ORIENT_HOOK_ENTRY_POINTS = ("sdlc_check.py", "mkt_check.py")
|
|
1662
|
+
ORIENT_HOOK_CONFIG_FILES = (".claude/settings.json",
|
|
1663
|
+
".claude/settings.local.json",
|
|
1664
|
+
".codex/hooks.json")
|
|
1665
|
+
ORIENT_HOOK_CONFIG_CAP = 1024 * 1024 # settings are tiny; cap pathological files
|
|
1666
|
+
_ORIENT_HOOK_CMD_RE = re.compile(r" orient(\s|$)")
|
|
1667
|
+
_ORIENT_HOOK_TOKEN_RE = re.compile(r'"([^"]*)"|(\S+)')
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def _orient_entries_in(p):
|
|
1671
|
+
"""Yield the SessionStart commands invoking an orient entry point from ONE
|
|
1672
|
+
client config file. Fail-open: an unreadable, oversized, malformed or
|
|
1673
|
+
directory-shaped file contributes nothing. Reads are utf-8-sig -- a BOM'd
|
|
1674
|
+
settings.json (PowerShell/Notepad) must not read as unwired (declared
|
|
1675
|
+
divergence from Node's JSON.parse)."""
|
|
1676
|
+
try:
|
|
1677
|
+
if not p.is_file() or p.stat().st_size > ORIENT_HOOK_CONFIG_CAP:
|
|
1678
|
+
return
|
|
1679
|
+
data = json.loads(p.read_text(encoding="utf-8-sig"))
|
|
1680
|
+
except Exception:
|
|
1681
|
+
return
|
|
1682
|
+
if not isinstance(data, dict):
|
|
1683
|
+
return
|
|
1684
|
+
hooks = data.get("hooks")
|
|
1685
|
+
groups = hooks.get("SessionStart") if isinstance(hooks, dict) else None
|
|
1686
|
+
if groups is None:
|
|
1687
|
+
groups = data.get("SessionStart") # the .codex/hooks.json shape
|
|
1688
|
+
if not isinstance(groups, list):
|
|
1689
|
+
return
|
|
1690
|
+
for group in groups:
|
|
1691
|
+
if not isinstance(group, dict):
|
|
1692
|
+
continue
|
|
1693
|
+
inner = group.get("hooks")
|
|
1694
|
+
for h in inner if isinstance(inner, list) else [group]:
|
|
1695
|
+
cmd = h.get("command") if isinstance(h, dict) else None
|
|
1696
|
+
if (isinstance(cmd, str)
|
|
1697
|
+
and any(n in cmd for n in ORIENT_HOOK_ENTRY_POINTS)
|
|
1698
|
+
and _ORIENT_HOOK_CMD_RE.search(cmd)):
|
|
1699
|
+
yield cmd
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _orient_user_settings_path():
|
|
1703
|
+
"""The machine-global settings file (F-042 wires it at install time).
|
|
1704
|
+
|
|
1705
|
+
Read at CALL time -- a DECLARED divergence from AGENTIC_SDLC_KB_ROOT's
|
|
1706
|
+
import-time read: unittest imports this module once before any test runs,
|
|
1707
|
+
so an import-time seam would be frozen before the matrix could set it.
|
|
1708
|
+
AGENTIC_SDLC_USER_SETTINGS is the test-isolation seam (python-side only);
|
|
1709
|
+
the production derivation mirrors lib.js: CLAUDE_CONFIG_DIR || ~/.claude."""
|
|
1710
|
+
seam = os.environ.get("AGENTIC_SDLC_USER_SETTINGS")
|
|
1711
|
+
if seam:
|
|
1712
|
+
return Path(seam)
|
|
1713
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
1714
|
+
return (Path(cfg) if cfg else Path.home() / ".claude") / "settings.json"
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def _orient_hook_commands(root):
|
|
1718
|
+
"""Yield (command, at_user_level) across every known client config: the
|
|
1719
|
+
three project-relative files, then the machine-global user settings --
|
|
1720
|
+
the file the installer wires (F-042), without which a fixed machine's
|
|
1721
|
+
projects would read as unwired forever."""
|
|
1722
|
+
for rel in ORIENT_HOOK_CONFIG_FILES:
|
|
1723
|
+
for cmd in _orient_entries_in(Path(root) / rel):
|
|
1724
|
+
yield cmd, False
|
|
1725
|
+
for cmd in _orient_entries_in(_orient_user_settings_path()):
|
|
1726
|
+
yield cmd, True
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def orient_hook_state(root):
|
|
1730
|
+
"""'wired' | 'dead' | 'unwired'.
|
|
1731
|
+
|
|
1732
|
+
A matching entry whose tokens name no validator is "cannot tell" and counts
|
|
1733
|
+
as wired (the lib.js contract: never report broken on cannot-tell). 'dead'
|
|
1734
|
+
requires every matching entry to name a validator that does not resolve:
|
|
1735
|
+
the client runs ALL SessionStart hooks -- project-level AND user-level
|
|
1736
|
+
merge -- so one live entry at either level still orients the session
|
|
1737
|
+
(any-resolves aggregation across levels). Relative tokens resolve against
|
|
1738
|
+
the project root (the vendored-validator convention); a USER-level relative
|
|
1739
|
+
token has no cwd to resolve against and is treated as cannot-tell."""
|
|
1740
|
+
matched = False
|
|
1741
|
+
for cmd, at_user_level in _orient_hook_commands(root):
|
|
1742
|
+
matched = True
|
|
1743
|
+
token = None
|
|
1744
|
+
for m in _ORIENT_HOOK_TOKEN_RE.finditer(cmd):
|
|
1745
|
+
tok = m.group(1) if m.group(1) is not None else m.group(2)
|
|
1746
|
+
if any(tok.endswith(n) for n in ORIENT_HOOK_ENTRY_POINTS):
|
|
1747
|
+
token = tok
|
|
1748
|
+
break
|
|
1749
|
+
if token is None:
|
|
1750
|
+
return "wired"
|
|
1751
|
+
tp = Path(token)
|
|
1752
|
+
if not tp.is_absolute():
|
|
1753
|
+
if at_user_level:
|
|
1754
|
+
return "wired" # cannot tell: no cwd exists at user level
|
|
1755
|
+
tp = Path(root) / tp
|
|
1756
|
+
if tp.exists():
|
|
1757
|
+
return "wired"
|
|
1758
|
+
return "dead" if matched else "unwired"
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def print_orient_hook_note(root):
|
|
1762
|
+
"""Print the FS-A wiring note (unwired / dead), or nothing when wired.
|
|
1763
|
+
|
|
1764
|
+
Informational only, by contract: never an exit code, never a validate
|
|
1765
|
+
warning (--strict CI must not fail on a hook that legitimately lives in
|
|
1766
|
+
git-ignored settings.local.json). Called by the spine's cmd_check AND by
|
|
1767
|
+
an overlay that replaces check with its own pipeline (the marketing lens
|
|
1768
|
+
does): the note is check-layer behaviour, whoever owns the check."""
|
|
1769
|
+
try:
|
|
1770
|
+
state = orient_hook_state(root)
|
|
1771
|
+
if state == "unwired":
|
|
1772
|
+
print("[note] agent sessions start without automatic orientation on this "
|
|
1773
|
+
"machine (no session-orientation hook found at project or user level). "
|
|
1774
|
+
"Fix once: update or reinstall the skill npm package (on Claude Code "
|
|
1775
|
+
"with Python it wires the hook machine-wide), or wire it per "
|
|
1776
|
+
"ENFORCEMENT.md par.4")
|
|
1777
|
+
elif state == "dead":
|
|
1778
|
+
print("[note] a session-orientation hook is configured but points at a "
|
|
1779
|
+
"validator that no longer exists, so sessions start unoriented. Fix: "
|
|
1780
|
+
"update or reinstall the skill npm package, or correct the command. "
|
|
1781
|
+
"Details: ENFORCEMENT.md par.4")
|
|
1782
|
+
except Exception:
|
|
1783
|
+
pass # detection must never break check
|
|
1784
|
+
|
|
1785
|
+
|
|
1649
1786
|
def cmd_check(root, strict=False, hybrid=False):
|
|
1650
1787
|
print("===== validate =====")
|
|
1651
1788
|
rc_v = cmd_validate(root, strict=strict, hybrid=hybrid)
|
|
@@ -1653,6 +1790,7 @@ def cmd_check(root, strict=False, hybrid=False):
|
|
|
1653
1790
|
rc_s = cmd_stale(root, hybrid=hybrid)
|
|
1654
1791
|
print(f"\ncheck: {'CLEAN' if not (rc_v or rc_s) else 'NOT CLEAN'} "
|
|
1655
1792
|
f"(validate rc={rc_v}, stale rc={rc_s})")
|
|
1793
|
+
print_orient_hook_note(root)
|
|
1656
1794
|
return 1 if (rc_v or rc_s) else 0
|
|
1657
1795
|
|
|
1658
1796
|
|