@warnyin/sdlc 0.5.0 → 0.5.2
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 +25 -0
- package/LICENSE +21 -21
- package/bin/cli.mjs +12 -1
- package/lib/caps.mjs +45 -45
- package/lib/config.mjs +41 -41
- package/lib/delta.mjs +227 -160
- package/lib/frontmatter.mjs +59 -59
- package/lib/glob.mjs +29 -29
- package/lib/manifest.mjs +99 -99
- package/lib/settings-merge.mjs +63 -63
- package/lib/usage.mjs +49 -46
- package/lib/validate.mjs +196 -186
- package/package.json +42 -42
- package/payload/adapters/agents-md.md +8 -8
- package/payload/adapters/claude/agents/sdlc-architect.md +12 -12
- package/payload/adapters/claude/agents/sdlc-builder.md +14 -14
- package/payload/adapters/claude/agents/sdlc-contractor.md +13 -13
- package/payload/adapters/claude/agents/sdlc-evaluator.md +13 -13
- package/payload/adapters/claude/agents/sdlc-learner.md +16 -16
- package/payload/adapters/claude/agents/sdlc-ops.md +11 -11
- package/payload/adapters/claude/agents/sdlc-quality.md +13 -13
- package/payload/adapters/claude/agents/sdlc-security.md +12 -12
- package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
- package/payload/adapters/claude/commands/sdlc/init.md +4 -4
- package/payload/adapters/claude/commands/sdlc/next.md +4 -4
- package/payload/adapters/claude/commands/sdlc/observe.md +4 -4
- package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
- package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
- package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +36 -33
- package/payload/adapters/claude/skills/sdlc-conventions/SKILL.md +26 -26
- package/payload/adapters/cline.md +8 -8
- package/payload/adapters/copilot.md +8 -8
- package/payload/adapters/cursor.mdc +7 -7
- package/payload/adapters/gemini.md +8 -8
- package/payload/adapters/windsurf.md +4 -4
- package/payload/hooks/_shared.mjs +154 -154
- package/payload/hooks/guard-writes.mjs +83 -83
- package/payload/hooks/inject-context.mjs +55 -55
- package/payload/hooks/journal.mjs +58 -58
- package/payload/hooks/session-summary.mjs +50 -50
- package/payload/hooks/validate-artifact.mjs +80 -80
- package/payload/playbook/context.md +26 -26
- package/payload/playbook/converge.md +19 -19
- package/payload/playbook/init.md +22 -22
- package/payload/playbook/observe.md +20 -20
- package/payload/playbook/principles.md +28 -28
- package/payload/playbook/routing.md +19 -19
- package/payload/playbook/rules-card.md +16 -16
- package/payload/playbook/ship.md +2 -0
- package/payload/playbook/steer.md +21 -21
- package/payload/templates/change-deep.md +29 -29
- package/payload/templates/change-standard.md +28 -28
- package/payload/templates/change-vibe.md +19 -19
- package/payload/templates/config.yaml +8 -8
- package/payload/templates/constitution.md +14 -14
- package/payload/templates/contract-evals.md +9 -9
- package/payload/templates/contract-tests.md +9 -9
- package/payload/templates/harness.md +33 -33
- package/payload/templates/spec.md +14 -14
- package/payload/templates/steering.md +9 -9
- package/scripts/validate.mjs +47 -47
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Gate + journal utility. Called by playbooks (sanctioned phase transitions)
|
|
3
|
-
// and wired as the PreCompact hook (`journal.mjs note compact`).
|
|
4
|
-
//
|
|
5
|
-
// node sdlc/.hooks/journal.mjs open-ship <change-id> unlock specs/archive writes (TTL 30m)
|
|
6
|
-
// node sdlc/.hooks/journal.mjs open-steer unlock constitution edits (TTL 30m)
|
|
7
|
-
// node sdlc/.hooks/journal.mjs close close any open gate
|
|
8
|
-
// node sdlc/.hooks/journal.mjs set-active <change-id> attribute sessions/events to a change
|
|
9
|
-
// node sdlc/.hooks/journal.mjs note <name> [k=v ...] append a journal event
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import process from 'node:process';
|
|
14
|
-
import {
|
|
15
|
-
resolveRoots, readStdinJson, writePhase, clearPhase, activeChange, appendJournal,
|
|
16
|
-
} from './_shared.mjs';
|
|
17
|
-
|
|
18
|
-
const { sdlcRoot } = resolveRoots(import.meta.url);
|
|
19
|
-
|
|
20
|
-
async function main() {
|
|
21
|
-
const [cmd, ...rest] = process.argv.slice(2);
|
|
22
|
-
|
|
23
|
-
if (cmd === 'open-ship') {
|
|
24
|
-
const change = rest[0];
|
|
25
|
-
if (!change) { console.error('usage: journal.mjs open-ship <change-id>'); process.exit(2); }
|
|
26
|
-
const phase = writePhase(sdlcRoot, { phase: 'ship', change });
|
|
27
|
-
appendJournal(sdlcRoot, change, { event: 'gate', gate: 'ship', action: 'open', expires: phase.expires });
|
|
28
|
-
console.log(`ship gate open for "${change}" until ${phase.expires}`);
|
|
29
|
-
} else if (cmd === 'open-steer') {
|
|
30
|
-
const phase = writePhase(sdlcRoot, { phase: 'steer' });
|
|
31
|
-
appendJournal(sdlcRoot, null, { event: 'gate', gate: 'steer', action: 'open', expires: phase.expires });
|
|
32
|
-
console.log(`steer gate open until ${phase.expires}`);
|
|
33
|
-
} else if (cmd === 'close') {
|
|
34
|
-
clearPhase(sdlcRoot);
|
|
35
|
-
console.log('gate closed');
|
|
36
|
-
} else if (cmd === 'set-active') {
|
|
37
|
-
const change = rest[0];
|
|
38
|
-
if (!change) { console.error('usage: journal.mjs set-active <change-id>'); process.exit(2); }
|
|
39
|
-
fs.mkdirSync(path.join(sdlcRoot, '.state'), { recursive: true });
|
|
40
|
-
fs.writeFileSync(path.join(sdlcRoot, '.state', 'active.json'), JSON.stringify({ change }));
|
|
41
|
-
console.log(`active change: ${change}`);
|
|
42
|
-
} else if (cmd === 'note') {
|
|
43
|
-
// When used as a hook, drain stdin so the harness never blocks on us.
|
|
44
|
-
if (!process.stdin.isTTY) await readStdinJson();
|
|
45
|
-
const name = rest[0] ?? 'note';
|
|
46
|
-
const extra = {};
|
|
47
|
-
for (const kv of rest.slice(1)) {
|
|
48
|
-
const [k, ...v] = kv.split('=');
|
|
49
|
-
if (k && v.length) extra[k] = v.join('=');
|
|
50
|
-
}
|
|
51
|
-
appendJournal(sdlcRoot, activeChange(sdlcRoot), { event: name, ...extra });
|
|
52
|
-
} else {
|
|
53
|
-
console.error('usage: journal.mjs open-ship|open-steer|close|set-active|note ...');
|
|
54
|
-
process.exit(2);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
main().catch(() => process.exit(0)); // fail open
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Gate + journal utility. Called by playbooks (sanctioned phase transitions)
|
|
3
|
+
// and wired as the PreCompact hook (`journal.mjs note compact`).
|
|
4
|
+
//
|
|
5
|
+
// node sdlc/.hooks/journal.mjs open-ship <change-id> unlock specs/archive writes (TTL 30m)
|
|
6
|
+
// node sdlc/.hooks/journal.mjs open-steer unlock constitution edits (TTL 30m)
|
|
7
|
+
// node sdlc/.hooks/journal.mjs close close any open gate
|
|
8
|
+
// node sdlc/.hooks/journal.mjs set-active <change-id> attribute sessions/events to a change
|
|
9
|
+
// node sdlc/.hooks/journal.mjs note <name> [k=v ...] append a journal event
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import process from 'node:process';
|
|
14
|
+
import {
|
|
15
|
+
resolveRoots, readStdinJson, writePhase, clearPhase, activeChange, appendJournal,
|
|
16
|
+
} from './_shared.mjs';
|
|
17
|
+
|
|
18
|
+
const { sdlcRoot } = resolveRoots(import.meta.url);
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
22
|
+
|
|
23
|
+
if (cmd === 'open-ship') {
|
|
24
|
+
const change = rest[0];
|
|
25
|
+
if (!change) { console.error('usage: journal.mjs open-ship <change-id>'); process.exit(2); }
|
|
26
|
+
const phase = writePhase(sdlcRoot, { phase: 'ship', change });
|
|
27
|
+
appendJournal(sdlcRoot, change, { event: 'gate', gate: 'ship', action: 'open', expires: phase.expires });
|
|
28
|
+
console.log(`ship gate open for "${change}" until ${phase.expires}`);
|
|
29
|
+
} else if (cmd === 'open-steer') {
|
|
30
|
+
const phase = writePhase(sdlcRoot, { phase: 'steer' });
|
|
31
|
+
appendJournal(sdlcRoot, null, { event: 'gate', gate: 'steer', action: 'open', expires: phase.expires });
|
|
32
|
+
console.log(`steer gate open until ${phase.expires}`);
|
|
33
|
+
} else if (cmd === 'close') {
|
|
34
|
+
clearPhase(sdlcRoot);
|
|
35
|
+
console.log('gate closed');
|
|
36
|
+
} else if (cmd === 'set-active') {
|
|
37
|
+
const change = rest[0];
|
|
38
|
+
if (!change) { console.error('usage: journal.mjs set-active <change-id>'); process.exit(2); }
|
|
39
|
+
fs.mkdirSync(path.join(sdlcRoot, '.state'), { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(sdlcRoot, '.state', 'active.json'), JSON.stringify({ change }));
|
|
41
|
+
console.log(`active change: ${change}`);
|
|
42
|
+
} else if (cmd === 'note') {
|
|
43
|
+
// When used as a hook, drain stdin so the harness never blocks on us.
|
|
44
|
+
if (!process.stdin.isTTY) await readStdinJson();
|
|
45
|
+
const name = rest[0] ?? 'note';
|
|
46
|
+
const extra = {};
|
|
47
|
+
for (const kv of rest.slice(1)) {
|
|
48
|
+
const [k, ...v] = kv.split('=');
|
|
49
|
+
if (k && v.length) extra[k] = v.join('=');
|
|
50
|
+
}
|
|
51
|
+
appendJournal(sdlcRoot, activeChange(sdlcRoot), { event: name, ...extra });
|
|
52
|
+
} else {
|
|
53
|
+
console.error('usage: journal.mjs open-ship|open-steer|close|set-active|note ...');
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
main().catch(() => process.exit(0)); // fail open
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Stop hook — the observability tap. Parses the session transcript's real
|
|
3
|
-
// usage numbers, attributes them to the active change, appends a `session`
|
|
4
|
-
// journal event, and prints a one-line summary. Cost is computed only when
|
|
5
|
-
// sdlc/config.yaml provides a price table — otherwise reported as n/a.
|
|
6
|
-
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import process from 'node:process';
|
|
10
|
-
import { resolveRoots, readStdinJson, activeChange, appendJournal } from './_shared.mjs';
|
|
11
|
-
import { parseTranscriptUsage, costUsd } from './lib/usage.mjs';
|
|
12
|
-
import { parseConfig } from './lib/config.mjs';
|
|
13
|
-
|
|
14
|
-
const { sdlcRoot } = resolveRoots(import.meta.url);
|
|
15
|
-
|
|
16
|
-
const fmt = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M`
|
|
17
|
-
: n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n));
|
|
18
|
-
|
|
19
|
-
async function main() {
|
|
20
|
-
const input = await readStdinJson();
|
|
21
|
-
const transcriptPath = input?.transcript_path;
|
|
22
|
-
if (!transcriptPath || !fs.existsSync(transcriptPath) || !fs.existsSync(sdlcRoot)) return;
|
|
23
|
-
|
|
24
|
-
const usage = parseTranscriptUsage(fs.readFileSync(transcriptPath, 'utf8'));
|
|
25
|
-
if (!usage.totals.input && !usage.totals.output) return;
|
|
26
|
-
|
|
27
|
-
let prices = null;
|
|
28
|
-
try {
|
|
29
|
-
prices = parseConfig(fs.readFileSync(path.join(sdlcRoot, 'config.yaml'), 'utf8')).prices;
|
|
30
|
-
} catch { /* no config, no cost */ }
|
|
31
|
-
const usd = costUsd(usage, prices);
|
|
32
|
-
|
|
33
|
-
const change = activeChange(sdlcRoot);
|
|
34
|
-
appendJournal(sdlcRoot, change, {
|
|
35
|
-
event: 'session',
|
|
36
|
-
session: input?.session_id ?? null,
|
|
37
|
-
totals: usage.totals,
|
|
38
|
-
models: usage.models,
|
|
39
|
-
costUsd: usd,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const t = usage.totals;
|
|
43
|
-
console.log(
|
|
44
|
-
`[sdlc] session: ${fmt(t.input)} in / ${fmt(t.output)} out / ${fmt(t.cacheRead)} cache-read`
|
|
45
|
-
+ ` · cost ${usd == null ? 'n/a' : `$${usd}`}`
|
|
46
|
-
+ (change ? ` · change ${change}` : ''),
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
main().catch(() => process.exit(0)); // fail open
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stop hook — the observability tap. Parses the session transcript's real
|
|
3
|
+
// usage numbers, attributes them to the active change, appends a `session`
|
|
4
|
+
// journal event, and prints a one-line summary. Cost is computed only when
|
|
5
|
+
// sdlc/config.yaml provides a price table — otherwise reported as n/a.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import process from 'node:process';
|
|
10
|
+
import { resolveRoots, readStdinJson, activeChange, appendJournal } from './_shared.mjs';
|
|
11
|
+
import { parseTranscriptUsage, costUsd } from './lib/usage.mjs';
|
|
12
|
+
import { parseConfig } from './lib/config.mjs';
|
|
13
|
+
|
|
14
|
+
const { sdlcRoot } = resolveRoots(import.meta.url);
|
|
15
|
+
|
|
16
|
+
const fmt = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M`
|
|
17
|
+
: n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n));
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const input = await readStdinJson();
|
|
21
|
+
const transcriptPath = input?.transcript_path;
|
|
22
|
+
if (!transcriptPath || !fs.existsSync(transcriptPath) || !fs.existsSync(sdlcRoot)) return;
|
|
23
|
+
|
|
24
|
+
const usage = parseTranscriptUsage(fs.readFileSync(transcriptPath, 'utf8'));
|
|
25
|
+
if (!usage.totals.input && !usage.totals.output) return;
|
|
26
|
+
|
|
27
|
+
let prices = null;
|
|
28
|
+
try {
|
|
29
|
+
prices = parseConfig(fs.readFileSync(path.join(sdlcRoot, 'config.yaml'), 'utf8')).prices;
|
|
30
|
+
} catch { /* no config, no cost */ }
|
|
31
|
+
const usd = costUsd(usage, prices);
|
|
32
|
+
|
|
33
|
+
const change = activeChange(sdlcRoot);
|
|
34
|
+
appendJournal(sdlcRoot, change, {
|
|
35
|
+
event: 'session',
|
|
36
|
+
session: input?.session_id ?? null,
|
|
37
|
+
totals: usage.totals,
|
|
38
|
+
models: usage.models,
|
|
39
|
+
costUsd: usd,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const t = usage.totals;
|
|
43
|
+
console.log(
|
|
44
|
+
`[sdlc] session: ${fmt(t.input)} in / ${fmt(t.output)} out / ${fmt(t.cacheRead)} cache-read`
|
|
45
|
+
+ ` · cost ${usd == null ? 'n/a' : `$${usd}`}`
|
|
46
|
+
+ (change ? ` · change ${change}` : ''),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch(() => process.exit(0)); // fail open
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// PostToolUse hook — two token-lean feedback loops:
|
|
3
|
-
// (a) a write under sdlc/ → targeted structural validation, warnings back
|
|
4
|
-
// to the model as additionalContext (fix drift the moment it happens);
|
|
5
|
-
// (b) a source-file write matching a steering pathMatch → emit a POINTER to
|
|
6
|
-
// that steering file, once per session (a pointer, never the content —
|
|
7
|
-
// dynamic context loading at near-zero cost). Hits are journaled so the
|
|
8
|
-
// learner can expire steering that never fires.
|
|
9
|
-
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import process from 'node:process';
|
|
13
|
-
import { resolveRoots, readStdinJson, activeChange, appendJournal, toPosixRel } from './_shared.mjs';
|
|
14
|
-
import { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
15
|
-
import { matchGlob } from './lib/glob.mjs';
|
|
16
|
-
import { validateChange, validateContext, formatIssues } from './lib/validate.mjs';
|
|
17
|
-
|
|
18
|
-
const { sdlcRoot, projectRoot } = resolveRoots(import.meta.url);
|
|
19
|
-
|
|
20
|
-
function respond(context) {
|
|
21
|
-
console.log(JSON.stringify({
|
|
22
|
-
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context },
|
|
23
|
-
}));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function validateSdlcWrite(rel) {
|
|
27
|
-
let issues = [];
|
|
28
|
-
const changeMatch = rel.match(/^sdlc\/changes\/([^/]+)\//);
|
|
29
|
-
if (changeMatch && changeMatch[1] !== 'archive') {
|
|
30
|
-
issues = validateChange(path.join(sdlcRoot, 'changes', changeMatch[1]), {
|
|
31
|
-
specsDir: path.join(sdlcRoot, 'specs'),
|
|
32
|
-
});
|
|
33
|
-
} else if (rel.startsWith('sdlc/context/') || rel === 'sdlc/harness.md') {
|
|
34
|
-
issues = validateContext(sdlcRoot);
|
|
35
|
-
}
|
|
36
|
-
if (issues.length) {
|
|
37
|
-
respond(`[sdlc validate]\n${formatIssues(issues)}\nFix errors before moving to the next stage.`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function steeringPointer(rel, sessionId) {
|
|
42
|
-
const steeringDir = path.join(sdlcRoot, 'context', 'steering');
|
|
43
|
-
if (!fs.existsSync(steeringDir)) return;
|
|
44
|
-
|
|
45
|
-
const safeSession = String(sessionId ?? '').replace(/[^A-Za-z0-9_-]/g, '') || 'nosession';
|
|
46
|
-
const seenPath = path.join(sdlcRoot, '.state', `pointers-${safeSession}.json`);
|
|
47
|
-
let seen = [];
|
|
48
|
-
try { seen = JSON.parse(fs.readFileSync(seenPath, 'utf8')); } catch { /* first hit */ }
|
|
49
|
-
|
|
50
|
-
const hits = [];
|
|
51
|
-
for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
52
|
-
const { data } = parseFrontmatter(fs.readFileSync(path.join(steeringDir, f), 'utf8'));
|
|
53
|
-
if (data.inclusion !== 'paths' || !Array.isArray(data.pathMatch)) continue;
|
|
54
|
-
if (!matchGlob(rel, data.pathMatch)) continue;
|
|
55
|
-
appendJournal(sdlcRoot, activeChange(sdlcRoot), { event: 'pointer', steering: f, file: rel });
|
|
56
|
-
if (!seen.includes(f)) hits.push(f);
|
|
57
|
-
}
|
|
58
|
-
if (!hits.length) return;
|
|
59
|
-
|
|
60
|
-
fs.mkdirSync(path.dirname(seenPath), { recursive: true });
|
|
61
|
-
fs.writeFileSync(seenPath, JSON.stringify([...seen, ...hits]));
|
|
62
|
-
respond(
|
|
63
|
-
'Steering applies to this area — read before editing further: '
|
|
64
|
-
+ hits.map((f) => `sdlc/context/steering/${f}`).join(', '),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function main() {
|
|
69
|
-
const input = await readStdinJson();
|
|
70
|
-
const filePath = input?.tool_input?.file_path;
|
|
71
|
-
if (!filePath || !fs.existsSync(sdlcRoot)) return;
|
|
72
|
-
|
|
73
|
-
const rel = toPosixRel(projectRoot, path.resolve(projectRoot, filePath));
|
|
74
|
-
if (!rel) return;
|
|
75
|
-
|
|
76
|
-
if (rel.startsWith('sdlc/')) validateSdlcWrite(rel);
|
|
77
|
-
else steeringPointer(rel, input?.session_id);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
main().catch(() => process.exit(0)); // fail open
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostToolUse hook — two token-lean feedback loops:
|
|
3
|
+
// (a) a write under sdlc/ → targeted structural validation, warnings back
|
|
4
|
+
// to the model as additionalContext (fix drift the moment it happens);
|
|
5
|
+
// (b) a source-file write matching a steering pathMatch → emit a POINTER to
|
|
6
|
+
// that steering file, once per session (a pointer, never the content —
|
|
7
|
+
// dynamic context loading at near-zero cost). Hits are journaled so the
|
|
8
|
+
// learner can expire steering that never fires.
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import process from 'node:process';
|
|
13
|
+
import { resolveRoots, readStdinJson, activeChange, appendJournal, toPosixRel } from './_shared.mjs';
|
|
14
|
+
import { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
15
|
+
import { matchGlob } from './lib/glob.mjs';
|
|
16
|
+
import { validateChange, validateContext, formatIssues } from './lib/validate.mjs';
|
|
17
|
+
|
|
18
|
+
const { sdlcRoot, projectRoot } = resolveRoots(import.meta.url);
|
|
19
|
+
|
|
20
|
+
function respond(context) {
|
|
21
|
+
console.log(JSON.stringify({
|
|
22
|
+
hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context },
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateSdlcWrite(rel) {
|
|
27
|
+
let issues = [];
|
|
28
|
+
const changeMatch = rel.match(/^sdlc\/changes\/([^/]+)\//);
|
|
29
|
+
if (changeMatch && changeMatch[1] !== 'archive') {
|
|
30
|
+
issues = validateChange(path.join(sdlcRoot, 'changes', changeMatch[1]), {
|
|
31
|
+
specsDir: path.join(sdlcRoot, 'specs'),
|
|
32
|
+
});
|
|
33
|
+
} else if (rel.startsWith('sdlc/context/') || rel === 'sdlc/harness.md') {
|
|
34
|
+
issues = validateContext(sdlcRoot);
|
|
35
|
+
}
|
|
36
|
+
if (issues.length) {
|
|
37
|
+
respond(`[sdlc validate]\n${formatIssues(issues)}\nFix errors before moving to the next stage.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function steeringPointer(rel, sessionId) {
|
|
42
|
+
const steeringDir = path.join(sdlcRoot, 'context', 'steering');
|
|
43
|
+
if (!fs.existsSync(steeringDir)) return;
|
|
44
|
+
|
|
45
|
+
const safeSession = String(sessionId ?? '').replace(/[^A-Za-z0-9_-]/g, '') || 'nosession';
|
|
46
|
+
const seenPath = path.join(sdlcRoot, '.state', `pointers-${safeSession}.json`);
|
|
47
|
+
let seen = [];
|
|
48
|
+
try { seen = JSON.parse(fs.readFileSync(seenPath, 'utf8')); } catch { /* first hit */ }
|
|
49
|
+
|
|
50
|
+
const hits = [];
|
|
51
|
+
for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
52
|
+
const { data } = parseFrontmatter(fs.readFileSync(path.join(steeringDir, f), 'utf8'));
|
|
53
|
+
if (data.inclusion !== 'paths' || !Array.isArray(data.pathMatch)) continue;
|
|
54
|
+
if (!matchGlob(rel, data.pathMatch)) continue;
|
|
55
|
+
appendJournal(sdlcRoot, activeChange(sdlcRoot), { event: 'pointer', steering: f, file: rel });
|
|
56
|
+
if (!seen.includes(f)) hits.push(f);
|
|
57
|
+
}
|
|
58
|
+
if (!hits.length) return;
|
|
59
|
+
|
|
60
|
+
fs.mkdirSync(path.dirname(seenPath), { recursive: true });
|
|
61
|
+
fs.writeFileSync(seenPath, JSON.stringify([...seen, ...hits]));
|
|
62
|
+
respond(
|
|
63
|
+
'Steering applies to this area — read before editing further: '
|
|
64
|
+
+ hits.map((f) => `sdlc/context/steering/${f}`).join(', '),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const input = await readStdinJson();
|
|
70
|
+
const filePath = input?.tool_input?.file_path;
|
|
71
|
+
if (!filePath || !fs.existsSync(sdlcRoot)) return;
|
|
72
|
+
|
|
73
|
+
const rel = toPosixRel(projectRoot, path.resolve(projectRoot, filePath));
|
|
74
|
+
if (!rel) return;
|
|
75
|
+
|
|
76
|
+
if (rel.startsWith('sdlc/')) validateSdlcWrite(rel);
|
|
77
|
+
else steeringPointer(rel, input?.session_id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch(() => process.exit(0)); // fail open
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
# Context engineering
|
|
2
|
-
|
|
3
|
-
Six context types: instructions, knowledge, memory, examples, tools, guardrails.
|
|
4
|
-
The design decision is WHERE each lives: static (always loaded, expensive) vs
|
|
5
|
-
dynamic (loaded on demand, cheap). That boundary is versioned code, reviewed in PRs.
|
|
6
|
-
|
|
7
|
-
## Static (injected every session by the SessionStart hook, hard cap 60 lines total)
|
|
8
|
-
- `sdlc/context/constitution.md` (≤30) — hard rules + stack facts only.
|
|
9
|
-
- `sdlc/context/steering/*.md` with `inclusion: always` — should be rare.
|
|
10
|
-
- One pointer line to the active change.
|
|
11
|
-
|
|
12
|
-
## Dynamic (loaded only when needed)
|
|
13
|
-
- `inclusion: paths` steering — the PostToolUse hook emits a POINTER when an edited
|
|
14
|
-
file matches `pathMatch`; you then read the file. Pointers cost ~1 line, not 40.
|
|
15
|
-
- `inclusion: manual` — read only when a playbook or user names it.
|
|
16
|
-
- `inclusion: agent` — subagents load it themselves; the main loop never pays for it.
|
|
17
|
-
- Living specs — read the `## Purpose` header first; open the full spec only for
|
|
18
|
-
capabilities your change touches.
|
|
19
|
-
- Playbooks — each command reads exactly one playbook file.
|
|
20
|
-
|
|
21
|
-
## Budget rules
|
|
22
|
-
- Always-budget (constitution + always-steering) ≤ 60 lines. Validator-enforced.
|
|
23
|
-
- Adding an always rule requires removing one, or demoting something to `paths`.
|
|
24
|
-
- `/sdlc:observe` reports steering that never fires — expire or demote it.
|
|
25
|
-
- A `compact` event in the journal means context overflowed: treat as a defect,
|
|
26
|
-
find the resident artifact that caused it.
|
|
1
|
+
# Context engineering
|
|
2
|
+
|
|
3
|
+
Six context types: instructions, knowledge, memory, examples, tools, guardrails.
|
|
4
|
+
The design decision is WHERE each lives: static (always loaded, expensive) vs
|
|
5
|
+
dynamic (loaded on demand, cheap). That boundary is versioned code, reviewed in PRs.
|
|
6
|
+
|
|
7
|
+
## Static (injected every session by the SessionStart hook, hard cap 60 lines total)
|
|
8
|
+
- `sdlc/context/constitution.md` (≤30) — hard rules + stack facts only.
|
|
9
|
+
- `sdlc/context/steering/*.md` with `inclusion: always` — should be rare.
|
|
10
|
+
- One pointer line to the active change.
|
|
11
|
+
|
|
12
|
+
## Dynamic (loaded only when needed)
|
|
13
|
+
- `inclusion: paths` steering — the PostToolUse hook emits a POINTER when an edited
|
|
14
|
+
file matches `pathMatch`; you then read the file. Pointers cost ~1 line, not 40.
|
|
15
|
+
- `inclusion: manual` — read only when a playbook or user names it.
|
|
16
|
+
- `inclusion: agent` — subagents load it themselves; the main loop never pays for it.
|
|
17
|
+
- Living specs — read the `## Purpose` header first; open the full spec only for
|
|
18
|
+
capabilities your change touches.
|
|
19
|
+
- Playbooks — each command reads exactly one playbook file.
|
|
20
|
+
|
|
21
|
+
## Budget rules
|
|
22
|
+
- Always-budget (constitution + always-steering) ≤ 60 lines. Validator-enforced.
|
|
23
|
+
- Adding an always rule requires removing one, or demoting something to `paths`.
|
|
24
|
+
- `/sdlc:observe` reports steering that never fires — expire or demote it.
|
|
25
|
+
- A `compact` event in the journal means context overflowed: treat as a defect,
|
|
26
|
+
find the resident artifact that caused it.
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
# /sdlc:converge [capability] — spec ↔ code drift (Maintenance)
|
|
2
|
-
|
|
3
|
-
Living specs are only useful while they are true. Converge closes the gap.
|
|
4
|
-
|
|
5
|
-
1. Scope: the named capability, or the ones `/sdlc:observe` flagged.
|
|
6
|
-
2. For each requirement in `sdlc/specs/<capability>/spec.md`, check the code
|
|
7
|
-
actually behaves that way (read the relevant code; run targeted tests where
|
|
8
|
-
cheap). Three outcomes per requirement:
|
|
9
|
-
- **true** — nothing to do.
|
|
10
|
-
- **code drifted** — behavior no longer matches the spec.
|
|
11
|
-
- **spec stale** — the spec describes behavior nobody wants anymore.
|
|
12
|
-
3. Report the diff table in chat (requirement · outcome · evidence file:line).
|
|
13
|
-
4. For every drift/stale finding the user wants fixed, open ONE change via
|
|
14
|
-
/sdlc:new whose Delta uses MODIFIED/REMOVED against the exact requirement
|
|
15
|
-
names — converge itself NEVER edits specs or code directly.
|
|
16
|
-
5. If a recent rule/playbook edit correlates with worse flywheel metrics
|
|
17
|
-
(observe shows it), propose the revert here as a change too.
|
|
18
|
-
|
|
19
|
-
Read-only except for creating proposed change folders.
|
|
1
|
+
# /sdlc:converge [capability] — spec ↔ code drift (Maintenance)
|
|
2
|
+
|
|
3
|
+
Living specs are only useful while they are true. Converge closes the gap.
|
|
4
|
+
|
|
5
|
+
1. Scope: the named capability, or the ones `/sdlc:observe` flagged.
|
|
6
|
+
2. For each requirement in `sdlc/specs/<capability>/spec.md`, check the code
|
|
7
|
+
actually behaves that way (read the relevant code; run targeted tests where
|
|
8
|
+
cheap). Three outcomes per requirement:
|
|
9
|
+
- **true** — nothing to do.
|
|
10
|
+
- **code drifted** — behavior no longer matches the spec.
|
|
11
|
+
- **spec stale** — the spec describes behavior nobody wants anymore.
|
|
12
|
+
3. Report the diff table in chat (requirement · outcome · evidence file:line).
|
|
13
|
+
4. For every drift/stale finding the user wants fixed, open ONE change via
|
|
14
|
+
/sdlc:new whose Delta uses MODIFIED/REMOVED against the exact requirement
|
|
15
|
+
names — converge itself NEVER edits specs or code directly.
|
|
16
|
+
5. If a recent rule/playbook edit correlates with worse flywheel metrics
|
|
17
|
+
(observe shows it), propose the revert here as a change too.
|
|
18
|
+
|
|
19
|
+
Read-only except for creating proposed change folders.
|
package/payload/playbook/init.md
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
# /sdlc:init — configure the harness (run once per project)
|
|
2
|
-
|
|
3
|
-
The only planned blocking human gate in the framework: the human approves the
|
|
4
|
-
policy the AI will then drive under.
|
|
5
|
-
|
|
6
|
-
1. Read `sdlc/config.yaml`, the repo README, manifest files (package.json etc.),
|
|
7
|
-
and skim the top-level structure. Do NOT deep-read the codebase.
|
|
8
|
-
2. Interview the user briefly (≤6 questions): what the project is, hard rules the
|
|
9
|
-
agent must never break, test command, risk areas (security/payments/data-loss),
|
|
10
|
-
and how autonomous shipping should be (adjusts `## Autonomy policy`).
|
|
11
|
-
3. Open the gate: `node sdlc/.hooks/journal.mjs open-steer`, then write:
|
|
12
|
-
- `sdlc/context/constitution.md` — replace template placeholders; ≤30 lines;
|
|
13
|
-
SHALL/SHALL NOT rules only, stack facts ≤3 lines.
|
|
14
|
-
- `sdlc/harness.md` — fill tools, test command, sandbox notes; adjust the
|
|
15
|
-
routing, triage, and Autonomy policy tables to this project.
|
|
16
|
-
- 0–3 steering seeds in `sdlc/context/steering/` for areas with real
|
|
17
|
-
conventions (prefer `inclusion: paths`; `always` needs strong justification).
|
|
18
|
-
4. Run `npx @warnyin/sdlc validate` — fix every error.
|
|
19
|
-
5. Show the user constitution + harness verbatim; iterate until approved.
|
|
20
|
-
6. Close the gate: `node sdlc/.hooks/journal.mjs close`.
|
|
21
|
-
|
|
22
|
-
Output: approved constitution + harness. No change folder is created here.
|
|
1
|
+
# /sdlc:init — configure the harness (run once per project)
|
|
2
|
+
|
|
3
|
+
The only planned blocking human gate in the framework: the human approves the
|
|
4
|
+
policy the AI will then drive under.
|
|
5
|
+
|
|
6
|
+
1. Read `sdlc/config.yaml`, the repo README, manifest files (package.json etc.),
|
|
7
|
+
and skim the top-level structure. Do NOT deep-read the codebase.
|
|
8
|
+
2. Interview the user briefly (≤6 questions): what the project is, hard rules the
|
|
9
|
+
agent must never break, test command, risk areas (security/payments/data-loss),
|
|
10
|
+
and how autonomous shipping should be (adjusts `## Autonomy policy`).
|
|
11
|
+
3. Open the gate: `node sdlc/.hooks/journal.mjs open-steer`, then write:
|
|
12
|
+
- `sdlc/context/constitution.md` — replace template placeholders; ≤30 lines;
|
|
13
|
+
SHALL/SHALL NOT rules only, stack facts ≤3 lines.
|
|
14
|
+
- `sdlc/harness.md` — fill tools, test command, sandbox notes; adjust the
|
|
15
|
+
routing, triage, and Autonomy policy tables to this project.
|
|
16
|
+
- 0–3 steering seeds in `sdlc/context/steering/` for areas with real
|
|
17
|
+
conventions (prefer `inclusion: paths`; `always` needs strong justification).
|
|
18
|
+
4. Run `npx @warnyin/sdlc validate` — fix every error.
|
|
19
|
+
5. Show the user constitution + harness verbatim; iterate until approved.
|
|
20
|
+
6. Close the gate: `node sdlc/.hooks/journal.mjs close`.
|
|
21
|
+
|
|
22
|
+
Output: approved constitution + harness. No change folder is created here.
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
# /sdlc:observe — cost, flow, and drift report
|
|
2
|
-
|
|
3
|
-
1. Run `npx @warnyin/sdlc observe --json` and render it for the human. Do not
|
|
4
|
-
recompute anything the CLI already computed.
|
|
5
|
-
2. Report, in this order (skip empty sections):
|
|
6
|
-
- **Pending digests** — archived changes whose digest the human has not been
|
|
7
|
-
shown yet (learner proposals awaiting a decision).
|
|
8
|
-
- **Cost** — tokens/cost per active + recent change; retry waste (tokens spent
|
|
9
|
-
after the first verify fail).
|
|
10
|
-
- **Flow** — lead time per shipped change and per-phase breakdown (created →
|
|
11
|
-
contracted → built → verified → shipped timestamps from the journal);
|
|
12
|
-
AI-time vs wait-time; first-pass success trend.
|
|
13
|
-
- **Residency** — always-loaded lines vs the 60 budget.
|
|
14
|
-
- **Drift flags** — steering never hit by a pointer, rules never triggering a
|
|
15
|
-
guard event, `compact` events (context overflow = a defect), specs touched
|
|
16
|
-
by many changes (converge candidates).
|
|
17
|
-
3. For each flag, offer the one-line fix (`/sdlc:steer` demotion, `/sdlc:converge`
|
|
18
|
-
on a capability, cap adjustment) — recommend, never auto-apply here.
|
|
19
|
-
|
|
20
|
-
Read-only: this command changes nothing.
|
|
1
|
+
# /sdlc:observe — cost, flow, and drift report
|
|
2
|
+
|
|
3
|
+
1. Run `npx @warnyin/sdlc observe --json` and render it for the human. Do not
|
|
4
|
+
recompute anything the CLI already computed.
|
|
5
|
+
2. Report, in this order (skip empty sections):
|
|
6
|
+
- **Pending digests** — archived changes whose digest the human has not been
|
|
7
|
+
shown yet (learner proposals awaiting a decision).
|
|
8
|
+
- **Cost** — tokens/cost per active + recent change; retry waste (tokens spent
|
|
9
|
+
after the first verify fail).
|
|
10
|
+
- **Flow** — lead time per shipped change and per-phase breakdown (created →
|
|
11
|
+
contracted → built → verified → shipped timestamps from the journal);
|
|
12
|
+
AI-time vs wait-time; first-pass success trend.
|
|
13
|
+
- **Residency** — always-loaded lines vs the 60 budget.
|
|
14
|
+
- **Drift flags** — steering never hit by a pointer, rules never triggering a
|
|
15
|
+
guard event, `compact` events (context overflow = a defect), specs touched
|
|
16
|
+
by many changes (converge candidates).
|
|
17
|
+
3. For each flag, offer the one-line fix (`/sdlc:steer` demotion, `/sdlc:converge`
|
|
18
|
+
on a capability, cap adjustment) — recommend, never auto-apply here.
|
|
19
|
+
|
|
20
|
+
Read-only: this command changes nothing.
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
# Principles
|
|
2
|
-
|
|
3
|
-
## Factory model
|
|
4
|
-
Your output is the system that produces code — specs, contracts, gates, feedback loops,
|
|
5
|
-
guardrails — not the code itself. Give agents success criteria, not step-by-step
|
|
6
|
-
instructions, then let them iterate. Humans set policy once (`/sdlc:init`) and read
|
|
7
|
-
ship digests asynchronously; they are interrupted only by policy-listed exceptions.
|
|
8
|
-
|
|
9
|
-
## Economics (CapEx / OpEx)
|
|
10
|
-
Configuration (constitution, harness, steering, contracts) is CapEx: paid once, reviewed
|
|
11
|
-
like code. Every resident line of context is OpEx paid in every turn. Therefore:
|
|
12
|
-
- Every mandatory artifact has a stated reason and a line cap (see template comments).
|
|
13
|
-
- No artifact may restate another; deltas are merged mechanically, never re-narrated.
|
|
14
|
-
- Retry loops are the expensive failure mode — contracts up front buy first-pass success.
|
|
15
|
-
|
|
16
|
-
## Anti-garbage rules
|
|
17
|
-
- Optional artifacts (design section, evals for standard tier) exist only on signal.
|
|
18
|
-
- If a section would be empty, delete the section — never keep placeholder prose.
|
|
19
|
-
- The learner may only ADD an always-loaded rule by displacing an old one (fixed budget).
|
|
20
|
-
- Machine data (journals, state, learning stats) never lives in prose files.
|
|
21
|
-
|
|
22
|
-
## Verification stance
|
|
23
|
-
Tests verify the deterministic; evals verify trajectory and quality. Both are written
|
|
24
|
-
before code — together they are the contract with the AI. "Seems to work" is not a gate.
|
|
25
|
-
|
|
26
|
-
## Minimalism
|
|
27
|
-
Prefer: do nothing → stdlib → existing dependency → smallest new code. Never cut:
|
|
28
|
-
trust-boundary validation, data-loss handling, security controls, the contract itself.
|
|
1
|
+
# Principles
|
|
2
|
+
|
|
3
|
+
## Factory model
|
|
4
|
+
Your output is the system that produces code — specs, contracts, gates, feedback loops,
|
|
5
|
+
guardrails — not the code itself. Give agents success criteria, not step-by-step
|
|
6
|
+
instructions, then let them iterate. Humans set policy once (`/sdlc:init`) and read
|
|
7
|
+
ship digests asynchronously; they are interrupted only by policy-listed exceptions.
|
|
8
|
+
|
|
9
|
+
## Economics (CapEx / OpEx)
|
|
10
|
+
Configuration (constitution, harness, steering, contracts) is CapEx: paid once, reviewed
|
|
11
|
+
like code. Every resident line of context is OpEx paid in every turn. Therefore:
|
|
12
|
+
- Every mandatory artifact has a stated reason and a line cap (see template comments).
|
|
13
|
+
- No artifact may restate another; deltas are merged mechanically, never re-narrated.
|
|
14
|
+
- Retry loops are the expensive failure mode — contracts up front buy first-pass success.
|
|
15
|
+
|
|
16
|
+
## Anti-garbage rules
|
|
17
|
+
- Optional artifacts (design section, evals for standard tier) exist only on signal.
|
|
18
|
+
- If a section would be empty, delete the section — never keep placeholder prose.
|
|
19
|
+
- The learner may only ADD an always-loaded rule by displacing an old one (fixed budget).
|
|
20
|
+
- Machine data (journals, state, learning stats) never lives in prose files.
|
|
21
|
+
|
|
22
|
+
## Verification stance
|
|
23
|
+
Tests verify the deterministic; evals verify trajectory and quality. Both are written
|
|
24
|
+
before code — together they are the contract with the AI. "Seems to work" is not a gate.
|
|
25
|
+
|
|
26
|
+
## Minimalism
|
|
27
|
+
Prefer: do nothing → stdlib → existing dependency → smallest new code. Never cut:
|
|
28
|
+
trust-boundary validation, data-loss handling, security controls, the contract itself.
|