@antoneeo/agentic-sdlc-skill 1.28.0 → 1.30.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,56 @@
2
2
 
3
3
  Tutte le modifiche significative a questa skill saranno documentate in questo file.
4
4
 
5
+ ## [1.30.0] - 2026-09-01
6
+
7
+ ### Added
8
+ - **F-045: Execute-Before-Specify (SKILL.md, Phase 3).** Every behavioural
9
+ claim about existing code in an L3 design ("the code today does X") derives
10
+ from an EXECUTED probe against the real system in its real states, never
11
+ from reading alone. Probes ship with the artifact as a repo harness
12
+ (`ai_docs/solutions/harness_[feature]/`) so the reviewer re-runs assertions
13
+ instead of re-deriving claims. Proportionality bound (only claims the design
14
+ rests on; structural facts stay with blast radius) and RED-first
15
+ falsifiability guard (show the probe red, then green -- a probe you cannot
16
+ make fail establishes nothing). Field measurement: eight consecutive
17
+ design-review FAILs converged to PASS in one round once claims were executed
18
+ first. ADR: `adr_2026-09-01_executed_probe_harness`.
19
+ - **F-045 (shared spine `review.md`, byte-identical x3): review-loop
20
+ convergence.** Three request/loop disciplines: *revise means converge, not
21
+ accrete* (lean rewrite at the latest before round 3, revision archaeology
22
+ lives in REVIEW_LOG, never in the artifact); *severity contract* stated with
23
+ every review request (blocker = unfit-to-proceed evidence only; wishes are
24
+ WARN at most -- calibrates the scale without pre-judging findings); *scripted
25
+ author pre-audit* (resolve every `file:line`/symbol anchor and recount every
26
+ count before requesting). Plus the lens-gated *behavioural-claim-probes*
27
+ reviewer clause (three findings: claim without probe, harness absent, probe
28
+ no longer passing) -- fires only in the lens whose SKILL.md defines
29
+ Execute-Before-Specify, i.e. this one.
30
+
31
+ ## [1.29.0] - 2026-08-28
32
+
33
+ Spine delta parked for the next release of this lens (shipped first in kb; the
34
+ spine stays byte-identical x3 in the repo while packages publish independently).
35
+
36
+ ### Added
37
+ - **F-041: `check` notes an unwired or dead session-orientation hook** (one
38
+ informational `[note]` line -- never the exit code, never a `validate` warning,
39
+ so CI is unaffected). `init` wires the hook only at init time; projects
40
+ initialized before F-036 or bootstrapped without `init` were silently unwired.
41
+
42
+ ### Added (F-042, parked with the above)
43
+ - Install/update wires the session-orientation hook machine-wide (Claude Code
44
+ with Python; user-level settings; removal is a standing per-target opt-out;
45
+ uninstall surgically removes what it wired). The check note is global-aware
46
+ and reworded in consequence-first user language.
47
+
48
+ ### Added (F-043 port, with the above)
49
+ - **The revision sentence.** Updating an existing analysis (the L2 row and the ANALYSIS write trigger) is a REWRITE, not an
50
+ append: full re-read, body rewritten to current state -- never an appended
51
+ delta section. Ported from the kb lens's `## Revision` doctrine (kb 1.10.0),
52
+ born from a field post-mortem where delta-append revisions left stale
53
+ sentences standing as printed truth.
54
+
5
55
  ## [1.28.0] - 2026-08-26
6
56
 
7
57
  F-038 — the gated rung: truthful log vocabulary and a mandated ask, no grant memory.
package/README.md CHANGED
@@ -94,6 +94,10 @@ skills/agentic-sdlc-skill/
94
94
 
95
95
  The validator is **two files**: `sdlc_core.py` is the family's shared spine, `sdlc_check.py` is this lens's entry point. Copy both, or neither — the entry point is useless alone.
96
96
 
97
+ Installing or updating the npm package wires the session-orientation hook
98
+ machine-wide (user-level Claude Code settings; removal is a standing opt-out
99
+ that no update overrides -- ENFORCEMENT.md par.4).
100
+
97
101
  ## The family: three lenses, one spine
98
102
 
99
103
  Same process, three fidelity disciplines — what the agent's assertions must be faithful to:
@@ -1,6 +1,6 @@
1
- {
2
- "name": "agentic-sdlc-skill",
3
- "version": "1.28.0",
4
- "description": "Documentation-First SDLC protocol with triage, Vision governance and optional devPNT integration.",
5
- "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
- }
1
+ {
2
+ "name": "agentic-sdlc-skill",
3
+ "version": "1.30.0",
4
+ "description": "Documentation-First SDLC protocol with triage, Vision governance and optional devPNT integration.",
5
+ "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
+ }
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "@antoneeo/agentic-sdlc-skill",
3
- "version": "1.28.0",
4
- "description": "Documentation-First SDLC protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, installed support files and optional devPNT integration.",
5
- "keywords": [
6
- "claude-code",
7
- "claude-skill",
8
- "gemini-cli",
9
- "skill",
10
- "sdlc",
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
- "agentic-sdlc-init": "scripts/init.js",
21
- "agentic-sdlc-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/agentic-sdlc-skill/SKILL.md",
29
- "skills/agentic-sdlc-skill/templates.md",
30
- "skills/agentic-sdlc-skill/architect.md",
31
- "skills/agentic-sdlc-skill/guides.md",
32
- "skills/agentic-sdlc-skill/vision.md",
33
- "skills/agentic-sdlc-skill/tdd.md",
34
- "skills/agentic-sdlc-skill/debugging.md",
35
- "skills/agentic-sdlc-skill/elicitation.md",
36
- "skills/agentic-sdlc-skill/review.md",
37
- "skills/agentic-sdlc-skill/dispatch.md",
38
- "skills/agentic-sdlc-skill/routing.md",
39
- "skills/agentic-sdlc-skill/ENFORCEMENT.md",
40
- "skills/agentic-sdlc-skill/scripts/sdlc_check.py",
41
- "skills/agentic-sdlc-skill/scripts/sdlc_core.py",
42
- "gemini-extension.json",
43
- "README.md",
44
- "CHANGELOG.md",
45
- "scripts/lib.js",
46
- "scripts/init.js",
47
- "scripts/postinstall.js",
48
- "scripts/preuninstall.js"
49
- ]
50
- }
1
+ {
2
+ "name": "@antoneeo/agentic-sdlc-skill",
3
+ "version": "1.30.0",
4
+ "description": "Documentation-First SDLC protocol for Claude Code, Gemini CLI, Google Antigravity and Codex with risk triage, Vision governance, installed support files and optional devPNT integration.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-skill",
8
+ "gemini-cli",
9
+ "skill",
10
+ "sdlc",
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
+ "agentic-sdlc-init": "scripts/init.js",
21
+ "agentic-sdlc-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/agentic-sdlc-skill/SKILL.md",
29
+ "skills/agentic-sdlc-skill/templates.md",
30
+ "skills/agentic-sdlc-skill/architect.md",
31
+ "skills/agentic-sdlc-skill/guides.md",
32
+ "skills/agentic-sdlc-skill/vision.md",
33
+ "skills/agentic-sdlc-skill/tdd.md",
34
+ "skills/agentic-sdlc-skill/debugging.md",
35
+ "skills/agentic-sdlc-skill/elicitation.md",
36
+ "skills/agentic-sdlc-skill/review.md",
37
+ "skills/agentic-sdlc-skill/dispatch.md",
38
+ "skills/agentic-sdlc-skill/routing.md",
39
+ "skills/agentic-sdlc-skill/ENFORCEMENT.md",
40
+ "skills/agentic-sdlc-skill/scripts/sdlc_check.py",
41
+ "skills/agentic-sdlc-skill/scripts/sdlc_core.py",
42
+ "gemini-extension.json",
43
+ "README.md",
44
+ "CHANGELOG.md",
45
+ "scripts/lib.js",
46
+ "scripts/init.js",
47
+ "scripts/postinstall.js",
48
+ "scripts/preuninstall.js"
49
+ ]
50
+ }
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
 
@@ -159,16 +159,8 @@ let protocolPreexisting = false;
159
159
  // Probed ONCE and reused by section 6c: re-filtering CLIENTS there would
160
160
  // run every client's detection a second time.
161
161
  const detectedClients = CLIENTS.filter(clientDetected);
162
- const PYTHON_CANDIDATES = ['python', 'python3', 'py'];
163
- const detectedPython = (() => {
164
- for (const py of PYTHON_CANDIDATES) {
165
- try {
166
- execSync(`${py} --version`, { stdio: 'ignore' });
167
- return py;
168
- } catch (e) { /* try the next interpreter */ }
169
- }
170
- return null;
171
- })();
162
+ // One probe, shared with postinstall's global wiring (F-042): lib owns it.
163
+ const detectedPython = detectPython();
172
164
 
173
165
  for (const client of detectedClients) {
174
166
  {
@@ -283,8 +275,21 @@ if (!claudeClient) {
283
275
  hybrid: hybridProject, docsLabel: 'ai_docs',
284
276
  });
285
277
  switch (r.code) {
278
+ case 'global':
279
+ // F-042: a resolving machine-global hook already orients every project.
280
+ console.log('⏭️ Covered by the machine-global orientation hook — no project-level');
281
+ console.log(` hook needed (${r.target}).`);
282
+ break;
286
283
  case 'wired':
287
284
  console.log(`🪝 Wired the SessionStart orientation hook in .claude/${r.file}.`);
285
+ if (r.globalNote === 'dead') {
286
+ console.log('⚠️ Note: a machine-global orientation hook exists but its validator');
287
+ console.log(' does not resolve — it emits nothing. Fix or remove it in your user');
288
+ console.log(' settings (ENFORCEMENT.md §4).');
289
+ } else if (r.globalNote === 'unverifiable') {
290
+ console.log('ℹ️ Note: your user settings carry an orientation hook this installer');
291
+ console.log(' cannot verify; the project-level hook above covers this project.');
292
+ }
288
293
  if (r.local) {
289
294
  console.log(' It names a path on THIS machine, so it went to the git-ignored');
290
295
  console.log(' file: each teammate runs init once to get their own.');
@@ -328,6 +333,12 @@ if (!claudeClient) {
328
333
  console.log(` hook command safely: ${r.validator}`);
329
334
  console.log(' Refusing to build one. Wire it by hand (ENFORCEMENT.md §4).');
330
335
  break;
336
+ default:
337
+ // A lens port missing a case must be loud, never a silent skip — the
338
+ // exact defect class the manual-snippet note below exists to prevent.
339
+ console.log(`ℹ️ Orientation hook: unhandled result '${r.code}'. Wire it by hand`);
340
+ console.log(' per ENFORCEMENT.md §4.');
341
+ break;
331
342
  }
332
343
  }
333
344
  for (const c of otherClients) {
package/scripts/lib.js CHANGED
@@ -383,6 +383,31 @@ function wireOrientHook(options) {
383
383
  : { code: 'broken', file: name, target: p, existing, command };
384
384
  }
385
385
 
386
+ // F-042: a resolving MACHINE-GLOBAL hook (user-level settings) already
387
+ // orients every project. Checked AFTER the project scan on purpose: a
388
+ // project-level broken/malformed/unverifiable must be reported first — a
389
+ // pre-check would mask a dead project hook behind "covered globally", the
390
+ // wired-and-DEAD state F-041 calls the worst of the three. A dead or
391
+ // unverifiable global hook falls through to the project write (the wrapper
392
+ // may be emitting nothing) and the caller gets `globalNote` to print the
393
+ // correction. A user-level token that is not absolute has no cwd to resolve
394
+ // against, so it is treated as unverifiable.
395
+ let globalNote;
396
+ {
397
+ const userTarget = path.join(client.home, 'settings.json');
398
+ const ustate = orientSettingsState(userTarget);
399
+ if (ustate.ok) {
400
+ const uexisting = findOrientHook(ustate.settings);
401
+ if (uexisting) {
402
+ const ufound = orientHookValidator(uexisting);
403
+ if (ufound !== null && path.isAbsolute(ufound) && fs.existsSync(ufound)) {
404
+ return { code: 'global', target: userTarget, command: uexisting };
405
+ }
406
+ globalNote = (ufound === null || !path.isAbsolute(ufound)) ? 'unverifiable' : 'dead';
407
+ }
408
+ }
409
+ }
410
+
386
411
  // A machine-specific command must not reach the shared, committed file.
387
412
  const file = vendored ? 'settings.json' : 'settings.local.json';
388
413
  const target = path.join(cwd, '.claude', file);
@@ -409,7 +434,209 @@ function wireOrientHook(options) {
409
434
  } catch (e) {
410
435
  return { code: 'write-failed', file, target, command, error: e.message };
411
436
  }
412
- return { code: 'wired', file, target, command, local: !vendored };
437
+ return { code: 'wired', file, target, command, local: !vendored, globalNote };
438
+ }
439
+
440
+ // --- F-042: the machine-global hook (install-time wiring) -------------------
441
+ // The F-041 field lesson: the check note asked a question a normal user cannot
442
+ // evaluate. The owner's ruling: the hook is wired by the act the user already
443
+ // performs — installing/updating the package — at the USER level, which the
444
+ // installer knows exactly. `orient` is fail-open (no docs root ⇒ silent), so a
445
+ // global hook is safe by construction on unmanaged projects. Removal is a
446
+ // STANDING opt-out: a per-target marker remembers every settings file ever
447
+ // wired, and no install or update ever re-adds a removed hook. Deleting the
448
+ // marker file is the documented re-enable gesture (ENFORCEMENT §4).
449
+
450
+ // Python detection, shared by init (project wiring + index) and postinstall
451
+ // (global wiring). One list, one probe.
452
+ const PYTHON_CANDIDATES = ['python', 'python3', 'py'];
453
+ function detectPython() {
454
+ for (const py of PYTHON_CANDIDATES) {
455
+ try {
456
+ execSync(`${py} --version`, { stdio: 'ignore' });
457
+ return py;
458
+ } catch (e) { /* try the next interpreter */ }
459
+ }
460
+ return null;
461
+ }
462
+
463
+ // Case/separator normalization uses the PLATFORM'S OWN path.relative: win32
464
+ // folds case and separators; forcing win32 semantics on POSIX would fold
465
+ // genuinely distinct paths (a false positive). One helper guards BOTH the
466
+ // uninstall attribution and the marker lookup — a naive prefix/string match
467
+ // misses `c:\` vs `C:\`, which on attribution re-manufactures the dead hook
468
+ // and on the marker re-wires against a standing opt-out.
469
+ function pathsEqual(a, b) {
470
+ try {
471
+ return path.relative(String(a), String(b)) === '';
472
+ } catch (e) {
473
+ return false;
474
+ }
475
+ }
476
+ function pathInside(child, parent) {
477
+ try {
478
+ const rel = path.relative(String(parent), String(child));
479
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
480
+ } catch (e) {
481
+ return false;
482
+ }
483
+ }
484
+
485
+ // The marker lives under the family's agent-global root (the same root the
486
+ // validator's AGENTIC_SDLC_KB_ROOT seam names), read at call time so the test
487
+ // battery's stubbed env applies. Every helper is fail-open: a broken marker
488
+ // must never break an install, and a failed marker write never undoes a wire.
489
+ function orientMarkerPath() {
490
+ const root = process.env.AGENTIC_SDLC_KB_ROOT
491
+ || path.join(os.homedir(), '.agentic-sdlc');
492
+ return path.join(root, 'orient-hook-wired');
493
+ }
494
+ function orientMarkerTargets() {
495
+ try {
496
+ return fs.readFileSync(orientMarkerPath(), 'utf8')
497
+ .split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
498
+ } catch (e) {
499
+ return [];
500
+ }
501
+ }
502
+ function orientMarkerHas(target) {
503
+ return orientMarkerTargets().some((t) => pathsEqual(t, target));
504
+ }
505
+ function orientMarkerAdd(target) {
506
+ try {
507
+ if (orientMarkerHas(target)) return;
508
+ fs.mkdirSync(path.dirname(orientMarkerPath()), { recursive: true });
509
+ fs.appendFileSync(orientMarkerPath(), target + String.fromCharCode(10), 'utf8');
510
+ } catch (e) { /* fail-open */ }
511
+ }
512
+ function orientMarkerRemove(target) {
513
+ try {
514
+ const rest = orientMarkerTargets().filter((t) => !pathsEqual(t, target));
515
+ if (rest.length) {
516
+ fs.writeFileSync(orientMarkerPath(), rest.join(String.fromCharCode(10)) + String.fromCharCode(10), 'utf8');
517
+ } else {
518
+ fs.rmSync(orientMarkerPath(), { force: true });
519
+ }
520
+ } catch (e) { /* fail-open */ }
521
+ }
522
+
523
+ // Wire the machine-global hook into client.home/settings.json — exactly ONE
524
+ // file (user level has no local split; the path is machine-local by nature).
525
+ // Codes follow the wireOrientHook contract, plus `opted-out`.
526
+ function wireGlobalOrientHook(options) {
527
+ const client = options.client;
528
+ const python = options.python;
529
+
530
+ const validator = path.join(skillTarget(client), 'scripts', ORIENT_ENTRY_POINT);
531
+ if (!fs.existsSync(validator)) return { code: 'no-validator', validator };
532
+ if (!python) return { code: 'no-python', validator };
533
+ if (ORIENT_UNSAFE_IN_PATH.test(validator)) return { code: 'unsafe-path', validator };
534
+ const command = orientHookCommand(python, validator, false);
535
+
536
+ const target = path.join(client.home, 'settings.json');
537
+ const state = orientSettingsState(target);
538
+ if (!state.ok) return { code: 'malformed', target, why: state.why, command };
539
+
540
+ const existing = findOrientHook(state.settings);
541
+ if (existing) {
542
+ const found = orientHookValidator(existing);
543
+ if (found === null || !path.isAbsolute(found)) {
544
+ // No cwd exists at user level, so an unresolvable token is "cannot
545
+ // tell" — left alone, never remembered as ours.
546
+ return { code: 'unverifiable', target, existing, command };
547
+ }
548
+ if (fs.existsSync(found)) {
549
+ // A hand-wired live hook, once removed, deserves the same respect as
550
+ // one this installer wrote: remember the target.
551
+ orientMarkerAdd(target);
552
+ return { code: 'already', target, command: existing };
553
+ }
554
+ return { code: 'broken', target, existing, command };
555
+ }
556
+
557
+ if (orientMarkerHas(target)) return { code: 'opted-out', target };
558
+
559
+ const settings = state.settings;
560
+ if (!settings.hooks) settings.hooks = {};
561
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
562
+ settings.hooks.SessionStart.push({
563
+ hooks: [{
564
+ type: 'command',
565
+ command,
566
+ timeout: ORIENT_HOOK_TIMEOUT,
567
+ statusMessage: 'Loading docs orientation...',
568
+ }],
569
+ });
570
+ try {
571
+ fs.mkdirSync(path.dirname(target), { recursive: true });
572
+ fs.writeFileSync(target, JSON.stringify(settings, null, 2) + String.fromCharCode(10), 'utf8');
573
+ } catch (e) {
574
+ return { code: 'write-failed', target, command, error: e.message };
575
+ }
576
+ orientMarkerAdd(target);
577
+ return { code: 'wired', target, command };
578
+ }
579
+
580
+ // Uninstall symmetry: removing the skill dirs would leave any global hook
581
+ // naming them wired-and-DEAD. So, across EVERY settings target the marker
582
+ // remembers (a hook wired under CLAUDE_CONFIG_DIR=/x must be cleaned from a
583
+ // shell without that env) unioned with the current client.home, remove ONLY
584
+ // the SessionStart entries whose validator token resolves INTO a root being
585
+ // removed. Foreign or unverifiable entries are never touched. The marker line
586
+ // is cleared ONLY when an entry was actually removed — when the user had
587
+ // already removed the hook, the line stays and the opt-out survives uninstall,
588
+ // reinstall, and sibling-lens uninstalls.
589
+ function removeGlobalOrientHooks(removedRoots) {
590
+ const targets = [];
591
+ const claude = CLIENTS.find((c) => c.key === 'claude');
592
+ if (claude) targets.push(path.join(claude.home, 'settings.json'));
593
+ for (const t of orientMarkerTargets()) {
594
+ if (!targets.some((x) => pathsEqual(x, t))) targets.push(t);
595
+ }
596
+ const removedFrom = [];
597
+ for (const target of targets) {
598
+ try {
599
+ const state = orientSettingsState(target);
600
+ if (!state.ok || !fs.existsSync(target)) continue;
601
+ const hooks = state.settings.hooks;
602
+ if (!hooks || !Array.isArray(hooks.SessionStart)) continue;
603
+ let removedHere = false;
604
+ const ours = (h) => {
605
+ const cmd = h && typeof h.command === 'string' ? h.command : '';
606
+ if (!(ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && / orient(\s|$)/.test(cmd))) return false;
607
+ const tok = orientHookValidator(cmd);
608
+ if (tok === null || !path.isAbsolute(tok)) return false; // cannot attribute: keep
609
+ return removedRoots.some((root) => pathInside(tok, root));
610
+ };
611
+ const kept = [];
612
+ for (const group of hooks.SessionStart) {
613
+ if (!group || typeof group !== 'object') { kept.push(group); continue; }
614
+ if (Array.isArray(group.hooks)) {
615
+ const inner = group.hooks.filter((h) => {
616
+ if (ours(h)) { removedHere = true; return false; }
617
+ return true;
618
+ });
619
+ if (inner.length) kept.push({ ...group, hooks: inner });
620
+ else if (inner.length !== group.hooks.length) { /* emptied: prune */ }
621
+ else kept.push(group);
622
+ } else if (ours(group)) {
623
+ removedHere = true; // direct-object shape: prune
624
+ } else {
625
+ kept.push(group);
626
+ }
627
+ }
628
+ if (!removedHere) continue;
629
+ state.settings.hooks.SessionStart = kept;
630
+ if (!kept.length) delete state.settings.hooks.SessionStart;
631
+ if (state.settings.hooks && !Object.keys(state.settings.hooks).length) {
632
+ delete state.settings.hooks;
633
+ }
634
+ fs.writeFileSync(target, JSON.stringify(state.settings, null, 2) + String.fromCharCode(10), 'utf8');
635
+ orientMarkerRemove(target);
636
+ removedFrom.push(target);
637
+ } catch (e) { /* fail-open per target: never abort an uninstall */ }
638
+ }
639
+ return removedFrom;
413
640
  }
414
641
 
415
642
  module.exports = {
@@ -422,6 +649,10 @@ module.exports = {
422
649
  clientDetected,
423
650
  skillTarget,
424
651
  wireOrientHook,
652
+ wireGlobalOrientHook,
653
+ removeGlobalOrientHooks,
654
+ detectPython,
655
+ pathsEqual,
425
656
  orientHookCommand,
426
657
  copyRecursive,
427
658
  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);
@@ -79,6 +79,37 @@ that cannot be placed in a command safely, or a settings file it cannot write. `
79
79
  prints which case applied, per client — a silent skip is what let "documented default
80
80
  that nobody installs" survive in the first place.
81
81
 
82
+ **Older and agent-bootstrapped projects are silently unwired -- `check` now tells you (F-041).**
83
+ `init` wires the hook at init time only: a project initialized before F-036, or whose
84
+ docs root was bootstrapped by the agent (which runs `index`, never `init`), never
85
+ received it retroactively -- and until F-041 nothing said so. `check` now prints, right after its summary line, a
86
+ one-line `[note]` when no orientation hook is detectable (`.claude/settings.json`,
87
+ `.claude/settings.local.json`, `.codex/hooks.json`), and a DISTINCT note when a hook is
88
+ wired but the validator it names does not resolve (the wired-and-DEAD state below --
89
+ silence there would bless the worst of the three states). The notes are informational
90
+ only: never the exit code, never a `validate` warning -- CI (`validate --strict`) does
91
+ not see them, because the wired hook legitimately lives in the git-ignored
92
+ `settings.local.json`. Known residuals, accepted as honest: a client with no hook
93
+ mechanism (Gemini today) keeps the note -- read it as the manual-reads reminder; a
94
+ `.codex/hooks.json` wired in a shape other than the documented one keeps it too.
95
+
96
+ **Installing the package wires the hook machine-wide (F-042).** On Claude Code
97
+ with Python, `npm i -g` (and every update) wires a SessionStart orient hook into
98
+ the USER-level settings (`$CLAUDE_CONFIG_DIR/settings.json` when set, else
99
+ `~/.claude/settings.json`), covering every project on the machine -- `orient` is
100
+ fail-open, so projects without a docs root see nothing. The install itself is
101
+ the consent: no question is asked. **Removing that hook entry is a standing
102
+ opt-out**: a per-target marker (`<agentic-sdlc root>/orient-hook-wired`,
103
+ default `~/.agentic-sdlc/`) remembers each settings file ever wired, and no
104
+ install or update ever re-adds a removed hook; deleting the marker file is the
105
+ re-enable gesture. `npm uninstall` removes the entries it can attribute to the
106
+ removed skill (and only those), so no hook is left pointing at deleted files.
107
+ Notes: the global command carries no `--hybrid` (a machine-global hook cannot
108
+ know per-project mode -- on Hybrid projects only the devPNT pointer line is
109
+ absent); a project wired per-project AND globally prints its orientation twice
110
+ -- remove one of the two entries; `init` reports "covered by the machine-global
111
+ hook" instead of wiring a project-level duplicate.
112
+
82
113
  **Which settings file, and why it is not always the shared one.** The command names a
83
114
  validator, and where that validator lives decides where the hook may be written:
84
115