@antoneeo/mkt-agentic-sdlc-skill 0.6.0 → 0.8.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
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0] - 2026-09-01
4
+
5
+ Spine-only release: `review.md` updated byte-identical across the family
6
+ (authored in the code lens, F-045 / code 1.30.0).
7
+
8
+ ### Added
9
+ - **Review-loop convergence (shared spine `review.md`).** *Revise means
10
+ converge, not accrete*: lean rewrite at the latest before round 3, revision
11
+ archaeology lives in REVIEW_LOG, never in the artifact. *Severity contract*
12
+ stated with every review request: blocker = unfit-to-proceed evidence only,
13
+ wishes are WARN at most. *Scripted author pre-audit*: resolve every anchor
14
+ and recount every count before requesting. The new *behavioural-claim-probes*
15
+ reviewer clause is lens-gated and self-disarmed in this lens (this SKILL.md
16
+ defines no Execute-Before-Specify duty).
17
+
18
+ ## [0.7.0] - 2026-08-28
19
+
20
+ Spine + overlay delta parked for the next release of this lens (shipped first
21
+ in kb; the spine stays byte-identical x3 in the repo while packages publish
22
+ independently). In this lens the note is re-attached by the overlay
23
+ (`mkt_check.py`, after its own check verdict), because the overlay replaces
24
+ the spine's check pipeline entirely.
25
+
26
+ ### Added
27
+ - **F-041: `check` notes an unwired or dead session-orientation hook** (one
28
+ informational `[note]` line -- never the exit code, never a `validate` warning,
29
+ so CI is unaffected). `init` wires the hook only at init time; projects
30
+ initialized before F-036 or bootstrapped without `init` were silently unwired.
31
+
32
+ ### Added (F-042, parked with the above)
33
+ - Install/update wires the session-orientation hook machine-wide (Claude Code
34
+ with Python; user-level settings; removal is a standing per-target opt-out;
35
+ uninstall surgically removes what it wired). The check note is global-aware
36
+ and reworded in consequence-first user language.
37
+
38
+ ### Added (F-043 port, with the above)
39
+ - **The revision sentence.** Updating an existing strategy or campaign doc (the E2 row) is a REWRITE, not an
40
+ append: full re-read, body rewritten to current state -- never an appended
41
+ delta section. Ported from the kb lens's `## Revision` doctrine (kb 1.10.0),
42
+ born from a field post-mortem where delta-append revisions left stale
43
+ sentences standing as printed truth.
44
+
3
45
  ## [0.6.0] - 2026-08-26
4
46
 
5
47
  F-038 — the gated rung: truthful log vocabulary and a mandated ask, no grant memory.
package/README.md CHANGED
@@ -69,6 +69,10 @@ The workstream registry (`audit/handoff.md`) is **generated** from one file per
69
69
  - **Standalone** — everything on the filesystem under `mkt_docs/`.
70
70
  - **Hybrid with devPNT** — plans and strategy artifacts governed as versioned devPNT documents (MKT-VISION, ICP/Personas, Threat Map, Strategy, Tactical Plan, Measurement Plan), with proposal/approval workflow and independent review gates.
71
71
 
72
+ Installing or updating the npm package wires the session-orientation hook
73
+ machine-wide (user-level Claude Code settings; removal is a standing opt-out
74
+ that no update overrides -- ENFORCEMENT.md par.4).
75
+
72
76
  ## The family: three lenses, one spine
73
77
 
74
78
  | Package | Faithful to | Unit of work |
@@ -1,6 +1,6 @@
1
- {
2
- "name": "mkt-agentic-sdlc",
3
- "version": "0.6.0",
4
- "description": "Evidence-First marketing planning protocol with engagement triage, SOSTAC workflow, evidence ledger and optional devPNT integration.",
5
- "author": "Antonio Pinto (https://github.com/Antoneeo)"
6
- }
1
+ {
2
+ "name": "mkt-agentic-sdlc",
3
+ "version": "0.8.0",
4
+ "description": "Evidence-First marketing planning protocol with engagement triage, SOSTAC workflow, evidence ledger 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/mkt-agentic-sdlc-skill",
3
- "version": "0.6.0",
4
- "description": "Evidence-First marketing planning protocol for Claude Code, Gemini CLI, Google Antigravity and Codex: engagement triage, SOSTAC workflow, evidence ledger, mechanical validator and optional devPNT integration.",
5
- "keywords": [
6
- "claude-code",
7
- "claude-skill",
8
- "gemini-cli",
9
- "skill",
10
- "marketing",
11
- "marketing-plan",
12
- "sostac",
13
- "ai-agent"
14
- ],
15
- "author": "Antonio Pinto (https://github.com/Antoneeo)",
16
- "license": "MIT",
17
- "publishConfig": {
18
- "access": "public"
19
- },
20
- "bin": {
21
- "mkt-sdlc-init": "scripts/init.js",
22
- "mkt-sdlc-install-skill": "scripts/postinstall.js"
23
- },
24
- "scripts": {
25
- "postinstall": "node scripts/postinstall.js",
26
- "preuninstall": "node scripts/preuninstall.js"
27
- },
28
- "files": [
29
- "skills/mkt-agentic-sdlc/SKILL.md",
30
- "skills/mkt-agentic-sdlc/frameworks.md",
31
- "skills/mkt-agentic-sdlc/elicitation.md",
32
- "skills/mkt-agentic-sdlc/research.md",
33
- "skills/mkt-agentic-sdlc/templates.md",
34
- "skills/mkt-agentic-sdlc/review.md",
35
- "skills/mkt-agentic-sdlc/scripts/sdlc_core.py",
36
- "skills/mkt-agentic-sdlc/routing.md",
37
- "skills/mkt-agentic-sdlc/guides.md",
38
- "skills/mkt-agentic-sdlc/vision.md",
39
- "skills/mkt-agentic-sdlc/dispatch.md",
40
- "skills/mkt-agentic-sdlc/ENFORCEMENT.md",
41
- "skills/mkt-agentic-sdlc/scripts/mkt_check.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/mkt-agentic-sdlc-skill",
3
+ "version": "0.8.0",
4
+ "description": "Evidence-First marketing planning protocol for Claude Code, Gemini CLI, Google Antigravity and Codex: engagement triage, SOSTAC workflow, evidence ledger, mechanical validator and optional devPNT integration.",
5
+ "keywords": [
6
+ "claude-code",
7
+ "claude-skill",
8
+ "gemini-cli",
9
+ "skill",
10
+ "marketing",
11
+ "marketing-plan",
12
+ "sostac",
13
+ "ai-agent"
14
+ ],
15
+ "author": "Antonio Pinto (https://github.com/Antoneeo)",
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "bin": {
21
+ "mkt-sdlc-init": "scripts/init.js",
22
+ "mkt-sdlc-install-skill": "scripts/postinstall.js"
23
+ },
24
+ "scripts": {
25
+ "postinstall": "node scripts/postinstall.js",
26
+ "preuninstall": "node scripts/preuninstall.js"
27
+ },
28
+ "files": [
29
+ "skills/mkt-agentic-sdlc/SKILL.md",
30
+ "skills/mkt-agentic-sdlc/frameworks.md",
31
+ "skills/mkt-agentic-sdlc/elicitation.md",
32
+ "skills/mkt-agentic-sdlc/research.md",
33
+ "skills/mkt-agentic-sdlc/templates.md",
34
+ "skills/mkt-agentic-sdlc/review.md",
35
+ "skills/mkt-agentic-sdlc/scripts/sdlc_core.py",
36
+ "skills/mkt-agentic-sdlc/routing.md",
37
+ "skills/mkt-agentic-sdlc/guides.md",
38
+ "skills/mkt-agentic-sdlc/vision.md",
39
+ "skills/mkt-agentic-sdlc/dispatch.md",
40
+ "skills/mkt-agentic-sdlc/ENFORCEMENT.md",
41
+ "skills/mkt-agentic-sdlc/scripts/mkt_check.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
 
@@ -150,16 +150,8 @@ let protocolPreexisting = false;
150
150
  // Probed ONCE and reused by section 6c: re-filtering CLIENTS there would
151
151
  // run every client's detection a second time.
152
152
  const detectedClients = CLIENTS.filter(clientDetected);
153
- const PYTHON_CANDIDATES = ['python', 'python3', 'py'];
154
- const detectedPython = (() => {
155
- for (const py of PYTHON_CANDIDATES) {
156
- try {
157
- execSync(`${py} --version`, { stdio: 'ignore' });
158
- return py;
159
- } catch (e) { /* try the next interpreter */ }
160
- }
161
- return null;
162
- })();
153
+ // One probe, shared with postinstall's global wiring (F-042): lib owns it.
154
+ const detectedPython = detectPython();
163
155
 
164
156
  for (const client of detectedClients) {
165
157
  {
@@ -273,8 +265,21 @@ if (!claudeClient) {
273
265
  hybrid: hybridProject, docsLabel: 'mkt_docs',
274
266
  });
275
267
  switch (r.code) {
268
+ case 'global':
269
+ // F-042: a resolving machine-global hook already orients every project.
270
+ console.log('⏭️ Covered by the machine-global orientation hook — no project-level');
271
+ console.log(` hook needed (${r.target}).`);
272
+ break;
276
273
  case 'wired':
277
274
  console.log(`🪝 Wired the SessionStart orientation hook in .claude/${r.file}.`);
275
+ if (r.globalNote === 'dead') {
276
+ console.log('⚠️ Note: a machine-global orientation hook exists but its validator');
277
+ console.log(' does not resolve — it emits nothing. Fix or remove it in your user');
278
+ console.log(' settings (ENFORCEMENT.md §4).');
279
+ } else if (r.globalNote === 'unverifiable') {
280
+ console.log('ℹ️ Note: your user settings carry an orientation hook this installer');
281
+ console.log(' cannot verify; the project-level hook above covers this project.');
282
+ }
278
283
  if (r.local) {
279
284
  console.log(' It names a path on THIS machine, so it went to the git-ignored');
280
285
  console.log(' file: each teammate runs init once to get their own.');
@@ -318,6 +323,12 @@ if (!claudeClient) {
318
323
  console.log(` hook command safely: ${r.validator}`);
319
324
  console.log(' Refusing to build one. Wire it by hand (ENFORCEMENT.md §4).');
320
325
  break;
326
+ default:
327
+ // A lens port missing a case must be loud, never a silent skip — the
328
+ // exact defect class the manual-snippet note below exists to prevent.
329
+ console.log(`ℹ️ Orientation hook: unhandled result '${r.code}'. Wire it by hand`);
330
+ console.log(' per ENFORCEMENT.md §4.');
331
+ break;
321
332
  }
322
333
  }
323
334
  for (const c of otherClients) {
package/scripts/lib.js CHANGED
@@ -389,6 +389,31 @@ function wireOrientHook(options) {
389
389
  : { code: 'broken', file: name, target: p, existing, command };
390
390
  }
391
391
 
392
+ // F-042: a resolving MACHINE-GLOBAL hook (user-level settings) already
393
+ // orients every project. Checked AFTER the project scan on purpose: a
394
+ // project-level broken/malformed/unverifiable must be reported first — a
395
+ // pre-check would mask a dead project hook behind "covered globally", the
396
+ // wired-and-DEAD state F-041 calls the worst of the three. A dead or
397
+ // unverifiable global hook falls through to the project write (the wrapper
398
+ // may be emitting nothing) and the caller gets `globalNote` to print the
399
+ // correction. A user-level token that is not absolute has no cwd to resolve
400
+ // against, so it is treated as unverifiable.
401
+ let globalNote;
402
+ {
403
+ const userTarget = path.join(client.home, 'settings.json');
404
+ const ustate = orientSettingsState(userTarget);
405
+ if (ustate.ok) {
406
+ const uexisting = findOrientHook(ustate.settings);
407
+ if (uexisting) {
408
+ const ufound = orientHookValidator(uexisting);
409
+ if (ufound !== null && path.isAbsolute(ufound) && fs.existsSync(ufound)) {
410
+ return { code: 'global', target: userTarget, command: uexisting };
411
+ }
412
+ globalNote = (ufound === null || !path.isAbsolute(ufound)) ? 'unverifiable' : 'dead';
413
+ }
414
+ }
415
+ }
416
+
392
417
  // A machine-specific command must not reach the shared, committed file.
393
418
  const file = vendored ? 'settings.json' : 'settings.local.json';
394
419
  const target = path.join(cwd, '.claude', file);
@@ -415,7 +440,209 @@ function wireOrientHook(options) {
415
440
  } catch (e) {
416
441
  return { code: 'write-failed', file, target, command, error: e.message };
417
442
  }
418
- return { code: 'wired', file, target, command, local: !vendored };
443
+ return { code: 'wired', file, target, command, local: !vendored, globalNote };
444
+ }
445
+
446
+ // --- F-042: the machine-global hook (install-time wiring) -------------------
447
+ // The F-041 field lesson: the check note asked a question a normal user cannot
448
+ // evaluate. The owner's ruling: the hook is wired by the act the user already
449
+ // performs — installing/updating the package — at the USER level, which the
450
+ // installer knows exactly. `orient` is fail-open (no docs root ⇒ silent), so a
451
+ // global hook is safe by construction on unmanaged projects. Removal is a
452
+ // STANDING opt-out: a per-target marker remembers every settings file ever
453
+ // wired, and no install or update ever re-adds a removed hook. Deleting the
454
+ // marker file is the documented re-enable gesture (ENFORCEMENT §4).
455
+
456
+ // Python detection, shared by init (project wiring + index) and postinstall
457
+ // (global wiring). One list, one probe.
458
+ const PYTHON_CANDIDATES = ['python', 'python3', 'py'];
459
+ function detectPython() {
460
+ for (const py of PYTHON_CANDIDATES) {
461
+ try {
462
+ execSync(`${py} --version`, { stdio: 'ignore' });
463
+ return py;
464
+ } catch (e) { /* try the next interpreter */ }
465
+ }
466
+ return null;
467
+ }
468
+
469
+ // Case/separator normalization uses the PLATFORM'S OWN path.relative: win32
470
+ // folds case and separators; forcing win32 semantics on POSIX would fold
471
+ // genuinely distinct paths (a false positive). One helper guards BOTH the
472
+ // uninstall attribution and the marker lookup — a naive prefix/string match
473
+ // misses `c:\` vs `C:\`, which on attribution re-manufactures the dead hook
474
+ // and on the marker re-wires against a standing opt-out.
475
+ function pathsEqual(a, b) {
476
+ try {
477
+ return path.relative(String(a), String(b)) === '';
478
+ } catch (e) {
479
+ return false;
480
+ }
481
+ }
482
+ function pathInside(child, parent) {
483
+ try {
484
+ const rel = path.relative(String(parent), String(child));
485
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
486
+ } catch (e) {
487
+ return false;
488
+ }
489
+ }
490
+
491
+ // The marker lives under the family's agent-global root (the same root the
492
+ // validator's AGENTIC_SDLC_KB_ROOT seam names), read at call time so the test
493
+ // battery's stubbed env applies. Every helper is fail-open: a broken marker
494
+ // must never break an install, and a failed marker write never undoes a wire.
495
+ function orientMarkerPath() {
496
+ const root = process.env.AGENTIC_SDLC_KB_ROOT
497
+ || path.join(os.homedir(), '.agentic-sdlc');
498
+ return path.join(root, 'orient-hook-wired');
499
+ }
500
+ function orientMarkerTargets() {
501
+ try {
502
+ return fs.readFileSync(orientMarkerPath(), 'utf8')
503
+ .split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
504
+ } catch (e) {
505
+ return [];
506
+ }
507
+ }
508
+ function orientMarkerHas(target) {
509
+ return orientMarkerTargets().some((t) => pathsEqual(t, target));
510
+ }
511
+ function orientMarkerAdd(target) {
512
+ try {
513
+ if (orientMarkerHas(target)) return;
514
+ fs.mkdirSync(path.dirname(orientMarkerPath()), { recursive: true });
515
+ fs.appendFileSync(orientMarkerPath(), target + String.fromCharCode(10), 'utf8');
516
+ } catch (e) { /* fail-open */ }
517
+ }
518
+ function orientMarkerRemove(target) {
519
+ try {
520
+ const rest = orientMarkerTargets().filter((t) => !pathsEqual(t, target));
521
+ if (rest.length) {
522
+ fs.writeFileSync(orientMarkerPath(), rest.join(String.fromCharCode(10)) + String.fromCharCode(10), 'utf8');
523
+ } else {
524
+ fs.rmSync(orientMarkerPath(), { force: true });
525
+ }
526
+ } catch (e) { /* fail-open */ }
527
+ }
528
+
529
+ // Wire the machine-global hook into client.home/settings.json — exactly ONE
530
+ // file (user level has no local split; the path is machine-local by nature).
531
+ // Codes follow the wireOrientHook contract, plus `opted-out`.
532
+ function wireGlobalOrientHook(options) {
533
+ const client = options.client;
534
+ const python = options.python;
535
+
536
+ const validator = path.join(skillTarget(client), 'scripts', ORIENT_ENTRY_POINT);
537
+ if (!fs.existsSync(validator)) return { code: 'no-validator', validator };
538
+ if (!python) return { code: 'no-python', validator };
539
+ if (ORIENT_UNSAFE_IN_PATH.test(validator)) return { code: 'unsafe-path', validator };
540
+ const command = orientHookCommand(python, validator, false);
541
+
542
+ const target = path.join(client.home, 'settings.json');
543
+ const state = orientSettingsState(target);
544
+ if (!state.ok) return { code: 'malformed', target, why: state.why, command };
545
+
546
+ const existing = findOrientHook(state.settings);
547
+ if (existing) {
548
+ const found = orientHookValidator(existing);
549
+ if (found === null || !path.isAbsolute(found)) {
550
+ // No cwd exists at user level, so an unresolvable token is "cannot
551
+ // tell" — left alone, never remembered as ours.
552
+ return { code: 'unverifiable', target, existing, command };
553
+ }
554
+ if (fs.existsSync(found)) {
555
+ // A hand-wired live hook, once removed, deserves the same respect as
556
+ // one this installer wrote: remember the target.
557
+ orientMarkerAdd(target);
558
+ return { code: 'already', target, command: existing };
559
+ }
560
+ return { code: 'broken', target, existing, command };
561
+ }
562
+
563
+ if (orientMarkerHas(target)) return { code: 'opted-out', target };
564
+
565
+ const settings = state.settings;
566
+ if (!settings.hooks) settings.hooks = {};
567
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
568
+ settings.hooks.SessionStart.push({
569
+ hooks: [{
570
+ type: 'command',
571
+ command,
572
+ timeout: ORIENT_HOOK_TIMEOUT,
573
+ statusMessage: 'Loading docs orientation...',
574
+ }],
575
+ });
576
+ try {
577
+ fs.mkdirSync(path.dirname(target), { recursive: true });
578
+ fs.writeFileSync(target, JSON.stringify(settings, null, 2) + String.fromCharCode(10), 'utf8');
579
+ } catch (e) {
580
+ return { code: 'write-failed', target, command, error: e.message };
581
+ }
582
+ orientMarkerAdd(target);
583
+ return { code: 'wired', target, command };
584
+ }
585
+
586
+ // Uninstall symmetry: removing the skill dirs would leave any global hook
587
+ // naming them wired-and-DEAD. So, across EVERY settings target the marker
588
+ // remembers (a hook wired under CLAUDE_CONFIG_DIR=/x must be cleaned from a
589
+ // shell without that env) unioned with the current client.home, remove ONLY
590
+ // the SessionStart entries whose validator token resolves INTO a root being
591
+ // removed. Foreign or unverifiable entries are never touched. The marker line
592
+ // is cleared ONLY when an entry was actually removed — when the user had
593
+ // already removed the hook, the line stays and the opt-out survives uninstall,
594
+ // reinstall, and sibling-lens uninstalls.
595
+ function removeGlobalOrientHooks(removedRoots) {
596
+ const targets = [];
597
+ const claude = CLIENTS.find((c) => c.key === 'claude');
598
+ if (claude) targets.push(path.join(claude.home, 'settings.json'));
599
+ for (const t of orientMarkerTargets()) {
600
+ if (!targets.some((x) => pathsEqual(x, t))) targets.push(t);
601
+ }
602
+ const removedFrom = [];
603
+ for (const target of targets) {
604
+ try {
605
+ const state = orientSettingsState(target);
606
+ if (!state.ok || !fs.existsSync(target)) continue;
607
+ const hooks = state.settings.hooks;
608
+ if (!hooks || !Array.isArray(hooks.SessionStart)) continue;
609
+ let removedHere = false;
610
+ const ours = (h) => {
611
+ const cmd = h && typeof h.command === 'string' ? h.command : '';
612
+ if (!(ORIENT_ENTRY_POINTS.some((n) => cmd.includes(n)) && / orient(\s|$)/.test(cmd))) return false;
613
+ const tok = orientHookValidator(cmd);
614
+ if (tok === null || !path.isAbsolute(tok)) return false; // cannot attribute: keep
615
+ return removedRoots.some((root) => pathInside(tok, root));
616
+ };
617
+ const kept = [];
618
+ for (const group of hooks.SessionStart) {
619
+ if (!group || typeof group !== 'object') { kept.push(group); continue; }
620
+ if (Array.isArray(group.hooks)) {
621
+ const inner = group.hooks.filter((h) => {
622
+ if (ours(h)) { removedHere = true; return false; }
623
+ return true;
624
+ });
625
+ if (inner.length) kept.push({ ...group, hooks: inner });
626
+ else if (inner.length !== group.hooks.length) { /* emptied: prune */ }
627
+ else kept.push(group);
628
+ } else if (ours(group)) {
629
+ removedHere = true; // direct-object shape: prune
630
+ } else {
631
+ kept.push(group);
632
+ }
633
+ }
634
+ if (!removedHere) continue;
635
+ state.settings.hooks.SessionStart = kept;
636
+ if (!kept.length) delete state.settings.hooks.SessionStart;
637
+ if (state.settings.hooks && !Object.keys(state.settings.hooks).length) {
638
+ delete state.settings.hooks;
639
+ }
640
+ fs.writeFileSync(target, JSON.stringify(state.settings, null, 2) + String.fromCharCode(10), 'utf8');
641
+ orientMarkerRemove(target);
642
+ removedFrom.push(target);
643
+ } catch (e) { /* fail-open per target: never abort an uninstall */ }
644
+ }
645
+ return removedFrom;
419
646
  }
420
647
 
421
648
  module.exports = {
@@ -428,6 +655,10 @@ module.exports = {
428
655
  clientDetected,
429
656
  skillTarget,
430
657
  wireOrientHook,
658
+ wireGlobalOrientHook,
659
+ removeGlobalOrientHooks,
660
+ detectPython,
661
+ pathsEqual,
431
662
  orientHookCommand,
432
663
  copyRecursive,
433
664
  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