@warnyin/sdlc 0.1.0 → 0.2.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 +53 -19
- package/LICENSE +21 -21
- package/README.md +83 -74
- package/bin/cli.mjs +111 -31
- package/bin/detect.mjs +51 -0
- package/bin/multiselect.mjs +147 -0
- package/bin/ui.mjs +78 -0
- package/lib/caps.mjs +45 -45
- package/lib/config.mjs +41 -41
- package/lib/delta.mjs +160 -160
- package/lib/frontmatter.mjs +59 -59
- package/lib/glob.mjs +29 -29
- package/lib/manifest.mjs +99 -99
- package/lib/observe.mjs +174 -174
- package/lib/settings-merge.mjs +63 -63
- package/lib/usage.mjs +46 -46
- package/lib/validate.mjs +186 -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/auto.md +5 -5
- package/payload/adapters/claude/commands/sdlc/build.md +5 -5
- package/payload/adapters/claude/commands/sdlc/contract.md +5 -5
- package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
- package/payload/adapters/claude/commands/sdlc/design.md +5 -5
- package/payload/adapters/claude/commands/sdlc/init.md +4 -4
- package/payload/adapters/claude/commands/sdlc/new.md +5 -5
- 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/review.md +5 -5
- package/payload/adapters/claude/commands/sdlc/ship.md +5 -5
- package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
- package/payload/adapters/claude/commands/sdlc/verify.md +5 -5
- package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
- package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +33 -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 -141
- 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/README.md +30 -30
- package/payload/playbook/auto.md +21 -21
- package/payload/playbook/build.md +23 -23
- package/payload/playbook/context.md +26 -26
- package/payload/playbook/contract.md +23 -23
- package/payload/playbook/converge.md +19 -19
- package/payload/playbook/design.md +20 -20
- package/payload/playbook/init.md +22 -22
- package/payload/playbook/new.md +22 -22
- package/payload/playbook/next.md +12 -12
- package/payload/playbook/observe.md +20 -20
- package/payload/playbook/principles.md +28 -28
- package/payload/playbook/review.md +17 -17
- package/payload/playbook/routing.md +19 -19
- package/payload/playbook/rules-card.md +16 -16
- package/payload/playbook/ship.md +24 -24
- package/payload/playbook/steer.md +21 -21
- package/payload/playbook/verify.md +24 -24
- 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 -38
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// SessionStart hook — THE static-context loader. Emits (hard cap 60 lines):
|
|
3
|
-
// constitution + every `inclusion: always` steering file + a one-line
|
|
4
|
-
// pointer to the active change. Everything else stays dynamic.
|
|
5
|
-
// Journals what was injected so /sdlc:observe can price residency honestly.
|
|
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 { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
12
|
-
import { CAPS } from './lib/caps.mjs';
|
|
13
|
-
|
|
14
|
-
const { sdlcRoot } = resolveRoots(import.meta.url);
|
|
15
|
-
|
|
16
|
-
async function main() {
|
|
17
|
-
await readStdinJson(); // drain; content not needed
|
|
18
|
-
if (!fs.existsSync(sdlcRoot)) return;
|
|
19
|
-
|
|
20
|
-
const injected = [];
|
|
21
|
-
const out = [];
|
|
22
|
-
|
|
23
|
-
const constitutionPath = path.join(sdlcRoot, 'context', 'constitution.md');
|
|
24
|
-
if (fs.existsSync(constitutionPath)) {
|
|
25
|
-
out.push(fs.readFileSync(constitutionPath, 'utf8').trim());
|
|
26
|
-
injected.push('context/constitution.md');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const steeringDir = path.join(sdlcRoot, 'context', 'steering');
|
|
30
|
-
if (fs.existsSync(steeringDir)) {
|
|
31
|
-
for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
32
|
-
const raw = fs.readFileSync(path.join(steeringDir, f), 'utf8');
|
|
33
|
-
const { data, body } = parseFrontmatter(raw);
|
|
34
|
-
if (data.inclusion !== 'always') continue;
|
|
35
|
-
out.push(body.trim());
|
|
36
|
-
injected.push(`context/steering/${f}`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const active = activeChange(sdlcRoot);
|
|
41
|
-
if (active) out.push(`Active change: sdlc/changes/${active}/change.md — run /sdlc:next for status.`);
|
|
42
|
-
|
|
43
|
-
if (!out.length) return;
|
|
44
|
-
|
|
45
|
-
let lines = out.join('\n\n').split('\n');
|
|
46
|
-
if (lines.length > CAPS.alwaysBudget) {
|
|
47
|
-
lines = lines.slice(0, CAPS.alwaysBudget);
|
|
48
|
-
lines.push(`[sdlc] static context truncated at ${CAPS.alwaysBudget} lines — run /sdlc:steer to distill (validate also flags this).`);
|
|
49
|
-
}
|
|
50
|
-
console.log(lines.join('\n'));
|
|
51
|
-
|
|
52
|
-
appendJournal(sdlcRoot, active, { event: 'inject', files: injected, lines: lines.length });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
main().catch(() => process.exit(0)); // fail open
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook — THE static-context loader. Emits (hard cap 60 lines):
|
|
3
|
+
// constitution + every `inclusion: always` steering file + a one-line
|
|
4
|
+
// pointer to the active change. Everything else stays dynamic.
|
|
5
|
+
// Journals what was injected so /sdlc:observe can price residency honestly.
|
|
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 { parseFrontmatter } from './lib/frontmatter.mjs';
|
|
12
|
+
import { CAPS } from './lib/caps.mjs';
|
|
13
|
+
|
|
14
|
+
const { sdlcRoot } = resolveRoots(import.meta.url);
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
await readStdinJson(); // drain; content not needed
|
|
18
|
+
if (!fs.existsSync(sdlcRoot)) return;
|
|
19
|
+
|
|
20
|
+
const injected = [];
|
|
21
|
+
const out = [];
|
|
22
|
+
|
|
23
|
+
const constitutionPath = path.join(sdlcRoot, 'context', 'constitution.md');
|
|
24
|
+
if (fs.existsSync(constitutionPath)) {
|
|
25
|
+
out.push(fs.readFileSync(constitutionPath, 'utf8').trim());
|
|
26
|
+
injected.push('context/constitution.md');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const steeringDir = path.join(sdlcRoot, 'context', 'steering');
|
|
30
|
+
if (fs.existsSync(steeringDir)) {
|
|
31
|
+
for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
|
|
32
|
+
const raw = fs.readFileSync(path.join(steeringDir, f), 'utf8');
|
|
33
|
+
const { data, body } = parseFrontmatter(raw);
|
|
34
|
+
if (data.inclusion !== 'always') continue;
|
|
35
|
+
out.push(body.trim());
|
|
36
|
+
injected.push(`context/steering/${f}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const active = activeChange(sdlcRoot);
|
|
41
|
+
if (active) out.push(`Active change: sdlc/changes/${active}/change.md — run /sdlc:next for status.`);
|
|
42
|
+
|
|
43
|
+
if (!out.length) return;
|
|
44
|
+
|
|
45
|
+
let lines = out.join('\n\n').split('\n');
|
|
46
|
+
if (lines.length > CAPS.alwaysBudget) {
|
|
47
|
+
lines = lines.slice(0, CAPS.alwaysBudget);
|
|
48
|
+
lines.push(`[sdlc] static context truncated at ${CAPS.alwaysBudget} lines — run /sdlc:steer to distill (validate also flags this).`);
|
|
49
|
+
}
|
|
50
|
+
console.log(lines.join('\n'));
|
|
51
|
+
|
|
52
|
+
appendJournal(sdlcRoot, active, { event: 'inject', files: injected, lines: lines.length });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main().catch(() => process.exit(0)); // fail open
|
|
@@ -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,30 +1,30 @@
|
|
|
1
|
-
# @warnyin/sdlc playbook
|
|
2
|
-
|
|
3
|
-
One change = one folder in `sdlc/changes/<id>/` moving through:
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
new → [design] → contract → build → verify → [review] → ship
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
| Command | Day-1 phase | Reads | Writes | Gate (automatic unless noted) |
|
|
10
|
-
|---|---|---|---|---|
|
|
11
|
-
| /sdlc:init | Configure harness | interview | constitution, harness.md | human approves (once) |
|
|
12
|
-
| /sdlc:auto | whole loop | — | everything below | escalation only |
|
|
13
|
-
| /sdlc:new | Requirements | specs Purpose headers | change.md | validator: delta + assumptions |
|
|
14
|
-
| /sdlc:design | Architecture | change + touched specs | change.md § Design | escalate irreversible only |
|
|
15
|
-
| /sdlc:contract | Contract-first | change.md | contract/*, failing tests | adversarial panel + validator |
|
|
16
|
-
| /sdlc:build | Run harness | change + contract + steering | code, task boxes | tasks done; specs locked by hook |
|
|
17
|
-
| /sdlc:verify | Feedback loop | contract | journal events | tests green AND evals ≥ bar |
|
|
18
|
-
| /sdlc:review | Review | diff + change | findings in change.md | blockers = 0 |
|
|
19
|
-
| /sdlc:ship | Ship | change | specs merge, archive, digest | validate --strict; policy may require human |
|
|
20
|
-
| /sdlc:observe | Observe | journals | report (chat) | — |
|
|
21
|
-
| /sdlc:converge | Maintenance | specs + code | proposed change | — |
|
|
22
|
-
| /sdlc:steer | Configure | context/ | steering, constitution | always-budget ≤ 60 |
|
|
23
|
-
| /sdlc:next | — | status | chat only | — |
|
|
24
|
-
|
|
25
|
-
Statuses: `new → contracted → building → verified → shipped`. Tiers: `vibe | standard | deep`
|
|
26
|
-
(triage table + Autonomy policy live in `sdlc/harness.md`).
|
|
27
|
-
|
|
28
|
-
Doctrine: `principles.md` (factory model, anti-garbage), `context.md` (static/dynamic),
|
|
29
|
-
`routing.md` (model tiers). Non-Claude harnesses: `rules-card.md` is embedded in your
|
|
30
|
-
tool's rules file; `npx @warnyin/sdlc validate` is the enforcement floor.
|
|
1
|
+
# @warnyin/sdlc playbook
|
|
2
|
+
|
|
3
|
+
One change = one folder in `sdlc/changes/<id>/` moving through:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
new → [design] → contract → build → verify → [review] → ship
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
| Command | Day-1 phase | Reads | Writes | Gate (automatic unless noted) |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| /sdlc:init | Configure harness | interview | constitution, harness.md | human approves (once) |
|
|
12
|
+
| /sdlc:auto | whole loop | — | everything below | escalation only |
|
|
13
|
+
| /sdlc:new | Requirements | specs Purpose headers | change.md | validator: delta + assumptions |
|
|
14
|
+
| /sdlc:design | Architecture | change + touched specs | change.md § Design | escalate irreversible only |
|
|
15
|
+
| /sdlc:contract | Contract-first | change.md | contract/*, failing tests | adversarial panel + validator |
|
|
16
|
+
| /sdlc:build | Run harness | change + contract + steering | code, task boxes | tasks done; specs locked by hook |
|
|
17
|
+
| /sdlc:verify | Feedback loop | contract | journal events | tests green AND evals ≥ bar |
|
|
18
|
+
| /sdlc:review | Review | diff + change | findings in change.md | blockers = 0 |
|
|
19
|
+
| /sdlc:ship | Ship | change | specs merge, archive, digest | validate --strict; policy may require human |
|
|
20
|
+
| /sdlc:observe | Observe | journals | report (chat) | — |
|
|
21
|
+
| /sdlc:converge | Maintenance | specs + code | proposed change | — |
|
|
22
|
+
| /sdlc:steer | Configure | context/ | steering, constitution | always-budget ≤ 60 |
|
|
23
|
+
| /sdlc:next | — | status | chat only | — |
|
|
24
|
+
|
|
25
|
+
Statuses: `new → contracted → building → verified → shipped`. Tiers: `vibe | standard | deep`
|
|
26
|
+
(triage table + Autonomy policy live in `sdlc/harness.md`).
|
|
27
|
+
|
|
28
|
+
Doctrine: `principles.md` (factory model, anti-garbage), `context.md` (static/dynamic),
|
|
29
|
+
`routing.md` (model tiers). Non-Claude harnesses: `rules-card.md` is embedded in your
|
|
30
|
+
tool's rules file; `npx @warnyin/sdlc validate` is the enforcement floor.
|
package/payload/playbook/auto.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
# /sdlc:auto <title> — the whole pipeline, one command
|
|
2
|
-
|
|
3
|
-
Runs new → [design] → contract → build → verify → [review] → ship, each stage by
|
|
4
|
-
its own playbook, WITHOUT pausing for the human except on the Autonomy-policy
|
|
5
|
-
escalation conditions:
|
|
6
|
-
|
|
7
|
-
- hard-floor surface (security, payments, data-loss, irreversible),
|
|
8
|
-
- a `[NEEDS CLARIFICATION]` the agent cannot resolve alone,
|
|
9
|
-
- verify failed more than 3 rounds,
|
|
10
|
-
- token budget exceeded (if the user set one),
|
|
11
|
-
- ship policy requires human approval (deep tier).
|
|
12
|
-
|
|
13
|
-
Rules:
|
|
14
|
-
- Announce the plan in ≤3 lines after triage (id, tier, task count), then work.
|
|
15
|
-
- Between stages run `npx @warnyin/sdlc validate <id>` — a red validator is a
|
|
16
|
-
hard stop for that stage, not a suggestion.
|
|
17
|
-
- On escalation: stop at the exact step, state what is needed in ≤5 lines, wait.
|
|
18
|
-
When the human answers, resume from that step — never restart the pipeline.
|
|
19
|
-
- On completion report one line: shipped + digest path + total cost if known.
|
|
20
|
-
|
|
21
|
-
This is orchestrator mode: the human describes the outcome and walks away.
|
|
1
|
+
# /sdlc:auto <title> — the whole pipeline, one command
|
|
2
|
+
|
|
3
|
+
Runs new → [design] → contract → build → verify → [review] → ship, each stage by
|
|
4
|
+
its own playbook, WITHOUT pausing for the human except on the Autonomy-policy
|
|
5
|
+
escalation conditions:
|
|
6
|
+
|
|
7
|
+
- hard-floor surface (security, payments, data-loss, irreversible),
|
|
8
|
+
- a `[NEEDS CLARIFICATION]` the agent cannot resolve alone,
|
|
9
|
+
- verify failed more than 3 rounds,
|
|
10
|
+
- token budget exceeded (if the user set one),
|
|
11
|
+
- ship policy requires human approval (deep tier).
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Announce the plan in ≤3 lines after triage (id, tier, task count), then work.
|
|
15
|
+
- Between stages run `npx @warnyin/sdlc validate <id>` — a red validator is a
|
|
16
|
+
hard stop for that stage, not a suggestion.
|
|
17
|
+
- On escalation: stop at the exact step, state what is needed in ≤5 lines, wait.
|
|
18
|
+
When the human answers, resume from that step — never restart the pipeline.
|
|
19
|
+
- On completion report one line: shipped + digest path + total cost if known.
|
|
20
|
+
|
|
21
|
+
This is orchestrator mode: the human describes the outcome and walks away.
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
# /sdlc:build <id> — implement (Run the harness)
|
|
2
|
-
|
|
3
|
-
Precondition: status ≥ contracted (vibe tier is exempt from contracts).
|
|
4
|
-
Set `status: building`.
|
|
5
|
-
|
|
6
|
-
Mode by size:
|
|
7
|
-
- **Conductor** (≤2 tasks): implement in this session, task by task.
|
|
8
|
-
- **Orchestrator** (>2 tasks): fan out one `sdlc-builder` subagent per task in a
|
|
9
|
-
`[P]` wave; serialize between dependent waves. Each builder receives ONLY:
|
|
10
|
-
its task line, `contract/tests.md`, the touched capability's spec, and steering
|
|
11
|
-
matching its file area — never the whole change history.
|
|
12
|
-
|
|
13
|
-
Rules for whoever implements:
|
|
14
|
-
- Follow steering pointers the moment the PostToolUse hook emits them.
|
|
15
|
-
- Never edit `sdlc/specs/**`, archive, journals, or lint/test configs to go
|
|
16
|
-
green — hooks deny the first two; the rest is the config-protection rule.
|
|
17
|
-
- Per-task self-check = that task's tests + lint only; the full test run belongs
|
|
18
|
-
to /sdlc:verify (moved, not removed).
|
|
19
|
-
- Tick `- [x]` in `## Tasks` as each task lands; note surprises in one line max.
|
|
20
|
-
- Honor `[tier:x]` markers when delegating (see routing.md).
|
|
21
|
-
|
|
22
|
-
Done when all tasks are ticked and the code compiles/lints.
|
|
23
|
-
`node sdlc/.hooks/journal.mjs note build tasks=<n>` then → /sdlc:verify.
|
|
1
|
+
# /sdlc:build <id> — implement (Run the harness)
|
|
2
|
+
|
|
3
|
+
Precondition: status ≥ contracted (vibe tier is exempt from contracts).
|
|
4
|
+
Set `status: building`.
|
|
5
|
+
|
|
6
|
+
Mode by size:
|
|
7
|
+
- **Conductor** (≤2 tasks): implement in this session, task by task.
|
|
8
|
+
- **Orchestrator** (>2 tasks): fan out one `sdlc-builder` subagent per task in a
|
|
9
|
+
`[P]` wave; serialize between dependent waves. Each builder receives ONLY:
|
|
10
|
+
its task line, `contract/tests.md`, the touched capability's spec, and steering
|
|
11
|
+
matching its file area — never the whole change history.
|
|
12
|
+
|
|
13
|
+
Rules for whoever implements:
|
|
14
|
+
- Follow steering pointers the moment the PostToolUse hook emits them.
|
|
15
|
+
- Never edit `sdlc/specs/**`, archive, journals, or lint/test configs to go
|
|
16
|
+
green — hooks deny the first two; the rest is the config-protection rule.
|
|
17
|
+
- Per-task self-check = that task's tests + lint only; the full test run belongs
|
|
18
|
+
to /sdlc:verify (moved, not removed).
|
|
19
|
+
- Tick `- [x]` in `## Tasks` as each task lands; note surprises in one line max.
|
|
20
|
+
- Honor `[tier:x]` markers when delegating (see routing.md).
|
|
21
|
+
|
|
22
|
+
Done when all tasks are ticked and the code compiles/lints.
|
|
23
|
+
`node sdlc/.hooks/journal.mjs note build tasks=<n>` then → /sdlc:verify.
|