@warnyin/sdlc 0.8.0 → 0.9.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +274 -258
  2. package/LICENSE +21 -21
  3. package/README.md +92 -92
  4. package/bin/cli.mjs +682 -682
  5. package/lib/active.mjs +199 -199
  6. package/lib/caps.mjs +46 -46
  7. package/lib/config.mjs +41 -41
  8. package/lib/delta.mjs +227 -227
  9. package/lib/frontmatter.mjs +59 -59
  10. package/lib/glob.mjs +29 -29
  11. package/lib/lenses.mjs +48 -48
  12. package/lib/manifest.mjs +99 -99
  13. package/lib/settings-merge.mjs +63 -63
  14. package/lib/skills.mjs +148 -148
  15. package/lib/validate.mjs +198 -198
  16. package/package.json +42 -42
  17. package/payload/adapters/agents-md.md +8 -8
  18. package/payload/adapters/claude/agents/sdlc-architect.md +12 -12
  19. package/payload/adapters/claude/agents/sdlc-builder.md +14 -14
  20. package/payload/adapters/claude/agents/sdlc-contractor.md +13 -13
  21. package/payload/adapters/claude/agents/sdlc-evaluator.md +13 -13
  22. package/payload/adapters/claude/agents/sdlc-learner.md +16 -16
  23. package/payload/adapters/claude/agents/sdlc-ops.md +11 -11
  24. package/payload/adapters/claude/agents/sdlc-quality.md +13 -13
  25. package/payload/adapters/claude/agents/sdlc-security.md +12 -12
  26. package/payload/adapters/claude/commands/sdlc/converge.md +5 -5
  27. package/payload/adapters/claude/commands/sdlc/init.md +4 -4
  28. package/payload/adapters/claude/commands/sdlc/next.md +4 -4
  29. package/payload/adapters/claude/commands/sdlc/observe.md +4 -4
  30. package/payload/adapters/claude/commands/sdlc/steer.md +4 -4
  31. package/payload/adapters/claude/skills/contract-writing/SKILL.md +26 -26
  32. package/payload/adapters/claude/skills/delta-spec-format/SKILL.md +36 -36
  33. package/payload/adapters/claude/skills/sdlc-conventions/SKILL.md +30 -30
  34. package/payload/adapters/cline.md +8 -8
  35. package/payload/adapters/copilot.md +8 -8
  36. package/payload/adapters/cursor.mdc +7 -7
  37. package/payload/adapters/gemini.md +8 -8
  38. package/payload/adapters/windsurf.md +4 -4
  39. package/payload/hooks/_shared.mjs +138 -138
  40. package/payload/hooks/guard-writes.mjs +87 -87
  41. package/payload/hooks/inject-context.mjs +57 -57
  42. package/payload/hooks/journal.mjs +66 -66
  43. package/payload/hooks/session-summary.mjs +52 -52
  44. package/payload/hooks/validate-artifact.mjs +84 -84
  45. package/payload/playbook/README.md +32 -32
  46. package/payload/playbook/context.md +26 -26
  47. package/payload/playbook/contract.md +29 -29
  48. package/payload/playbook/converge.md +19 -19
  49. package/payload/playbook/design.md +28 -28
  50. package/payload/playbook/init.md +22 -22
  51. package/payload/playbook/lenses.md +64 -64
  52. package/payload/playbook/new.md +41 -33
  53. package/payload/playbook/next.md +24 -24
  54. package/payload/playbook/observe.md +20 -20
  55. package/payload/playbook/principles.md +28 -28
  56. package/payload/playbook/review.md +31 -31
  57. package/payload/playbook/routing.md +19 -19
  58. package/payload/playbook/rules-card.md +17 -16
  59. package/payload/playbook/ship.md +35 -35
  60. package/payload/playbook/steer.md +21 -21
  61. package/payload/playbook/verify.md +42 -42
  62. package/payload/templates/change-deep.md +29 -29
  63. package/payload/templates/change-standard.md +28 -28
  64. package/payload/templates/change-vibe.md +19 -19
  65. package/payload/templates/config.yaml +8 -8
  66. package/payload/templates/constitution.md +14 -14
  67. package/payload/templates/contract-evals.md +9 -9
  68. package/payload/templates/contract-tests.md +9 -9
  69. package/payload/templates/harness.md +34 -34
  70. package/payload/templates/spec.md +14 -14
  71. package/payload/templates/steering.md +9 -9
  72. package/scripts/validate.mjs +47 -47
@@ -1,66 +1,66 @@
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 this session's (and the
9
- // project's) events to a change
10
- // node sdlc/.hooks/journal.mjs note <name> [k=v ...] append a journal event
11
-
12
- import process from 'node:process';
13
- import {
14
- resolveRoots, readStdinJson, writePhase, clearPhase, activeChange, appendJournal,
15
- } from './_shared.mjs';
16
- import { isOpenChange, pickSessionId, writeActive } from './lib/active.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
- // A pointer the resolver would ignore must not be written and reported as done.
40
- if (!isOpenChange(sdlcRoot, change)) {
41
- console.error(`usage: journal.mjs set-active <change-id> — "${change}" is not an open change under sdlc/changes/`);
42
- process.exit(2);
43
- }
44
- const written = writeActive(sdlcRoot, change, { sessionId: process.env.CLAUDE_CODE_SESSION_ID });
45
- // A refused write (a planted link under .state/) must be visible, not reported as done.
46
- if (written.project) console.log(`active change: ${change}`);
47
- else console.error(`[sdlc] active change "${change}" not recorded: sdlc/.state does not resolve inside this project`);
48
- } else if (cmd === 'note') {
49
- // When used as a hook, drain stdin so the harness never blocks on us.
50
- let stdinInput = null;
51
- if (!process.stdin.isTTY) stdinInput = await readStdinJson();
52
- const sessionId = pickSessionId(stdinInput?.session_id, process.env.CLAUDE_CODE_SESSION_ID);
53
- const name = rest[0] ?? 'note';
54
- const extra = {};
55
- for (const kv of rest.slice(1)) {
56
- const [k, ...v] = kv.split('=');
57
- if (k && v.length) extra[k] = v.join('=');
58
- }
59
- appendJournal(sdlcRoot, activeChange(sdlcRoot, sessionId), { event: name, ...extra });
60
- } else {
61
- console.error('usage: journal.mjs open-ship|open-steer|close|set-active|note ...');
62
- process.exit(2);
63
- }
64
- }
65
-
66
- 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 this session's (and the
9
+ // project's) events to a change
10
+ // node sdlc/.hooks/journal.mjs note <name> [k=v ...] append a journal event
11
+
12
+ import process from 'node:process';
13
+ import {
14
+ resolveRoots, readStdinJson, writePhase, clearPhase, activeChange, appendJournal,
15
+ } from './_shared.mjs';
16
+ import { isOpenChange, pickSessionId, writeActive } from './lib/active.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
+ // A pointer the resolver would ignore must not be written and reported as done.
40
+ if (!isOpenChange(sdlcRoot, change)) {
41
+ console.error(`usage: journal.mjs set-active <change-id> — "${change}" is not an open change under sdlc/changes/`);
42
+ process.exit(2);
43
+ }
44
+ const written = writeActive(sdlcRoot, change, { sessionId: process.env.CLAUDE_CODE_SESSION_ID });
45
+ // A refused write (a planted link under .state/) must be visible, not reported as done.
46
+ if (written.project) console.log(`active change: ${change}`);
47
+ else console.error(`[sdlc] active change "${change}" not recorded: sdlc/.state does not resolve inside this project`);
48
+ } else if (cmd === 'note') {
49
+ // When used as a hook, drain stdin so the harness never blocks on us.
50
+ let stdinInput = null;
51
+ if (!process.stdin.isTTY) stdinInput = await readStdinJson();
52
+ const sessionId = pickSessionId(stdinInput?.session_id, process.env.CLAUDE_CODE_SESSION_ID);
53
+ const name = rest[0] ?? 'note';
54
+ const extra = {};
55
+ for (const kv of rest.slice(1)) {
56
+ const [k, ...v] = kv.split('=');
57
+ if (k && v.length) extra[k] = v.join('=');
58
+ }
59
+ appendJournal(sdlcRoot, activeChange(sdlcRoot, sessionId), { event: name, ...extra });
60
+ } else {
61
+ console.error('usage: journal.mjs open-ship|open-steer|close|set-active|note ...');
62
+ process.exit(2);
63
+ }
64
+ }
65
+
66
+ main().catch(() => process.exit(0)); // fail open
@@ -1,52 +1,52 @@
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
- import { pickSessionId } from './lib/active.mjs';
14
-
15
- const { sdlcRoot } = resolveRoots(import.meta.url);
16
-
17
- const fmt = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M`
18
- : n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n));
19
-
20
- async function main() {
21
- const input = await readStdinJson();
22
- const transcriptPath = input?.transcript_path;
23
- if (!transcriptPath || !fs.existsSync(transcriptPath) || !fs.existsSync(sdlcRoot)) return;
24
-
25
- const usage = parseTranscriptUsage(fs.readFileSync(transcriptPath, 'utf8'));
26
- if (!usage.totals.input && !usage.totals.output) return;
27
-
28
- let prices = null;
29
- try {
30
- prices = parseConfig(fs.readFileSync(path.join(sdlcRoot, 'config.yaml'), 'utf8')).prices;
31
- } catch { /* no config, no cost */ }
32
- const usd = costUsd(usage, prices);
33
-
34
- const sessionId = pickSessionId(input?.session_id, process.env.CLAUDE_CODE_SESSION_ID);
35
- const change = activeChange(sdlcRoot, sessionId);
36
- appendJournal(sdlcRoot, change, {
37
- event: 'session',
38
- session: input?.session_id ?? null,
39
- totals: usage.totals,
40
- models: usage.models,
41
- costUsd: usd,
42
- });
43
-
44
- const t = usage.totals;
45
- console.log(
46
- `[sdlc] session: ${fmt(t.input)} in / ${fmt(t.output)} out / ${fmt(t.cacheRead)} cache-read`
47
- + ` · cost ${usd == null ? 'n/a' : `$${usd}`}`
48
- + (change ? ` · change ${change}` : ''),
49
- );
50
- }
51
-
52
- 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
+ import { pickSessionId } from './lib/active.mjs';
14
+
15
+ const { sdlcRoot } = resolveRoots(import.meta.url);
16
+
17
+ const fmt = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M`
18
+ : n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n));
19
+
20
+ async function main() {
21
+ const input = await readStdinJson();
22
+ const transcriptPath = input?.transcript_path;
23
+ if (!transcriptPath || !fs.existsSync(transcriptPath) || !fs.existsSync(sdlcRoot)) return;
24
+
25
+ const usage = parseTranscriptUsage(fs.readFileSync(transcriptPath, 'utf8'));
26
+ if (!usage.totals.input && !usage.totals.output) return;
27
+
28
+ let prices = null;
29
+ try {
30
+ prices = parseConfig(fs.readFileSync(path.join(sdlcRoot, 'config.yaml'), 'utf8')).prices;
31
+ } catch { /* no config, no cost */ }
32
+ const usd = costUsd(usage, prices);
33
+
34
+ const sessionId = pickSessionId(input?.session_id, process.env.CLAUDE_CODE_SESSION_ID);
35
+ const change = activeChange(sdlcRoot, sessionId);
36
+ appendJournal(sdlcRoot, change, {
37
+ event: 'session',
38
+ session: input?.session_id ?? null,
39
+ totals: usage.totals,
40
+ models: usage.models,
41
+ costUsd: usd,
42
+ });
43
+
44
+ const t = usage.totals;
45
+ console.log(
46
+ `[sdlc] session: ${fmt(t.input)} in / ${fmt(t.output)} out / ${fmt(t.cacheRead)} cache-read`
47
+ + ` · cost ${usd == null ? 'n/a' : `$${usd}`}`
48
+ + (change ? ` · change ${change}` : ''),
49
+ );
50
+ }
51
+
52
+ main().catch(() => process.exit(0)); // fail open
@@ -1,84 +1,84 @@
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
- import { isSafeSessionId, pickSessionId } from './lib/active.mjs';
18
-
19
- const { sdlcRoot, projectRoot } = resolveRoots(import.meta.url);
20
-
21
- function respond(context) {
22
- console.log(JSON.stringify({
23
- hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context },
24
- }));
25
- }
26
-
27
- function validateSdlcWrite(rel) {
28
- let issues = [];
29
- const changeMatch = rel.match(/^sdlc\/changes\/([^/]+)\//);
30
- if (changeMatch && changeMatch[1] !== 'archive') {
31
- issues = validateChange(path.join(sdlcRoot, 'changes', changeMatch[1]), {
32
- specsDir: path.join(sdlcRoot, 'specs'),
33
- });
34
- } else if (rel.startsWith('sdlc/context/') || rel === 'sdlc/harness.md') {
35
- issues = validateContext(sdlcRoot);
36
- }
37
- if (issues.length) {
38
- respond(`[sdlc validate]\n${formatIssues(issues)}\nFix errors before moving to the next stage.`);
39
- }
40
- }
41
-
42
- function steeringPointer(rel, sessionId) {
43
- const steeringDir = path.join(sdlcRoot, 'context', 'steering');
44
- if (!fs.existsSync(steeringDir)) return;
45
-
46
- // Refused, not stripped: stripping aliases `a/b` onto `ab` and would hand one session's
47
- // seen-steering list to another. Same single-safe-segment rule as the active pointers.
48
- const safeSession = isSafeSessionId(sessionId) ? sessionId : 'nosession';
49
- const seenPath = path.join(sdlcRoot, '.state', `pointers-${safeSession}.json`);
50
- let seen = [];
51
- try { seen = JSON.parse(fs.readFileSync(seenPath, 'utf8')); } catch { /* first hit */ }
52
-
53
- const hits = [];
54
- for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
55
- const { data } = parseFrontmatter(fs.readFileSync(path.join(steeringDir, f), 'utf8'));
56
- if (data.inclusion !== 'paths' || !Array.isArray(data.pathMatch)) continue;
57
- if (!matchGlob(rel, data.pathMatch)) continue;
58
- appendJournal(sdlcRoot, activeChange(sdlcRoot, sessionId), { event: 'pointer', steering: f, file: rel });
59
- if (!seen.includes(f)) hits.push(f);
60
- }
61
- if (!hits.length) return;
62
-
63
- fs.mkdirSync(path.dirname(seenPath), { recursive: true });
64
- fs.writeFileSync(seenPath, JSON.stringify([...seen, ...hits]));
65
- respond(
66
- 'Steering applies to this area — read before editing further: '
67
- + hits.map((f) => `sdlc/context/steering/${f}`).join(', '),
68
- );
69
- }
70
-
71
- async function main() {
72
- const input = await readStdinJson();
73
- const filePath = input?.tool_input?.file_path;
74
- if (!filePath || !fs.existsSync(sdlcRoot)) return;
75
-
76
- const rel = toPosixRel(projectRoot, path.resolve(projectRoot, filePath));
77
- if (!rel) return;
78
-
79
- const sessionId = pickSessionId(input?.session_id, process.env.CLAUDE_CODE_SESSION_ID);
80
- if (rel.startsWith('sdlc/')) validateSdlcWrite(rel);
81
- else steeringPointer(rel, sessionId);
82
- }
83
-
84
- 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
+ import { isSafeSessionId, pickSessionId } from './lib/active.mjs';
18
+
19
+ const { sdlcRoot, projectRoot } = resolveRoots(import.meta.url);
20
+
21
+ function respond(context) {
22
+ console.log(JSON.stringify({
23
+ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context },
24
+ }));
25
+ }
26
+
27
+ function validateSdlcWrite(rel) {
28
+ let issues = [];
29
+ const changeMatch = rel.match(/^sdlc\/changes\/([^/]+)\//);
30
+ if (changeMatch && changeMatch[1] !== 'archive') {
31
+ issues = validateChange(path.join(sdlcRoot, 'changes', changeMatch[1]), {
32
+ specsDir: path.join(sdlcRoot, 'specs'),
33
+ });
34
+ } else if (rel.startsWith('sdlc/context/') || rel === 'sdlc/harness.md') {
35
+ issues = validateContext(sdlcRoot);
36
+ }
37
+ if (issues.length) {
38
+ respond(`[sdlc validate]\n${formatIssues(issues)}\nFix errors before moving to the next stage.`);
39
+ }
40
+ }
41
+
42
+ function steeringPointer(rel, sessionId) {
43
+ const steeringDir = path.join(sdlcRoot, 'context', 'steering');
44
+ if (!fs.existsSync(steeringDir)) return;
45
+
46
+ // Refused, not stripped: stripping aliases `a/b` onto `ab` and would hand one session's
47
+ // seen-steering list to another. Same single-safe-segment rule as the active pointers.
48
+ const safeSession = isSafeSessionId(sessionId) ? sessionId : 'nosession';
49
+ const seenPath = path.join(sdlcRoot, '.state', `pointers-${safeSession}.json`);
50
+ let seen = [];
51
+ try { seen = JSON.parse(fs.readFileSync(seenPath, 'utf8')); } catch { /* first hit */ }
52
+
53
+ const hits = [];
54
+ for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
55
+ const { data } = parseFrontmatter(fs.readFileSync(path.join(steeringDir, f), 'utf8'));
56
+ if (data.inclusion !== 'paths' || !Array.isArray(data.pathMatch)) continue;
57
+ if (!matchGlob(rel, data.pathMatch)) continue;
58
+ appendJournal(sdlcRoot, activeChange(sdlcRoot, sessionId), { event: 'pointer', steering: f, file: rel });
59
+ if (!seen.includes(f)) hits.push(f);
60
+ }
61
+ if (!hits.length) return;
62
+
63
+ fs.mkdirSync(path.dirname(seenPath), { recursive: true });
64
+ fs.writeFileSync(seenPath, JSON.stringify([...seen, ...hits]));
65
+ respond(
66
+ 'Steering applies to this area — read before editing further: '
67
+ + hits.map((f) => `sdlc/context/steering/${f}`).join(', '),
68
+ );
69
+ }
70
+
71
+ async function main() {
72
+ const input = await readStdinJson();
73
+ const filePath = input?.tool_input?.file_path;
74
+ if (!filePath || !fs.existsSync(sdlcRoot)) return;
75
+
76
+ const rel = toPosixRel(projectRoot, path.resolve(projectRoot, filePath));
77
+ if (!rel) return;
78
+
79
+ const sessionId = pickSessionId(input?.session_id, process.env.CLAUDE_CODE_SESSION_ID);
80
+ if (rel.startsWith('sdlc/')) validateSdlcWrite(rel);
81
+ else steeringPointer(rel, sessionId);
82
+ }
83
+
84
+ main().catch(() => process.exit(0)); // fail open
@@ -1,32 +1,32 @@
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 | status (resumes an open change) | everything below | escalation only |
13
- | any stage `--auto` | that stage → ship | status + your answers | everything from there | one confirmation up front |
14
- | /sdlc:new | Requirements | specs Purpose headers | change.md | validator: delta + assumptions |
15
- | /sdlc:design | Architecture | change + touched specs | change.md § Design | escalate irreversible only |
16
- | /sdlc:contract | Contract-first | change.md | contract/*, failing tests | adversarial panel + validator |
17
- | /sdlc:build | Run harness | change + contract + steering | code, task boxes | tasks done; specs locked by hook |
18
- | /sdlc:verify | Feedback loop | contract | journal events | tests green AND evals ≥ bar |
19
- | /sdlc:review | Review | diff + change | findings in change.md | blockers = 0 |
20
- | /sdlc:ship | Ship | change | specs merge, archive, digest | validate --strict; policy may require human |
21
- | /sdlc:observe | Observe | journals | report (chat) | — |
22
- | /sdlc:converge | Maintenance | specs + code | proposed change | — |
23
- | /sdlc:steer | Configure | context/ | steering, constitution | always-budget ≤ 60 |
24
- | /sdlc:next | — | status | chat only | — |
25
- | /sdlc:feedback | — | context + your words | an issue upstream | human approves the draft |
26
-
27
- Statuses: `new → contracted → building → verified → shipped`. Tiers: `vibe | standard | deep`
28
- (triage table + Autonomy policy live in `sdlc/harness.md`).
29
-
30
- Doctrine: `principles.md` (factory model, anti-garbage), `context.md` (static/dynamic),
31
- `routing.md` (model tiers), `lenses.md` (expert lenses a change records only on signal). Non-Claude harnesses: `rules-card.md` is embedded in your
32
- 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 | status (resumes an open change) | everything below | escalation only |
13
+ | any stage `--auto` | that stage → ship | status + your answers | everything from there | one confirmation up front |
14
+ | /sdlc:new | Requirements | specs Purpose headers | change.md | validator: delta + assumptions |
15
+ | /sdlc:design | Architecture | change + touched specs | change.md § Design | escalate irreversible only |
16
+ | /sdlc:contract | Contract-first | change.md | contract/*, failing tests | adversarial panel + validator |
17
+ | /sdlc:build | Run harness | change + contract + steering | code, task boxes | tasks done; specs locked by hook |
18
+ | /sdlc:verify | Feedback loop | contract | journal events | tests green AND evals ≥ bar |
19
+ | /sdlc:review | Review | diff + change | findings in change.md | blockers = 0 |
20
+ | /sdlc:ship | Ship | change | specs merge, archive, digest | validate --strict; policy may require human |
21
+ | /sdlc:observe | Observe | journals | report (chat) | — |
22
+ | /sdlc:converge | Maintenance | specs + code | proposed change | — |
23
+ | /sdlc:steer | Configure | context/ | steering, constitution | always-budget ≤ 60 |
24
+ | /sdlc:next | — | status | chat only | — |
25
+ | /sdlc:feedback | — | context + your words | an issue upstream | human approves the draft |
26
+
27
+ Statuses: `new → contracted → building → verified → shipped`. Tiers: `vibe | standard | deep`
28
+ (triage table + Autonomy policy live in `sdlc/harness.md`).
29
+
30
+ Doctrine: `principles.md` (factory model, anti-garbage), `context.md` (static/dynamic),
31
+ `routing.md` (model tiers), `lenses.md` (expert lenses a change records only on signal). Non-Claude harnesses: `rules-card.md` is embedded in your
32
+ tool's rules file; `npx @warnyin/sdlc validate` is the enforcement floor.
@@ -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,29 +1,29 @@
1
- # /sdlc:contract <id> — tests + evals before code
2
-
3
- The contract IS the handshake. Nothing in `## Tasks` may be implemented while
4
- status is `new`.
5
-
6
- 1. From the Delta scenarios, write `contract/tests.md` (≤60 lines): one row per
7
- behavior — Given/When/Then, kind, mapped requirement. List what is explicitly
8
- out of scope and why.
9
- 1b. A non-empty `lenses`: add each lens's contract bars (`lenses.md` § contributes) as
10
- test rows where a test can prove them, and as evals quality lines where only judgment
11
- can — so any tier with lenses writes `contract/evals.md` in step 2.
12
- 2. Deep tier (standard optional): write `contract/evals.md` (≤40 lines) — the
13
- trajectory + quality rubric the sdlc-evaluator will score.
14
- 3. Generate failing tests: delegate to the `sdlc-contractor` agent (cheap tier)
15
- with ONLY tests.md + the delta + the project's test conventions. Run the test
16
- command from `sdlc/harness.md` — every new test must FAIL (red) now; a test
17
- that passes before implementation tests nothing.
18
- 4. Adversarial check instead of human approval: ask the `sdlc-quality` agent to
19
- attack the contract — uncovered scenarios, untestable rows, missing edge
20
- cases vs the delta. Fix findings; one round is usually enough, two max.
21
- 5. `npx @warnyin/sdlc validate <id>` clean → set frontmatter `status: contracted`
22
- and `node sdlc/.hooks/journal.mjs note contract tests=<n>`.
23
-
24
- Escalate only if the delta itself turns out ambiguous (back to /sdlc:new step 5).
25
-
26
- Next: /sdlc:build.
27
-
28
- `--auto`: do this stage, then continue to ship under `auto.md`'s unattended
29
- mode — gather, confirm once, run. The stage still does its own work first.
1
+ # /sdlc:contract <id> — tests + evals before code
2
+
3
+ The contract IS the handshake. Nothing in `## Tasks` may be implemented while
4
+ status is `new`.
5
+
6
+ 1. From the Delta scenarios, write `contract/tests.md` (≤60 lines): one row per
7
+ behavior — Given/When/Then, kind, mapped requirement. List what is explicitly
8
+ out of scope and why.
9
+ 1b. A non-empty `lenses`: add each lens's contract bars (`lenses.md` § contributes) as
10
+ test rows where a test can prove them, and as evals quality lines where only judgment
11
+ can — so any tier with lenses writes `contract/evals.md` in step 2.
12
+ 2. Deep tier (standard optional): write `contract/evals.md` (≤40 lines) — the
13
+ trajectory + quality rubric the sdlc-evaluator will score.
14
+ 3. Generate failing tests: delegate to the `sdlc-contractor` agent (cheap tier)
15
+ with ONLY tests.md + the delta + the project's test conventions. Run the test
16
+ command from `sdlc/harness.md` — every new test must FAIL (red) now; a test
17
+ that passes before implementation tests nothing.
18
+ 4. Adversarial check instead of human approval: ask the `sdlc-quality` agent to
19
+ attack the contract — uncovered scenarios, untestable rows, missing edge
20
+ cases vs the delta. Fix findings; one round is usually enough, two max.
21
+ 5. `npx @warnyin/sdlc validate <id>` clean → set frontmatter `status: contracted`
22
+ and `node sdlc/.hooks/journal.mjs note contract tests=<n>`.
23
+
24
+ Escalate only if the delta itself turns out ambiguous (back to /sdlc:new step 5).
25
+
26
+ Next: /sdlc:build.
27
+
28
+ `--auto`: do this stage, then continue to ship under `auto.md`'s unattended
29
+ mode — gather, confirm once, run. The stage still does its own work first.
@@ -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.