@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 +42 -0
- package/README.md +4 -0
- package/gemini-extension.json +6 -6
- package/package.json +50 -50
- package/scripts/init.js +22 -11
- package/scripts/lib.js +232 -1
- package/scripts/postinstall.js +34 -2
- package/scripts/preuninstall.js +14 -1
- package/skills/mkt-agentic-sdlc/ENFORCEMENT.md +31 -0
- package/skills/mkt-agentic-sdlc/SKILL.md +240 -240
- package/skills/mkt-agentic-sdlc/review.md +51 -0
- package/skills/mkt-agentic-sdlc/scripts/mkt_check.py +5 -0
- package/skills/mkt-agentic-sdlc/scripts/sdlc_core.py +138 -0
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 |
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mkt-agentic-sdlc",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
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,
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const {
|
|
4
|
+
const {
|
|
5
|
+
SKILL_SOURCE, CLIENTS, clientDetected, skillTarget, copyRecursive,
|
|
6
|
+
wireGlobalOrientHook, detectPython,
|
|
7
|
+
} = require('./lib');
|
|
5
8
|
|
|
6
9
|
function installSkill(client) {
|
|
7
10
|
if (!fs.existsSync(SKILL_SOURCE)) {
|
|
@@ -29,7 +32,36 @@ for (const client of CLIENTS) {
|
|
|
29
32
|
if (clientDetected(client)) {
|
|
30
33
|
console.log(`✅ Detected: ${client.label}`);
|
|
31
34
|
detected = true;
|
|
32
|
-
installSkill(client);
|
|
35
|
+
const installed = installSkill(client);
|
|
36
|
+
// F-042: wire the machine-global orientation hook — the install IS the
|
|
37
|
+
// consent, so no question is ever asked. Claude Code only (the one client
|
|
38
|
+
// whose hook shape a fixture pins); removal is a standing opt-out the
|
|
39
|
+
// marker remembers. A convenience must never fail an npm install: wrapped.
|
|
40
|
+
if (client.key === 'claude' && installed) {
|
|
41
|
+
try {
|
|
42
|
+
const r = wireGlobalOrientHook({ client, python: detectPython() });
|
|
43
|
+
if (r.code === 'wired') {
|
|
44
|
+
console.log('🔗 Session orientation wired for every project on this machine');
|
|
45
|
+
console.log(` (${r.target}).`);
|
|
46
|
+
console.log(' Agents now start oriented on any project with a docs root; projects');
|
|
47
|
+
console.log(' without one see nothing. To opt out, remove that hook entry — it');
|
|
48
|
+
console.log(' will not be re-added.');
|
|
49
|
+
} else if (r.code === 'broken') {
|
|
50
|
+
console.log('⚠️ A session-orientation hook exists but the validator it names does');
|
|
51
|
+
console.log(' not resolve, so it has been emitting nothing. Correct its command to:');
|
|
52
|
+
console.log(` ${r.command}`);
|
|
53
|
+
} else if (r.code === 'no-python') {
|
|
54
|
+
console.log('ℹ️ Python not found: session orientation not wired (wire it later per');
|
|
55
|
+
console.log(' ENFORCEMENT.md §4).');
|
|
56
|
+
} else if (r.code === 'malformed' || r.code === 'write-failed'
|
|
57
|
+
|| r.code === 'unsafe-path') {
|
|
58
|
+
console.log(`⚠️ Session orientation not wired (${r.code}). Wire it by hand per`);
|
|
59
|
+
console.log(' ENFORCEMENT.md §4.');
|
|
60
|
+
}
|
|
61
|
+
// already / opted-out / unverifiable / no-validator: silent — an
|
|
62
|
+
// install must not nag (the opt-out is the user's standing choice).
|
|
63
|
+
} catch (e) { /* never fail the install for a convenience */ }
|
|
64
|
+
}
|
|
33
65
|
}
|
|
34
66
|
}
|
|
35
67
|
|
package/scripts/preuninstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const { CLIENTS, skillTarget } = require('./lib');
|
|
4
|
+
const { CLIENTS, skillTarget, removeGlobalOrientHooks } = require('./lib');
|
|
5
5
|
|
|
6
6
|
function removeSkill(client) {
|
|
7
7
|
const target = skillTarget(client);
|
|
@@ -14,4 +14,17 @@ function removeSkill(client) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// F-042: BEFORE the skill dirs go, remove the global orientation hooks that
|
|
18
|
+
// name them — otherwise every future session carries a hook whose validator no
|
|
19
|
+
// longer exists, the wired-and-DEAD state the check note calls the worst of
|
|
20
|
+
// the three. Surgical (only entries attributable to the removed dirs; the
|
|
21
|
+
// marker's opt-out lines survive when nothing was removed) and fail-open (an
|
|
22
|
+
// uninstall must never abort on a settings file).
|
|
23
|
+
try {
|
|
24
|
+
const removedFrom = removeGlobalOrientHooks(CLIENTS.map(skillTarget));
|
|
25
|
+
for (const target of removedFrom) {
|
|
26
|
+
console.log(`🧹 Removed the session-orientation hook entry (${target}).`);
|
|
27
|
+
}
|
|
28
|
+
} catch (e) { /* never abort an uninstall for a convenience */ }
|
|
29
|
+
|
|
17
30
|
CLIENTS.forEach(removeSkill);
|
|
@@ -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
|
|