@antoneeo/kb-agentic-skill 1.8.0 → 1.10.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,48 @@
2
2
 
3
3
  Every significant change to this skill is recorded here.
4
4
 
5
+ ## [1.10.0] - 2026-08-28
6
+
7
+ F-043 -- the revision doctrine: documents read as current state.
8
+
9
+ ### Added
10
+ - **`## Revision — the full re-read` (SKILL.md).** Incorporating new knowledge
11
+ into an existing document is a rewrite, not an append: full re-read first,
12
+ body rewritten to current state, history in the Diary (past entries never
13
+ rewritten -- a correction is a new entry), claim tables exempt (rows move only
14
+ per reconciliation), node frontmatter exempt (a revision never re-parents),
15
+ and a multi-document pass re-reads PER DOCUMENT. Wired where the trigger
16
+ fires: the L2 triage row and the L3 Phase-4 flow (citation-only pointers);
17
+ the honest limit is in the doctrine's own text. Born from a field post-mortem
18
+ (12 documents realigned by delta-append; stale sentences left standing) --
19
+ and completed by its own review: three rounds, each catching the doctrine's
20
+ failure mode inside its own ANALYSIS.
21
+ - **The `remind` line carries the gesture** ("Re-read it whole and rewrite it
22
+ to current state ..."): 443 chars, ~75 tokens per prompt, still one constant
23
+ zero-read line.
24
+
25
+ ## [1.9.0] - 2026-08-28
26
+
27
+ F-042 -- the install wires the hook; the incomprehensible question dies.
28
+
29
+ ### Added
30
+ - **Installing or updating the package wires the session-orientation hook
31
+ machine-wide** (Claude Code with Python; user-level settings). The npm install
32
+ is the consent -- no question is ever asked. Removal is a standing opt-out: a
33
+ per-target marker remembers every settings file ever wired and no update
34
+ re-adds a removed hook; deleting the marker re-enables. `npm uninstall`
35
+ surgically removes only the entries attributable to the removed skill, across
36
+ every world the marker remembers, so no dead hook is left behind. `init` now
37
+ reports "covered by the machine-global hook" instead of wiring a duplicate,
38
+ and still reports a dead or unverifiable global wrapper while wiring the
39
+ project.
40
+ - **The check note is global-aware and speaks user language.** Detection now
41
+ also reads the user-level settings (`CLAUDE_CONFIG_DIR` honored;
42
+ `AGENTIC_SDLC_USER_SETTINGS` is the test seam), so a fixed machine stops
43
+ being nagged; the two notes are reworded consequence-first ("agent sessions
44
+ start without automatic orientation...") so the agent relaying them can ask a
45
+ question a normal user understands.
46
+
5
47
  ## [1.8.0] - 2026-08-28
6
48
 
7
49
  F-041 -- the mid-session drift field report, both items.
package/README.md CHANGED
@@ -24,6 +24,8 @@ Two axes are kept apart on purpose — **abstraction** (a topic made of topics:
24
24
 
25
25
  7. **Capture — the day's decisions reach the ledger before they evaporate.** At a user-signed closing ("chiudiamo"), one sweep question — carrying its own inline result: "today I recorded X and Y — any other decisions from today?" — reaches even the decisions taken outside the session, which no transcript search could find. What surfaces lands on the existing rails (an elicited dated note, an L1 claim row) with explicit provenance; the sweep is idempotent (nothing captured twice), never creates topics, and stays silent when no one signed off. The kb `orient` shows notes recency at every session start — dated from note frontmatter, because file mtimes lie after a clone or worktree — so a missed capture is visible the next morning.
26
26
 
27
+ 8. **Revision — documents read as current state.** Incorporating new knowledge into an existing document is a full re-read and a body rewrite, not a stack of dated deltas: anchored edits are each correct while the document as a whole rots. The doctrine keeps history in the Diary (past entries never rewritten), leaves claim tables to reconciliation and graph edges to the taxonomy, and runs per document even in a multi-document pass. Born from a real field post-mortem: twelve documents realigned by append, every untouched sentence left standing as printed truth.
28
+
27
29
  Deliberately absent: any per-node coverage or completion state. `gaps:` says what a node lacks; nothing collects it into a dashboard. A **source** does record how far it has been read, but on its own sidecar and nowhere else — the corpus index prints that fact for every artifact, including the finished ones, because a list of only what is behind is the dashboard this method refuses.
28
30
 
29
31
  ## Key features
@@ -71,7 +73,12 @@ kb-agentic-init
71
73
 
72
74
  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
75
 
74
- It also **wires the SessionStart orientation hook** into `.claude/`, so an agent opening
76
+ Installing or updating the npm package **wires the session-orientation hook
77
+ machine-wide** (user-level `~/.claude/settings.json`, Claude Code with Python):
78
+ every project with a docs root greets its agent already oriented, and removing
79
+ the entry is a standing opt-out that no update ever overrides. `init` still
80
+ wires per-project where that is the better shape (a vendored validator, a
81
+ shared team file), so an agent opening
75
82
  the project is handed the reading guide, the generated manifest, the guide router and the
76
83
  last handoff before it does anything — instead of only when it remembers to look. That
77
84
  wiring used to be a manual step documented in `ENFORCEMENT.md`; being manual, it was
@@ -82,8 +89,8 @@ it: a repo that vendors it gets a repo-relative command in the shared `.claude/s
82
89
  (portable — commit it); otherwise the path is machine-specific and goes to the git-ignored
83
90
  `.claude/settings.local.json`, and each teammate runs `init` once. Re-running `init` never
84
91
  duplicates the hook, and a hook whose validator no longer resolves is reported as **broken**
85
- with the correction rather than counted as installed. Since F-041, `check` also
86
- prints a one-line note when a project has no orientation hook wired (or a dead
92
+ with the correction rather than counted as installed. `check` prints a one-line
93
+ note when a machine has no orientation hook at project or user level (or a dead
87
94
  one), and an opt-in per-turn reminder exists for long sessions: `sdlc_check.py
88
95
  remind`, one constant line via UserPromptSubmit -- documented in `ENFORCEMENT.md`
89
96
  par.4, never wired by default.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kb-agentic-skill",
3
- "version": "1.8.0",
3
+ "version": "1.10.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.8.0",
3
+ "version": "1.10.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
- 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
- })();
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,
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
- const { SKILL_SOURCE, CLIENTS, clientDetected, skillTarget, copyRecursive } = require('./lib');
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
 
@@ -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);
@@ -100,6 +100,23 @@ not see them, because the wired hook legitimately lives in the git-ignored
100
100
  mechanism (Gemini today) keeps the note -- read it as the manual-reads reminder; a
101
101
  `.codex/hooks.json` wired in a shape other than the documented one keeps it too.
102
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
+
103
120
  **Which settings file, and why it is not always the shared one.** The command names a
104
121
  validator, and where that validator lives decides where the hook may be written:
105
122
 
@@ -152,8 +169,8 @@ Gemini CLI — wire the same command into its startup-hook mechanism if present;
152
169
  `orient` fires once, at session start; a long session drifts after it (a compaction,
153
170
  ten turns of other work -- the F-041 field report). `sdlc_check.py remind` prints ONE
154
171
  constant line -- the kb minimum: the triage levels, the topic-index recall trigger, the
155
- capture moment -- for the client to inject at every prompt. Cost: ~50 tokens per
156
- prompt, which is exactly why `init` never wires it and never will by default: the
172
+ capture moment, the revision gesture -- for the client to inject at every prompt.
173
+ Cost: 443 characters, ~75 tokens per prompt, which is exactly why `init` never wires it and never will by default: the
157
174
  per-turn cost is the project's choice. The line is constant and zero-read, so nothing
158
175
  repo-controlled can ride into the agent's context through it, and the vision's
159
176
  no-per-turn-graph-queries non-goal stays honored -- the line re-arms the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: kb-agentic
3
- version: 1.8.0
3
+ version: 1.10.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
@@ -46,7 +46,7 @@ Always classify the request before choosing the process. Declare the chosen leve
46
46
  | Level | Criteria | Required process |
47
47
  |---|---|---|
48
48
  | **L1 - Quick Fact / Snippet** | One claim row added to an existing topic; a typo; a preference update. No node created, no source entering the corpus, no frontmatter change. | Implement edit directly in existing note. No new documents. |
49
- | **L2 - Propagation of settled knowledge** | The fact is **already settled** in the corpus and the work carries it into existing documents — restating, correcting a stale copy, updating an SOP that quotes it. No node created or superseded, no hierarchy change, no node frontmatter change, no new source ingested. | Mini-analysis in message: objective, impact, sources, validation. Update existing document or create single SOP/note. |
49
+ | **L2 - Propagation of settled knowledge** | The fact is **already settled** in the corpus and the work carries it into existing documents — restating, correcting a stale copy, updating an SOP that quotes it. No node created or superseded, no hierarchy change, no node frontmatter change, no new source ingested. | Mini-analysis in message: objective, impact, sources, validation. Update existing document (full re-read per `## Revision`) or create single SOP/note. |
50
50
  | **L3 - New knowledge unit / Corpus** | A source enters the corpus; a topic node is created or superseded; the hierarchy moves; a conflict must be reconciled; or what a claim asserts changes. | Full workflow: Vision Gate, Spec Elicitation, Taxonomy Pass, Knowledge Analysis, Distillation, Review, Indexing. |
51
51
  | **Spike - Exploration** | Time-boxed exploratory research or draft without merging into official KB. | Outcome in `ai_docs/solutions/SPIKE_[topic].md`. |
52
52
 
@@ -124,7 +124,7 @@ Use this mode when `devpnt_*` tools are available and point at the current proje
124
124
  - **Design review gate (end of Phase 3, before any drafting):** the analysis is reviewed by somebody other than its author — a subagent with fresh context, or a declared self-pass when no higher rung is usable — a permission-gated rung is asked about, never silently skipped (`review.md`). Follow `review.md`; log the outcome in `audit/reviews/REVIEW_LOG.md`. A knowledge structure reviewed only by the person who chose it is not reviewed.
125
125
 
126
126
  ### 4. Knowledge Processing & Distillation
127
- - **Isolate the work (Branch/worktree hygiene).** Distillation rewrites existing notes: do it on a branch or a worktree, never directly on the shared corpus, so a half-finished reconciliation is never what the next reader finds.
127
+ - **Isolate the work (Branch/worktree hygiene).** Distillation rewrites existing notes — full re-read per `## Revision` — and does it on a branch or a worktree, never directly on the shared corpus, so a half-finished reconciliation is never what the next reader finds.
128
128
  - Before drafting (L2/L3; L1 exempt), **consult the guide router** for a guide covering the task and read it first (the consult trigger, `guides.md` §0). A targeted description match, not a blanket read. Its result is the router verdict already declared with the triage level (Rule Zero).
129
129
  - Execute knowledge extraction using **Signal Distillation** (`distillation.md`).
130
130
  - **Opt-in subagent execution**: for an L3 with an approved analysis, the work MAY be executed via subagents per `dispatch.md`; default stays same-session.
@@ -243,6 +243,24 @@ exist — this section adds only the MOMENT.
243
243
  - **Silence stays legal where no one signed off**: no closing signal → no question,
244
244
  no ritual line.
245
245
 
246
+ ## Revision — the full re-read
247
+
248
+ Incorporating new knowledge into an existing document is a rewrite, not an
249
+ append: anchored edits are each correct while the global invariant — the body
250
+ reads as CURRENT state — is never re-verified. The gesture:
251
+ - Re-read the WHOLE document before editing; then rewrite the body so it reads
252
+ as written today — new knowledge fused, never quarantined in dated
253
+ sections — never append a delta.
254
+ - History goes to the Diary/notes, never an inline chronicle — and past Diary
255
+ entries are never rewritten: a correction is a new entry.
256
+ - A SUPERSEDED banner is a tombstone, never a revision device in a living doc.
257
+ - Claim tables are EXEMPT (rows move only per `reconciliation.md`) and node
258
+ frontmatter moves only per `taxonomy.md` — a revision never re-parents; the
259
+ rewrite governs prose, never the ledger or the graph edges.
260
+ - A multi-document pass re-reads PER DOCUMENT; set coverage is not integrity.
261
+ - The re-read raises the probability of catching a false premise; it cannot
262
+ guarantee it — domain judgment stays the human's.
263
+
246
264
  ## Mechanical Enforcement
247
265
  - `scripts/sdlc_check.py index`: updates `ai_docs/INDEX.md` and `ai_docs/reference/INDEX.md`.
248
266
  - `scripts/sdlc_check.py validate`: validates YAML frontmatter (`status`, `description`).
@@ -1602,7 +1602,7 @@ def kb_cmd_remind(_argv):
1602
1602
  """F-041 item B: the per-turn one-line reminder (UserPromptSubmit, opt-in).
1603
1603
 
1604
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
1605
+ prompt: triage, the recall trigger, the capture moment, the revision gesture. Constant by
1606
1606
  contract: nothing repo-controlled may ride a line injected into the agent's
1607
1607
  context every turn, and zero reads is what keeps the per-turn cost flat.
1608
1608
  The vision's non-goal bans per-turn GRAPH queries; this line only re-arms
@@ -1614,7 +1614,7 @@ def kb_cmd_remind(_argv):
1614
1614
  "settled knowledge, L3 new knowledge unit, Spike exploration; declare "
1615
1615
  "the level. Answering about this project's domain? Read topics/INDEX.md "
1616
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.")
1617
+ "Decisions made today get recorded in the KB at session close. Revising an existing document? Re-read it whole and rewrite it to current state -- never append a delta.")
1618
1618
  return 0
1619
1619
 
1620
1620
 
@@ -1654,8 +1654,10 @@ def cmd_mark(root, paths):
1654
1654
  # hook is wired at init only -- pre-F-036 and agent-bootstrapped projects never
1655
1655
  # got it, and a wired-but-dead hook is "the worst of the three states").
1656
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. Entry-point names are pinned
1658
- # equal on both sides.
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.
1659
1661
  ORIENT_HOOK_ENTRY_POINTS = ("sdlc_check.py", "mkt_check.py")
1660
1662
  ORIENT_HOOK_CONFIG_FILES = (".claude/settings.json",
1661
1663
  ".claude/settings.local.json",
@@ -1665,38 +1667,63 @@ _ORIENT_HOOK_CMD_RE = re.compile(r" orient(\s|$)")
1665
1667
  _ORIENT_HOOK_TOKEN_RE = re.compile(r'"([^"]*)"|(\S+)')
1666
1668
 
1667
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
+
1668
1717
  def _orient_hook_commands(root):
1669
- """Yield the command strings of SessionStart entries invoking an orient
1670
- entry point, across every known client config under root. Fail-open: an
1671
- unreadable, oversized, malformed or directory-shaped file contributes
1672
- nothing. Reads are utf-8-sig -- a BOM'd settings.json (PowerShell/Notepad)
1673
- must not read as unwired (declared divergence from Node's JSON.parse)."""
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."""
1674
1722
  for rel in ORIENT_HOOK_CONFIG_FILES:
1675
- p = Path(root) / rel
1676
- try:
1677
- if not p.is_file() or p.stat().st_size > ORIENT_HOOK_CONFIG_CAP:
1678
- continue
1679
- data = json.loads(p.read_text(encoding="utf-8-sig"))
1680
- except Exception:
1681
- continue
1682
- if not isinstance(data, dict):
1683
- continue
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
- continue
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
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
1700
1727
 
1701
1728
 
1702
1729
  def orient_hook_state(root):
@@ -1705,11 +1732,13 @@ def orient_hook_state(root):
1705
1732
  A matching entry whose tokens name no validator is "cannot tell" and counts
1706
1733
  as wired (the lib.js contract: never report broken on cannot-tell). 'dead'
1707
1734
  requires every matching entry to name a validator that does not resolve:
1708
- the client runs ALL SessionStart hooks, so one live entry beside a dead one
1709
- still orients the session (any-resolves aggregation). Relative tokens
1710
- resolve against the project root -- the vendored-validator convention."""
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."""
1711
1740
  matched = False
1712
- for cmd in _orient_hook_commands(root):
1741
+ for cmd, at_user_level in _orient_hook_commands(root):
1713
1742
  matched = True
1714
1743
  token = None
1715
1744
  for m in _ORIENT_HOOK_TOKEN_RE.finditer(cmd):
@@ -1721,6 +1750,8 @@ def orient_hook_state(root):
1721
1750
  return "wired"
1722
1751
  tp = Path(token)
1723
1752
  if not tp.is_absolute():
1753
+ if at_user_level:
1754
+ return "wired" # cannot tell: no cwd exists at user level
1724
1755
  tp = Path(root) / tp
1725
1756
  if tp.exists():
1726
1757
  return "wired"
@@ -1738,13 +1769,16 @@ def print_orient_hook_note(root):
1738
1769
  try:
1739
1770
  state = orient_hook_state(root)
1740
1771
  if state == "unwired":
1741
- print("[note] no session-orientation hook wired (.claude/settings.json, "
1742
- ".claude/settings.local.json, .codex/hooks.json): sessions start "
1743
- "unoriented -- run the installer's init or wire it per ENFORCEMENT.md par.4")
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")
1744
1777
  elif state == "dead":
1745
- print("[note] a session-orientation hook is wired but the validator it names "
1746
- "does not resolve: sessions start unoriented -- re-run init or fix the "
1747
- "command (ENFORCEMENT.md par.4)")
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")
1748
1782
  except Exception:
1749
1783
  pass # detection must never break check
1750
1784