@antoneeo/agentic-sdlc-skill 1.27.1 → 1.29.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 +55 -0
- package/README.md +5 -1
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/init.js +22 -11
- package/scripts/lib.js +232 -1
- package/scripts/postinstall.js +34 -2
- package/scripts/preuninstall.js +14 -1
- package/skills/agentic-sdlc-skill/ENFORCEMENT.md +31 -0
- package/skills/agentic-sdlc-skill/SKILL.md +3 -3
- package/skills/agentic-sdlc-skill/dispatch.md +98 -96
- package/skills/agentic-sdlc-skill/review.md +45 -6
- package/skills/agentic-sdlc-skill/scripts/sdlc_core.py +138 -0
- package/skills/agentic-sdlc-skill/templates.md +12 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,61 @@
|
|
|
2
2
|
|
|
3
3
|
Tutte le modifiche significative a questa skill saranno documentate in questo file.
|
|
4
4
|
|
|
5
|
+
## [1.29.0] - 2026-08-28
|
|
6
|
+
|
|
7
|
+
Spine delta parked for the next release of this lens (shipped first in kb; the
|
|
8
|
+
spine stays byte-identical x3 in the repo while packages publish independently).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **F-041: `check` notes an unwired or dead session-orientation hook** (one
|
|
12
|
+
informational `[note]` line -- never the exit code, never a `validate` warning,
|
|
13
|
+
so CI is unaffected). `init` wires the hook only at init time; projects
|
|
14
|
+
initialized before F-036 or bootstrapped without `init` were silently unwired.
|
|
15
|
+
|
|
16
|
+
### Added (F-042, parked with the above)
|
|
17
|
+
- Install/update wires the session-orientation hook machine-wide (Claude Code
|
|
18
|
+
with Python; user-level settings; removal is a standing per-target opt-out;
|
|
19
|
+
uninstall surgically removes what it wired). The check note is global-aware
|
|
20
|
+
and reworded in consequence-first user language.
|
|
21
|
+
|
|
22
|
+
### Added (F-043 port, with the above)
|
|
23
|
+
- **The revision sentence.** Updating an existing analysis (the L2 row and the ANALYSIS write trigger) is a REWRITE, not an
|
|
24
|
+
append: full re-read, body rewritten to current state -- never an appended
|
|
25
|
+
delta section. Ported from the kb lens's `## Revision` doctrine (kb 1.10.0),
|
|
26
|
+
born from a field post-mortem where delta-append revisions left stale
|
|
27
|
+
sentences standing as printed truth.
|
|
28
|
+
|
|
29
|
+
## [1.28.0] - 2026-08-26
|
|
30
|
+
|
|
31
|
+
F-038 — the gated rung: truthful log vocabulary and a mandated ask, no grant memory.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **A review rung that exists behind a permission policy is no longer logged as
|
|
35
|
+
"unavailable".** The ladder knew two states (the client supports the rung, or not)
|
|
36
|
+
and hard-coded rung 3's reason as "no subagent facility on this client" — so a gated
|
|
37
|
+
facility produced false log rows, and the ladder's own prohibition ("rung 3 is
|
|
38
|
+
illegitimate wherever rung 1 or 2 exists") was unsatisfiable on a gated client: the
|
|
39
|
+
agent could not obey it without asking, and no rule said to ask.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **The gated-rung stop.** At a due review gate (design; closure), when the best rung is
|
|
43
|
+
permission-gated (a standing policy — an interactive per-call prompt is NOT this) and
|
|
44
|
+
no ungated rung works (a working one-shot CLI pre-empts the stop; a gated rung 2 joins
|
|
45
|
+
the question instead), the gate stops and asks ONE question in the five-bullet blocking
|
|
46
|
+
form: the gated rung(s) vs the fallback, each higher rung's status, the quoted policy,
|
|
47
|
+
the cost (~130-175k subagent tokens per deep review, measured 2026-08) AND what
|
|
48
|
+
independence last bought, and what stays blocked. **No grant memory**: the answer holds
|
|
49
|
+
while the conversation does; an agent that cannot recall a grant asks again. Unattended
|
|
50
|
+
runs never emit the question — rung 2 is still owed a try, then rung 3 with its reason.
|
|
51
|
+
(Successor to a design CANCELLED at the review cap after grant memory failed three
|
|
52
|
+
ways; rulings r18/r19.)
|
|
53
|
+
- **The reason words.** A below-rung-1 row says why the rung(s) above did not run:
|
|
54
|
+
`absent` (the client has no such facility — a claim about the client, never a policy),
|
|
55
|
+
`gated, declined`, `gated, unattended`, `gated, pre-empted` (nothing asked — an
|
|
56
|
+
ungated lower rung ran the review). A rung-1 row owes nothing. For rung 3 this
|
|
57
|
+
re-words an existing duty; for rung 2 it is a new one-word duty, disclosed and
|
|
58
|
+
accepted with the rest.
|
|
59
|
+
|
|
5
60
|
## [1.27.1 / kb 1.5.1 / mkt 0.5.1] - 2026-08-25
|
|
6
61
|
|
|
7
62
|
### Fixed
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- **Self-activating**: a SessionStart hook emits repo-sourced orientation; a deterministic self-eval battery guards the skill's own doctrine as the release gate.
|
|
15
15
|
- **Standalone complete**: works fully with local `ai_docs/` without requiring devPNT.
|
|
16
16
|
- **devPNT symbiosis**: when devPNT is available, Master Plan, Action Plan, M-VISION, and governed artifacts become the authoritative planning layer, with independent fresh-context reviews of technical artifacts and diffs.
|
|
17
|
-
- **Independent review, twice**: the design is reviewed before it is implemented and the diff before the work is declared done — by somebody other than its author. Three rungs of independence (fresh-context subagent > one-shot run > a declared self-pass, legal only when the
|
|
17
|
+
- **Independent review, twice**: the design is reviewed before it is implemented and the diff before the work is declared done — by somebody other than its author. Three rungs of independence (fresh-context subagent > one-shot run > a declared self-pass, legal only when no higher rung is usable: absent, or permission-gated and declined — a gated rung is asked about, never silently skipped, and the log says which), capped at 3 rounds, one log line per review, and a PASS is invalid on "found nothing" — it must state where each constraint is satisfied.
|
|
18
18
|
- **Question discipline**: a question to the user is legal only when the agent searched first and names the search with its result, and names the decision the answer unblocks. Otherwise it proceeds on a declared assumption — same evidence duty, batched, never a stream of "shall I proceed?".
|
|
19
19
|
- **Installed support files**: Claude, Codex, Gemini, and Google Antigravity receive the full skill folder, including `templates.md`, `architect.md`, `guides.md`, `vision.md`, `tdd.md`, `debugging.md`, `elicitation.md`, `review.md`, `dispatch.md`, `routing.md`, `ENFORCEMENT.md`, and the validator's two files, `scripts/sdlc_check.py` + `scripts/sdlc_core.py`.
|
|
20
20
|
- **Mechanical checks**: optional validator for document structure, generated feature history, stale audit areas, and protected-path gates — `check`, `validate`, `index`, `stale`, `mark`, `gate`, `plan`, `orient`, `migrate`.
|
|
@@ -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:
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/agentic-sdlc-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
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
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
|
|
|
@@ -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
|
-
|
|
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,
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: agentic-sdlc
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.29.0
|
|
4
4
|
description: Documentation-First SDLC protocol with risk-proportional triage, Vision as a guide, a complete Standalone mode and optional symbiosis with devPNT. Use for features, significant bugs, refactors, audits and documented maintenance.
|
|
5
5
|
author: Antonio Pinto (https://github.com/Antoneeo)
|
|
6
6
|
copyright: (c) 2026 Antonio Pinto
|
|
@@ -44,7 +44,7 @@ Always classify the request before choosing the process. Declare the chosen leve
|
|
|
44
44
|
| Level | Criteria | Required process |
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| **L1 - Trivial** | About 10 lines in 1-2 files; no API, dependency or new-behavior change; typos or fixes restoring already-expected behavior | Implement. Run relevant existing tests. No new documents. |
|
|
47
|
-
| **L2 - Small** | Clear root cause; at most 3 files; no new dependency or public API; low risk | Mini-analysis in the message: objective, impact, security, tests. Tests mandatory. No new document, except updating an existing analysis on the same topic, or the handoff (see Write Triggers). |
|
|
47
|
+
| **L2 - Small** | Clear root cause; at most 3 files; no new dependency or public API; low risk | Mini-analysis in the message: objective, impact, security, tests. Tests mandatory. No new document, except updating an existing analysis on the same topic (full re-read, body rewritten to current state -- never an appended delta section), or the handoff (see Write Triggers). |
|
|
48
48
|
| **L3 - Significant** | More than 3 files, APIs/contracts, new dependency, user-visible behavior, security-sensitive area, architectural change or non-obvious design | Full workflow: Vision Gate, analysis, plan, implementation, tests, closure. |
|
|
49
49
|
| **Spike** | Time-boxed exploration to reduce uncertainty | Code not mergeable into main. Outcome in `ai_docs/solutions/SPIKE_[topic].md`. For production, reclassify as L2 or L3. |
|
|
50
50
|
|
|
@@ -72,7 +72,7 @@ Triage decides IF documentation is due; this table decides WHICH document each e
|
|
|
72
72
|
|
|
73
73
|
| Document | Write trigger | Phase |
|
|
74
74
|
|---|---|---|
|
|
75
|
-
| `solutions/ANALYSIS_[feature].md` | Every L3, after elicitation and before any code. On topic match with an existing analysis, update that one instead of a new file. A capability the architect pass splits out as its own unit of change (`architect.md` §4) gets its own ANALYSIS, and the two documents name each other. | 3 |
|
|
75
|
+
| `solutions/ANALYSIS_[feature].md` | Every L3, after elicitation and before any code. On topic match with an existing analysis, update that one instead of a new file -- a revision re-reads the WHOLE document and rewrites it to current state (history in the Diary), never an appended delta section. A capability the architect pass splits out as its own unit of change (`architect.md` §4) gets its own ANALYSIS, and the two documents name each other. | 3 |
|
|
76
76
|
| `solutions/SPIKE_[topic].md` | Closing any Spike — including a failed one (a negative outcome is still an outcome). | — |
|
|
77
77
|
| `vision/features/VISION_[feature].md` | Feature known multi-milestone at analysis time, OR the retroactive trigger: you are about to create the SECOND `ANALYSIS_*` on the same theme — extract the shared feature vision first, then let both analyses reference it. | 3 |
|
|
78
78
|
| `audit/handoff.md` (workstream registry) | **Never by hand — generated by `sdlc_check.py index` from the `HANDOFF_*.md` sources**, and `validate` errors when the two disagree. Regenerate at every L3 closure and at session end; the `Date:` header is derived, so no writer touches it. It is an inventory for lookup, not a work board: no assignment, no due dates, no ordering, no holder. | 5 / session end |
|
|
@@ -1,96 +1,98 @@
|
|
|
1
|
-
# Subagent Execution Discipline
|
|
2
|
-
|
|
3
|
-
Opt-in orchestration for L3 work with an approved design: the orchestrator
|
|
4
|
-
drives a `PLAN_[feature].md` through subagents instead of implementing every
|
|
5
|
-
task in the same session. Default stays same-session; this is an escalation,
|
|
6
|
-
never a requirement.
|
|
7
|
-
|
|
8
|
-
## Trigger
|
|
9
|
-
|
|
10
|
-
Only for L3 (an approved E-TDD in Hybrid, or an ANALYSIS Action Plan in
|
|
11
|
-
Standalone). Never for L1/L2 — the plan/ledger machinery is overhead a small
|
|
12
|
-
change does not need. The plan is always `derived-from` the accepted design:
|
|
13
|
-
it is never independently authored, exactly like an E-TDD is never authored
|
|
14
|
-
without an E-ISP.
|
|
15
|
-
|
|
16
|
-
## The loop
|
|
17
|
-
|
|
18
|
-
1. `sdlc_check.py plan validate PLAN_[feature].md` — zero-execution schema +
|
|
19
|
-
confinement + ledger cross-check. Non-zero exit = **no dispatch**. This is
|
|
20
|
-
the hard gate: "no valid plan, no dispatch."
|
|
21
|
-
2. For each task, in plan order:
|
|
22
|
-
- Read the task's status from the sidecar ledger
|
|
23
|
-
(`PLAN_[feature].ledger.json`). `status: done` (exact sentinel) → skip,
|
|
24
|
-
never re-dispatch. Anything else (pending, failed, missing, or a
|
|
25
|
-
corrupt-but-parseable entry) → treat as pending and dispatch.
|
|
26
|
-
- `sdlc_check.py plan brief PLAN_[feature].md --task <id>` — prints the
|
|
27
|
-
task block, the `produces` of prior-order tasks (interfaces), and
|
|
28
|
-
`guides` pointers (paths, never pasted content) to stdout.
|
|
29
|
-
- Spawn the subagent with that brief as its entire context window.
|
|
30
|
-
- Run `task.verify` out of band (the orchestrator executes it — the
|
|
31
|
-
validator only ever prints it, never runs it) plus the one-shot review
|
|
32
|
-
below.
|
|
33
|
-
- Write `{status, verify_result, timestamp}` back to the ledger. The
|
|
34
|
-
validator never writes the ledger — single-writer, orchestrator-owned.
|
|
35
|
-
|
|
36
|
-
**Guide consumption under dispatch.** Selecting each task's `guides` field IS the
|
|
37
|
-
consult trigger (`guides.md` §0) applied at plan-authoring time: the orchestrator
|
|
38
|
-
runs the router lookup (project router `ai_docs/reference/INDEX.md` + the agent-KB
|
|
39
|
-
router) when populating `guides`. A dispatched context-free subagent does **NOT**
|
|
40
|
-
run its own router consult — it reads the guide pointers handed to it in the
|
|
41
|
-
brief. The router verdict (Rule Zero) is therefore declared ONCE, by the
|
|
42
|
-
orchestrator, when it authors the plan; a dispatched subagent does not declare
|
|
43
|
-
one. (Proactive guide-creation stays at closure — the same broad final pass
|
|
44
|
-
below — so it needs no separate dispatch hook.)
|
|
45
|
-
|
|
46
|
-
## Model tiers (client-relative, no provider names)
|
|
47
|
-
|
|
48
|
-
- Default dispatch: **economy** implementer tier.
|
|
49
|
-
- After **two** consecutive `verify_result: fail` on the same task: escalate
|
|
50
|
-
to the **deep** tier for the retry (ADR 2026-07-02). Do not escalate on the
|
|
51
|
-
first failure — a single fail is often a brief or environment issue, not a
|
|
52
|
-
capability gap.
|
|
53
|
-
|
|
54
|
-
## Review slots — one-shot, not iterative
|
|
55
|
-
|
|
56
|
-
Exactly three review touches per task, never a loop:
|
|
57
|
-
|
|
58
|
-
1. Inline self-review by the implementer subagent before it reports done
|
|
59
|
-
(the standard critical-review pass, not a separate call).
|
|
60
|
-
2. One reviewer pass per task (Hybrid: reuse the devPNT code-review gate;
|
|
61
|
-
Standalone: the `review.md` discipline).
|
|
62
|
-
3. One broad final pass over the whole plan at closure, after all tasks are
|
|
63
|
-
DONE — catches cross-task drift a per-task review cannot see.
|
|
64
|
-
|
|
65
|
-
If a review FAILs, fix and re-run `verify` — that is a normal loop iteration
|
|
66
|
-
via the ledger's fail path, not an extra review slot.
|
|
67
|
-
|
|
68
|
-
**The scoped re-review is a round INSIDE slot 2 or 3, never a fourth slot.**
|
|
69
|
-
`review.md` §Receiving requires every review-driven correction to be
|
|
70
|
-
re-reviewed against the correction alone — that round belongs to the slot whose
|
|
71
|
-
findings caused it, and is bounded by the same cap of 3 rounds. "Never a loop"
|
|
72
|
-
bounds the SLOTS (no unbounded re-reviewing of a task), not the rounds inside
|
|
73
|
-
one slot: a fix nobody looked at is the defect the round exists to catch. Slot 1
|
|
74
|
-
is the implementer's own pass, not an independent review, so its fixes carry no
|
|
75
|
-
re-review round. The ledger records the round on the task's fail path as usual.
|
|
76
|
-
|
|
77
|
-
## Ledger protocol summary
|
|
78
|
-
|
|
79
|
-
Read → skip-if-done → dispatch-if-pending → write. The ledger is the only
|
|
80
|
-
memory the loop needs across sessions or context compaction: a resumed
|
|
81
|
-
orchestrator re-reads it and picks up exactly where it left off, never
|
|
82
|
-
re-running a DONE task.
|
|
83
|
-
|
|
84
|
-
## Degradation
|
|
85
|
-
|
|
86
|
-
No subagent-spawning tool available
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
# Subagent Execution Discipline
|
|
2
|
+
|
|
3
|
+
Opt-in orchestration for L3 work with an approved design: the orchestrator
|
|
4
|
+
drives a `PLAN_[feature].md` through subagents instead of implementing every
|
|
5
|
+
task in the same session. Default stays same-session; this is an escalation,
|
|
6
|
+
never a requirement.
|
|
7
|
+
|
|
8
|
+
## Trigger
|
|
9
|
+
|
|
10
|
+
Only for L3 (an approved E-TDD in Hybrid, or an ANALYSIS Action Plan in
|
|
11
|
+
Standalone). Never for L1/L2 — the plan/ledger machinery is overhead a small
|
|
12
|
+
change does not need. The plan is always `derived-from` the accepted design:
|
|
13
|
+
it is never independently authored, exactly like an E-TDD is never authored
|
|
14
|
+
without an E-ISP.
|
|
15
|
+
|
|
16
|
+
## The loop
|
|
17
|
+
|
|
18
|
+
1. `sdlc_check.py plan validate PLAN_[feature].md` — zero-execution schema +
|
|
19
|
+
confinement + ledger cross-check. Non-zero exit = **no dispatch**. This is
|
|
20
|
+
the hard gate: "no valid plan, no dispatch."
|
|
21
|
+
2. For each task, in plan order:
|
|
22
|
+
- Read the task's status from the sidecar ledger
|
|
23
|
+
(`PLAN_[feature].ledger.json`). `status: done` (exact sentinel) → skip,
|
|
24
|
+
never re-dispatch. Anything else (pending, failed, missing, or a
|
|
25
|
+
corrupt-but-parseable entry) → treat as pending and dispatch.
|
|
26
|
+
- `sdlc_check.py plan brief PLAN_[feature].md --task <id>` — prints the
|
|
27
|
+
task block, the `produces` of prior-order tasks (interfaces), and
|
|
28
|
+
`guides` pointers (paths, never pasted content) to stdout.
|
|
29
|
+
- Spawn the subagent with that brief as its entire context window.
|
|
30
|
+
- Run `task.verify` out of band (the orchestrator executes it — the
|
|
31
|
+
validator only ever prints it, never runs it) plus the one-shot review
|
|
32
|
+
below.
|
|
33
|
+
- Write `{status, verify_result, timestamp}` back to the ledger. The
|
|
34
|
+
validator never writes the ledger — single-writer, orchestrator-owned.
|
|
35
|
+
|
|
36
|
+
**Guide consumption under dispatch.** Selecting each task's `guides` field IS the
|
|
37
|
+
consult trigger (`guides.md` §0) applied at plan-authoring time: the orchestrator
|
|
38
|
+
runs the router lookup (project router `ai_docs/reference/INDEX.md` + the agent-KB
|
|
39
|
+
router) when populating `guides`. A dispatched context-free subagent does **NOT**
|
|
40
|
+
run its own router consult — it reads the guide pointers handed to it in the
|
|
41
|
+
brief. The router verdict (Rule Zero) is therefore declared ONCE, by the
|
|
42
|
+
orchestrator, when it authors the plan; a dispatched subagent does not declare
|
|
43
|
+
one. (Proactive guide-creation stays at closure — the same broad final pass
|
|
44
|
+
below — so it needs no separate dispatch hook.)
|
|
45
|
+
|
|
46
|
+
## Model tiers (client-relative, no provider names)
|
|
47
|
+
|
|
48
|
+
- Default dispatch: **economy** implementer tier.
|
|
49
|
+
- After **two** consecutive `verify_result: fail` on the same task: escalate
|
|
50
|
+
to the **deep** tier for the retry (ADR 2026-07-02). Do not escalate on the
|
|
51
|
+
first failure — a single fail is often a brief or environment issue, not a
|
|
52
|
+
capability gap.
|
|
53
|
+
|
|
54
|
+
## Review slots — one-shot, not iterative
|
|
55
|
+
|
|
56
|
+
Exactly three review touches per task, never a loop:
|
|
57
|
+
|
|
58
|
+
1. Inline self-review by the implementer subagent before it reports done
|
|
59
|
+
(the standard critical-review pass, not a separate call).
|
|
60
|
+
2. One reviewer pass per task (Hybrid: reuse the devPNT code-review gate;
|
|
61
|
+
Standalone: the `review.md` discipline).
|
|
62
|
+
3. One broad final pass over the whole plan at closure, after all tasks are
|
|
63
|
+
DONE — catches cross-task drift a per-task review cannot see.
|
|
64
|
+
|
|
65
|
+
If a review FAILs, fix and re-run `verify` — that is a normal loop iteration
|
|
66
|
+
via the ledger's fail path, not an extra review slot.
|
|
67
|
+
|
|
68
|
+
**The scoped re-review is a round INSIDE slot 2 or 3, never a fourth slot.**
|
|
69
|
+
`review.md` §Receiving requires every review-driven correction to be
|
|
70
|
+
re-reviewed against the correction alone — that round belongs to the slot whose
|
|
71
|
+
findings caused it, and is bounded by the same cap of 3 rounds. "Never a loop"
|
|
72
|
+
bounds the SLOTS (no unbounded re-reviewing of a task), not the rounds inside
|
|
73
|
+
one slot: a fix nobody looked at is the defect the round exists to catch. Slot 1
|
|
74
|
+
is the implementer's own pass, not an independent review, so its fixes carry no
|
|
75
|
+
re-review round. The ledger records the round on the task's fail path as usual.
|
|
76
|
+
|
|
77
|
+
## Ledger protocol summary
|
|
78
|
+
|
|
79
|
+
Read → skip-if-done → dispatch-if-pending → write. The ledger is the only
|
|
80
|
+
memory the loop needs across sessions or context compaction: a resumed
|
|
81
|
+
orchestrator re-reads it and picks up exactly where it left off, never
|
|
82
|
+
re-running a DONE task.
|
|
83
|
+
|
|
84
|
+
## Degradation
|
|
85
|
+
|
|
86
|
+
No subagent-spawning tool available — absent, or permission-gated with no grant
|
|
87
|
+
in context (`review.md`'s gated-rung stop owns the asking; task dispatch never
|
|
88
|
+
asks; the broad final pass IS `review.md` moment 2 and asks there) → the
|
|
89
|
+
orchestrator runs each task in the same session, against the same plan and ledger,
|
|
90
|
+
with the same one-shot review slots. No capability is lost, only the parallelism/isolation subagents would
|
|
91
|
+
have added.
|
|
92
|
+
|
|
93
|
+
## Hybrid note
|
|
94
|
+
|
|
95
|
+
The plan's `derived-from` points at the accepted E-TDD document key. Per-task
|
|
96
|
+
review reuses the devPNT independent reviewers (§4.6 code review gate) rather
|
|
97
|
+
than restating review doctrine — see `review.md` for the single definition
|
|
98
|
+
both modes share.
|
|
@@ -12,7 +12,7 @@ Two moments, and they review different things:
|
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| **1. Design review** | End of Phase 3 — **before any implementation** | the ANALYSIS (Standalone) / the `E-ISP`+`E-TDD` (Hybrid) | L3 |
|
|
14
14
|
| **1b. Late arrival** | Work that became L3 *after* code existed — an L1/L2 reclassified mid-flight, or a design increment on a feature already implemented — runs moment 1 **now**, before any further implementation, logged `design (late)` | same | L3 |
|
|
15
|
-
| **2. Closure review** | Phase 5, before DONE | the actual diff, against that approved design | L2 / L3 — the L2
|
|
15
|
+
| **2. Closure review** | Phase 5, before DONE | the actual diff, against that approved design | L2 / L3 — the L2 closure review is optional; every review that runs logs its row |
|
|
16
16
|
|
|
17
17
|
**Why the design review is its own moment, and not a nicety.** The closure review
|
|
18
18
|
can only tell you the code matches the design; it cannot tell you the design was
|
|
@@ -30,17 +30,56 @@ blind to it, which is why independence, not effort, is what this gate buys.
|
|
|
30
30
|
a SELF-CONTAINED prompt — the reviewer session has no other context, which is
|
|
31
31
|
exactly what makes it independent.
|
|
32
32
|
3. **A declared self-pass** — a separate, explicitly adversarial pass by you,
|
|
33
|
-
against the same checklist. **Rung 3 is illegitimate wherever
|
|
34
|
-
|
|
35
|
-
it is choosing zero independence, which is the one thing this
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
against the same checklist. **Rung 3 is illegitimate wherever a higher rung
|
|
34
|
+
is usable**: on a client with a working subagent facility or one-shot CLI,
|
|
35
|
+
descending to it is choosing zero independence, which is the one thing this
|
|
36
|
+
gate buys. A gated rung the user DECLINED is not usable — see *The gated
|
|
37
|
+
rung* below; a gated rung nobody asked about is not declined (unattended is
|
|
38
|
+
its own case below). When
|
|
39
|
+
you do use it, the log row must carry *why*, in the reason words below —
|
|
40
|
+
`self-pass (declared; absent — the client has no such facility)` — not merely
|
|
41
|
+
that you did. A rung named without its
|
|
38
42
|
reason is indistinguishable from a rung chosen for convenience.
|
|
39
43
|
|
|
40
44
|
Rung 3 stays in the ladder deliberately: it is what keeps the methodology
|
|
41
45
|
completable with no network, no account and no subagent facility. It is a floor,
|
|
42
46
|
never a default.
|
|
43
47
|
|
|
48
|
+
**The gated rung — present is not absent (F-038).** A rung that exists behind a
|
|
49
|
+
standing policy or instruction forbidding its use absent a user request is
|
|
50
|
+
**permission-gated**, not unavailable. An interactive per-call approval prompt is
|
|
51
|
+
NOT this: answering the prompt IS the grant. With an ungated rung usable — rung 1
|
|
52
|
+
free, or a one-shot CLI that works (try it, or show it failed) — the prohibition
|
|
53
|
+
above is already satisfiable and no stop fires (the row still carries
|
|
54
|
+
`gated, pre-empted` when a gated rung sat above the one that ran); a gated
|
|
55
|
+
rung 2 is not "usable" for this clause, it joins the question instead. When the best rung is gated and no
|
|
56
|
+
ungated rung works, the gate STOPS and asks: descending to rung 3 on silence is
|
|
57
|
+
illegal while a user is reachable. The stop is doctrine-mandated (legal by mandate,
|
|
58
|
+
`elicitation.md` §Blocking is reserved) and carries that file's five-bullet
|
|
59
|
+
blocking form — its closed-list exemption covers this file's round-cap hand-over
|
|
60
|
+
only, not this stop. The question offers the gated rung(s) against the fallback, with what each
|
|
61
|
+
buys; states each higher rung's status (tried / unusable / gated) with the
|
|
62
|
+
standing instruction quoted and "no grant visible in my current context"; says
|
|
63
|
+
why no assumption survives (assuming either way writes a false log row); says
|
|
64
|
+
why it is the USER'S call — their tokens (~130-175k per deep review, measured
|
|
65
|
+
2026-08) against a benefit only they can price, stated with what independence
|
|
66
|
+
last bought; and names what stays blocked — which reviewer runs, nothing else.
|
|
67
|
+
**No grant memory exists**: the answer holds while the conversation does, and an
|
|
68
|
+
agent that cannot recall a grant asks again — one question per gate per intact
|
|
69
|
+
context; re-asking is a smaller defect than a false row. Unattended
|
|
70
|
+
(`elicitation.md`'s Unattended path — the user is not reachable), no question is
|
|
71
|
+
emitted: rung 2 is still owed a try, then rung 3 runs with its reason logged.
|
|
72
|
+
|
|
73
|
+
**The reason words.** A below-rung-1 row carries why the rung(s) above did not
|
|
74
|
+
run; a rung-1 row owes nothing. `absent` — the client has no such facility (a
|
|
75
|
+
claim about the client, never about a policy); `gated, declined` — usable only
|
|
76
|
+
with the user's assent at this gate, and the user withheld it (a standing policy
|
|
77
|
+
answered no, or a per-call prompt denied); `gated, unattended` — a standing policy
|
|
78
|
+
gates it and no user is reachable to ask; `gated, pre-empted` — gated, nothing
|
|
79
|
+
asked: an ungated lower rung ran the review instead. `gated` always appears with
|
|
80
|
+
its outcome. The trigger term "permission-gated" scopes the STOP; the row word
|
|
81
|
+
`gated` additionally covers a denied per-call prompt, which never triggers one.
|
|
82
|
+
|
|
44
83
|
Use a different model from the author's where the client allows it.
|
|
45
84
|
|
|
46
85
|
**Rounds are capped at 3.** FAIL → revise → re-review. If findings still stand
|
|
@@ -1646,6 +1646,143 @@ def cmd_mark(root, paths):
|
|
|
1646
1646
|
return 0
|
|
1647
1647
|
|
|
1648
1648
|
|
|
1649
|
+
# ------------------------------------------------- orientation-hook wiring
|
|
1650
|
+
# F-041: read-only mirror of the installer-side detection in lib.js
|
|
1651
|
+
# (findOrientHook + orientHookValidator). The JS owns WRITING hooks, with its
|
|
1652
|
+
# injection guards; this side only DETECTS, at `check`, so a project whose
|
|
1653
|
+
# sessions start unoriented hears about it in-project (ENFORCEMENT par.4: the
|
|
1654
|
+
# hook is wired at init only -- pre-F-036 and agent-bootstrapped projects never
|
|
1655
|
+
# got it, and a wired-but-dead hook is "the worst of the three states").
|
|
1656
|
+
# The file list is a SUPERSET of the JS one: .codex/hooks.json is documented in
|
|
1657
|
+
# ENFORCEMENT par.4 but never written by init. Pinned equal on both sides:
|
|
1658
|
+
# the entry-point names, and (F-042) the user-settings path derivation --
|
|
1659
|
+
# CLAUDE_CONFIG_DIR when set, else <home>/.claude -- empty-string env falsy
|
|
1660
|
+
# on both sides.
|
|
1661
|
+
ORIENT_HOOK_ENTRY_POINTS = ("sdlc_check.py", "mkt_check.py")
|
|
1662
|
+
ORIENT_HOOK_CONFIG_FILES = (".claude/settings.json",
|
|
1663
|
+
".claude/settings.local.json",
|
|
1664
|
+
".codex/hooks.json")
|
|
1665
|
+
ORIENT_HOOK_CONFIG_CAP = 1024 * 1024 # settings are tiny; cap pathological files
|
|
1666
|
+
_ORIENT_HOOK_CMD_RE = re.compile(r" orient(\s|$)")
|
|
1667
|
+
_ORIENT_HOOK_TOKEN_RE = re.compile(r'"([^"]*)"|(\S+)')
|
|
1668
|
+
|
|
1669
|
+
|
|
1670
|
+
def _orient_entries_in(p):
|
|
1671
|
+
"""Yield the SessionStart commands invoking an orient entry point from ONE
|
|
1672
|
+
client config file. Fail-open: an unreadable, oversized, malformed or
|
|
1673
|
+
directory-shaped file contributes nothing. Reads are utf-8-sig -- a BOM'd
|
|
1674
|
+
settings.json (PowerShell/Notepad) must not read as unwired (declared
|
|
1675
|
+
divergence from Node's JSON.parse)."""
|
|
1676
|
+
try:
|
|
1677
|
+
if not p.is_file() or p.stat().st_size > ORIENT_HOOK_CONFIG_CAP:
|
|
1678
|
+
return
|
|
1679
|
+
data = json.loads(p.read_text(encoding="utf-8-sig"))
|
|
1680
|
+
except Exception:
|
|
1681
|
+
return
|
|
1682
|
+
if not isinstance(data, dict):
|
|
1683
|
+
return
|
|
1684
|
+
hooks = data.get("hooks")
|
|
1685
|
+
groups = hooks.get("SessionStart") if isinstance(hooks, dict) else None
|
|
1686
|
+
if groups is None:
|
|
1687
|
+
groups = data.get("SessionStart") # the .codex/hooks.json shape
|
|
1688
|
+
if not isinstance(groups, list):
|
|
1689
|
+
return
|
|
1690
|
+
for group in groups:
|
|
1691
|
+
if not isinstance(group, dict):
|
|
1692
|
+
continue
|
|
1693
|
+
inner = group.get("hooks")
|
|
1694
|
+
for h in inner if isinstance(inner, list) else [group]:
|
|
1695
|
+
cmd = h.get("command") if isinstance(h, dict) else None
|
|
1696
|
+
if (isinstance(cmd, str)
|
|
1697
|
+
and any(n in cmd for n in ORIENT_HOOK_ENTRY_POINTS)
|
|
1698
|
+
and _ORIENT_HOOK_CMD_RE.search(cmd)):
|
|
1699
|
+
yield cmd
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _orient_user_settings_path():
|
|
1703
|
+
"""The machine-global settings file (F-042 wires it at install time).
|
|
1704
|
+
|
|
1705
|
+
Read at CALL time -- a DECLARED divergence from AGENTIC_SDLC_KB_ROOT's
|
|
1706
|
+
import-time read: unittest imports this module once before any test runs,
|
|
1707
|
+
so an import-time seam would be frozen before the matrix could set it.
|
|
1708
|
+
AGENTIC_SDLC_USER_SETTINGS is the test-isolation seam (python-side only);
|
|
1709
|
+
the production derivation mirrors lib.js: CLAUDE_CONFIG_DIR || ~/.claude."""
|
|
1710
|
+
seam = os.environ.get("AGENTIC_SDLC_USER_SETTINGS")
|
|
1711
|
+
if seam:
|
|
1712
|
+
return Path(seam)
|
|
1713
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
1714
|
+
return (Path(cfg) if cfg else Path.home() / ".claude") / "settings.json"
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def _orient_hook_commands(root):
|
|
1718
|
+
"""Yield (command, at_user_level) across every known client config: the
|
|
1719
|
+
three project-relative files, then the machine-global user settings --
|
|
1720
|
+
the file the installer wires (F-042), without which a fixed machine's
|
|
1721
|
+
projects would read as unwired forever."""
|
|
1722
|
+
for rel in ORIENT_HOOK_CONFIG_FILES:
|
|
1723
|
+
for cmd in _orient_entries_in(Path(root) / rel):
|
|
1724
|
+
yield cmd, False
|
|
1725
|
+
for cmd in _orient_entries_in(_orient_user_settings_path()):
|
|
1726
|
+
yield cmd, True
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def orient_hook_state(root):
|
|
1730
|
+
"""'wired' | 'dead' | 'unwired'.
|
|
1731
|
+
|
|
1732
|
+
A matching entry whose tokens name no validator is "cannot tell" and counts
|
|
1733
|
+
as wired (the lib.js contract: never report broken on cannot-tell). 'dead'
|
|
1734
|
+
requires every matching entry to name a validator that does not resolve:
|
|
1735
|
+
the client runs ALL SessionStart hooks -- project-level AND user-level
|
|
1736
|
+
merge -- so one live entry at either level still orients the session
|
|
1737
|
+
(any-resolves aggregation across levels). Relative tokens resolve against
|
|
1738
|
+
the project root (the vendored-validator convention); a USER-level relative
|
|
1739
|
+
token has no cwd to resolve against and is treated as cannot-tell."""
|
|
1740
|
+
matched = False
|
|
1741
|
+
for cmd, at_user_level in _orient_hook_commands(root):
|
|
1742
|
+
matched = True
|
|
1743
|
+
token = None
|
|
1744
|
+
for m in _ORIENT_HOOK_TOKEN_RE.finditer(cmd):
|
|
1745
|
+
tok = m.group(1) if m.group(1) is not None else m.group(2)
|
|
1746
|
+
if any(tok.endswith(n) for n in ORIENT_HOOK_ENTRY_POINTS):
|
|
1747
|
+
token = tok
|
|
1748
|
+
break
|
|
1749
|
+
if token is None:
|
|
1750
|
+
return "wired"
|
|
1751
|
+
tp = Path(token)
|
|
1752
|
+
if not tp.is_absolute():
|
|
1753
|
+
if at_user_level:
|
|
1754
|
+
return "wired" # cannot tell: no cwd exists at user level
|
|
1755
|
+
tp = Path(root) / tp
|
|
1756
|
+
if tp.exists():
|
|
1757
|
+
return "wired"
|
|
1758
|
+
return "dead" if matched else "unwired"
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def print_orient_hook_note(root):
|
|
1762
|
+
"""Print the FS-A wiring note (unwired / dead), or nothing when wired.
|
|
1763
|
+
|
|
1764
|
+
Informational only, by contract: never an exit code, never a validate
|
|
1765
|
+
warning (--strict CI must not fail on a hook that legitimately lives in
|
|
1766
|
+
git-ignored settings.local.json). Called by the spine's cmd_check AND by
|
|
1767
|
+
an overlay that replaces check with its own pipeline (the marketing lens
|
|
1768
|
+
does): the note is check-layer behaviour, whoever owns the check."""
|
|
1769
|
+
try:
|
|
1770
|
+
state = orient_hook_state(root)
|
|
1771
|
+
if state == "unwired":
|
|
1772
|
+
print("[note] agent sessions start without automatic orientation on this "
|
|
1773
|
+
"machine (no session-orientation hook found at project or user level). "
|
|
1774
|
+
"Fix once: update or reinstall the skill npm package (on Claude Code "
|
|
1775
|
+
"with Python it wires the hook machine-wide), or wire it per "
|
|
1776
|
+
"ENFORCEMENT.md par.4")
|
|
1777
|
+
elif state == "dead":
|
|
1778
|
+
print("[note] a session-orientation hook is configured but points at a "
|
|
1779
|
+
"validator that no longer exists, so sessions start unoriented. Fix: "
|
|
1780
|
+
"update or reinstall the skill npm package, or correct the command. "
|
|
1781
|
+
"Details: ENFORCEMENT.md par.4")
|
|
1782
|
+
except Exception:
|
|
1783
|
+
pass # detection must never break check
|
|
1784
|
+
|
|
1785
|
+
|
|
1649
1786
|
def cmd_check(root, strict=False, hybrid=False):
|
|
1650
1787
|
print("===== validate =====")
|
|
1651
1788
|
rc_v = cmd_validate(root, strict=strict, hybrid=hybrid)
|
|
@@ -1653,6 +1790,7 @@ def cmd_check(root, strict=False, hybrid=False):
|
|
|
1653
1790
|
rc_s = cmd_stale(root, hybrid=hybrid)
|
|
1654
1791
|
print(f"\ncheck: {'CLEAN' if not (rc_v or rc_s) else 'NOT CLEAN'} "
|
|
1655
1792
|
f"(validate rc={rc_v}, stale rc={rc_s})")
|
|
1793
|
+
print_orient_hook_note(root)
|
|
1656
1794
|
return 1 if (rc_v or rc_s) else 0
|
|
1657
1795
|
|
|
1658
1796
|
|
|
@@ -508,7 +508,7 @@ adds values to the existing columns rather than a second table.
|
|
|
508
508
|
| date | doc_key | tier | reviewer | findings_raised | findings_real | verdict | revise_rounds |
|
|
509
509
|
|---|---|---|---|---|---|---|---|
|
|
510
510
|
| 2026-06-11 | ANALYSIS_login_sso.md | design | subagent (opus, fresh ctx) | 4 | 3 | FAIL → PASS | 2 |
|
|
511
|
-
| 2026-06-12 | diff feature/sso-login | closure | self-pass (declared;
|
|
511
|
+
| 2026-06-12 | diff feature/sso-login | closure | self-pass (declared; absent) | 2 | 2 | PASS with findings → corrections re-reviewed, PASS | 2 |
|
|
512
512
|
|
|
513
513
|
## Notes
|
|
514
514
|
<!-- One short paragraph per review that found something worth remembering: what
|
|
@@ -523,9 +523,17 @@ and `closure` (Standalone); `deep`, `light`, `code`, `guide`, `vision` (devPNT g
|
|
|
523
523
|
and the Vision blind check). The validator reads this column by its header name, so
|
|
524
524
|
extra or reordered columns are fine — but the header must say `tier`. `reviewer`
|
|
525
525
|
records the realization actually used — fresh subagent, one-shot client run, or a
|
|
526
|
-
**declared** self-pass
|
|
527
|
-
|
|
528
|
-
|
|
526
|
+
**declared** self-pass — and, for any rung below rung 1, WHY the rung(s) above did
|
|
527
|
+
not run, in the ladder's reason words (`review.md`): `absent` — the client has no
|
|
528
|
+
such facility (a claim about the client, never about a policy); `gated, declined` —
|
|
529
|
+
usable only with the user's assent at this gate, withheld (a standing policy
|
|
530
|
+
answered no, or a per-call prompt denied); `gated, unattended` — a standing policy
|
|
531
|
+
gates it, no user reachable; `gated, pre-empted` — gated, nothing asked, an ungated
|
|
532
|
+
lower rung ran the review. A rung-1 row owes nothing. `gated` here is the ROW
|
|
533
|
+
word — it also covers a denied per-call prompt, which never triggers `review.md`'s
|
|
534
|
+
stop. Writing `self-pass` with its
|
|
535
|
+
true reason is honest; writing nothing, or implying independence you did not have,
|
|
536
|
+
is the failure this column exists to prevent. `findings_real` is how many raised findings survived
|
|
529
537
|
triage: over time it is the only evidence of whether the gate earns its cost.
|
|
530
538
|
`revise_rounds` counts **review rounds**, not fix cycles: the first review is round 1
|
|
531
539
|
and every scoped re-review adds one. A review that produced findings which were then
|