@warnyin/sdlc 0.3.0 → 0.5.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/lib/observe.mjs CHANGED
@@ -1,174 +1,187 @@
1
- // Observability aggregation — reads journals + context files, computes the
2
- // numbers /sdlc:observe reports. Pure given an sdlc root; no LLM involved.
3
-
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import { parseFrontmatter } from './frontmatter.mjs';
7
- import { CAPS, countEffectiveLines } from './caps.mjs';
8
-
9
- function readJournal(dir) {
10
- const p = path.join(dir, 'journal.ndjson');
11
- if (!fs.existsSync(p)) return [];
12
- return fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).map((l) => {
13
- try { return JSON.parse(l); } catch { return null; }
14
- }).filter(Boolean);
15
- }
16
-
17
- function summarizeChange(dir, id, archived) {
18
- const events = readJournal(dir);
19
- const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
20
- let costUsd = 0;
21
- let costKnown = false;
22
- let sessions = 0;
23
- const verify = { rounds: 0, firstPass: null };
24
- let guards = 0;
25
- let compacts = 0;
26
-
27
- for (const e of events) {
28
- if (e.event === 'session') {
29
- sessions++;
30
- for (const k of Object.keys(tokens)) tokens[k] += e.totals?.[k] ?? 0;
31
- if (typeof e.costUsd === 'number') { costUsd += e.costUsd; costKnown = true; }
32
- } else if (e.event === 'verify') {
33
- verify.rounds++;
34
- if (verify.firstPass === null) verify.firstPass = e.result === 'pass';
35
- } else if (e.event === 'guard') guards++;
36
- else if (e.event === 'compact') compacts++;
37
- }
38
-
39
- const first = events[0]?.ts ? Date.parse(events[0].ts) : null;
40
- const shipEvent = events.find((e) => e.event === 'ship');
41
- const shippedAt = shipEvent?.ts ? Date.parse(shipEvent.ts) : null;
42
- const leadTimeMs = first != null && shippedAt != null ? shippedAt - first : null;
43
-
44
- let tier = null;
45
- let status = null;
46
- const changePath = path.join(dir, 'change.md');
47
- if (fs.existsSync(changePath)) {
48
- const { data } = parseFrontmatter(fs.readFileSync(changePath, 'utf8'));
49
- tier = data.tier ?? null;
50
- status = data.status ?? null;
51
- }
52
-
53
- return {
54
- id, archived, tier, status, sessions, tokens,
55
- costUsd: costKnown ? Number(costUsd.toFixed(4)) : null,
56
- verify, guards, compacts, leadTimeMs,
57
- digest: archived ? fs.existsSync(path.join(dir, 'digest.md')) : null,
58
- };
59
- }
60
-
61
- export function buildReport(sdlcRoot) {
62
- const changes = [];
63
-
64
- const changesDir = path.join(sdlcRoot, 'changes');
65
- if (fs.existsSync(changesDir)) {
66
- for (const d of fs.readdirSync(changesDir, { withFileTypes: true })) {
67
- if (!d.isDirectory() || d.name === 'archive') continue;
68
- changes.push(summarizeChange(path.join(changesDir, d.name), d.name, false));
69
- }
70
- const archiveDir = path.join(changesDir, 'archive');
71
- if (fs.existsSync(archiveDir)) {
72
- for (const d of fs.readdirSync(archiveDir, { withFileTypes: true })) {
73
- if (!d.isDirectory()) continue;
74
- changes.push(summarizeChange(path.join(archiveDir, d.name), d.name, true));
75
- }
76
- }
77
- }
78
-
79
- // Residency: constitution + always-steering effective lines vs budget.
80
- let alwaysLines = 0;
81
- const constitutionPath = path.join(sdlcRoot, 'context', 'constitution.md');
82
- if (fs.existsSync(constitutionPath)) {
83
- alwaysLines += countEffectiveLines(fs.readFileSync(constitutionPath, 'utf8'));
84
- }
85
- const steering = [];
86
- const pointerHits = new Map();
87
- for (const c of changes) {
88
- const dir = c.archived
89
- ? path.join(sdlcRoot, 'changes', 'archive', c.id)
90
- : path.join(sdlcRoot, 'changes', c.id);
91
- for (const e of readJournal(dir)) {
92
- if (e.event === 'pointer' && e.steering) {
93
- pointerHits.set(e.steering, (pointerHits.get(e.steering) ?? 0) + 1);
94
- }
95
- }
96
- }
97
- // Global journal (events with no active change) counts too.
98
- for (const e of readJournal(path.join(sdlcRoot, '.state'))) {
99
- if (e.event === 'pointer' && e.steering) {
100
- pointerHits.set(e.steering, (pointerHits.get(e.steering) ?? 0) + 1);
101
- }
102
- }
103
-
104
- const steeringDir = path.join(sdlcRoot, 'context', 'steering');
105
- if (fs.existsSync(steeringDir)) {
106
- for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
107
- const raw = fs.readFileSync(path.join(steeringDir, f), 'utf8');
108
- const { data } = parseFrontmatter(raw);
109
- const inclusion = data.inclusion ?? 'manual';
110
- if (inclusion === 'always') alwaysLines += countEffectiveLines(raw);
111
- steering.push({ file: f, inclusion, pointerHits: pointerHits.get(f) ?? 0 });
112
- }
113
- }
114
-
115
- // Flags plain strings the playbook can surface with suggested fixes.
116
- const flags = [];
117
- if (alwaysLines > CAPS.alwaysBudget) {
118
- flags.push(`residency: always-loaded is ${alwaysLines}/${CAPS.alwaysBudget} lines — distill via /sdlc:steer`);
119
- }
120
- const shipped = changes.filter((c) => c.archived);
121
- for (const s of steering.filter((s) => s.inclusion === 'paths' && s.pointerHits === 0)) {
122
- if (shipped.length >= 2) {
123
- flags.push(`steering/${s.file}: zero pointer hits across ${shipped.length} shipped changes — demote or delete`);
124
- }
125
- }
126
- for (const c of changes) {
127
- if (c.compacts > 0) flags.push(`${c.id}: ${c.compacts} compact event(s) — context overflowed, find the resident artifact`);
128
- if (c.verify.rounds > 3) flags.push(`${c.id}: ${c.verify.rounds} verify rounds — contract or routing needs attention`);
129
- }
130
- for (const c of shipped.filter((c) => c.digest === false)) {
131
- flags.push(`${c.id}: archived without digest.md — ship playbook step 4 was skipped`);
132
- }
133
-
134
- const firstPassRuns = changes.filter((c) => c.verify.firstPass !== null);
135
- const summary = {
136
- active: changes.filter((c) => !c.archived).length,
137
- shipped: shipped.length,
138
- firstPassRate: firstPassRuns.length
139
- ? Number((firstPassRuns.filter((c) => c.verify.firstPass).length / firstPassRuns.length).toFixed(2))
140
- : null,
141
- };
142
-
143
- return {
144
- summary,
145
- changes,
146
- residency: { alwaysLines, budget: CAPS.alwaysBudget },
147
- steering,
148
- flags,
149
- };
150
- }
151
-
152
- const fmtK = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M`
153
- : n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n));
154
-
155
- export function renderReport(report) {
156
- const lines = [];
157
- const { summary, residency } = report;
158
- lines.push(`changes: ${summary.active} active · ${summary.shipped} shipped`
159
- + (summary.firstPassRate != null ? ` · first-pass ${(summary.firstPassRate * 100).toFixed(0)}%` : ''));
160
- lines.push(`residency: ${residency.alwaysLines}/${residency.budget} always-loaded lines`);
161
- for (const c of report.changes) {
162
- const t = c.tokens;
163
- const lead = c.leadTimeMs != null ? ` · lead ${(c.leadTimeMs / 3_600_000).toFixed(1)}h` : '';
164
- lines.push(` ${c.archived ? '✓' : '·'} ${c.id} [${c.tier ?? '?'}]`
165
- + ` ${fmtK(t.input)}in/${fmtK(t.output)}out`
166
- + (c.costUsd != null ? ` $${c.costUsd}` : '')
167
- + ` · verify×${c.verify.rounds}${lead}`);
168
- }
169
- for (const s of report.steering) {
170
- lines.push(` steering/${s.file} [${s.inclusion}] hits:${s.pointerHits}`);
171
- }
172
- for (const f of report.flags) lines.push(` ⚠ ${f}`);
173
- return lines.join('\n');
174
- }
1
+ // Observability aggregation — reads journals + context files, computes the
2
+ // numbers /sdlc:observe reports. Pure given an sdlc root; no LLM involved.
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { parseFrontmatter } from './frontmatter.mjs';
7
+ import { CAPS, countEffectiveLines } from './caps.mjs';
8
+
9
+ function readJournal(dir) {
10
+ const p = path.join(dir, 'journal.ndjson');
11
+ if (!fs.existsSync(p)) return [];
12
+ return fs.readFileSync(p, 'utf8').split('\n').filter(Boolean).map((l) => {
13
+ try { return JSON.parse(l); } catch { return null; }
14
+ }).filter(Boolean);
15
+ }
16
+
17
+ function summarizeChange(dir, id, archived) {
18
+ const events = readJournal(dir);
19
+ const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
20
+ let costUsd = 0;
21
+ let costKnown = false;
22
+ let sessions = 0;
23
+ const verify = { rounds: 0, firstPass: null };
24
+ // null = the journal never said; a run that predates provenance must not be
25
+ // read as independently judged.
26
+ let selfJudged = null;
27
+ // escalations the run passed without a human, because one was pre-approved.
28
+ let preauthorized = 0;
29
+ let guards = 0;
30
+ let compacts = 0;
31
+
32
+ for (const e of events) {
33
+ if (e.event === 'escalation' && e.preauth === 'yes') preauthorized++;
34
+ if (e.event === 'verify' || e.event === 'review') {
35
+ // the weakest link decides: one self-produced judgment marks the change.
36
+ if (e.mode === 'solo') selfJudged = true;
37
+ else if (e.mode === 'panel' && selfJudged === null) selfJudged = false;
38
+ }
39
+ if (e.event === 'session') {
40
+ sessions++;
41
+ for (const k of Object.keys(tokens)) tokens[k] += e.totals?.[k] ?? 0;
42
+ if (typeof e.costUsd === 'number') { costUsd += e.costUsd; costKnown = true; }
43
+ } else if (e.event === 'verify') {
44
+ verify.rounds++;
45
+ if (verify.firstPass === null) verify.firstPass = e.result === 'pass';
46
+ } else if (e.event === 'guard') guards++;
47
+ else if (e.event === 'compact') compacts++;
48
+ }
49
+
50
+ const first = events[0]?.ts ? Date.parse(events[0].ts) : null;
51
+ const shipEvent = events.find((e) => e.event === 'ship');
52
+ const shippedAt = shipEvent?.ts ? Date.parse(shipEvent.ts) : null;
53
+ const leadTimeMs = first != null && shippedAt != null ? shippedAt - first : null;
54
+
55
+ let tier = null;
56
+ let status = null;
57
+ const changePath = path.join(dir, 'change.md');
58
+ if (fs.existsSync(changePath)) {
59
+ const { data } = parseFrontmatter(fs.readFileSync(changePath, 'utf8'));
60
+ tier = data.tier ?? null;
61
+ status = data.status ?? null;
62
+ }
63
+
64
+ return {
65
+ id, archived, tier, status, sessions, tokens,
66
+ costUsd: costKnown ? Number(costUsd.toFixed(4)) : null,
67
+ verify, selfJudged, preauthorized, guards, compacts, leadTimeMs,
68
+ digest: archived ? fs.existsSync(path.join(dir, 'digest.md')) : null,
69
+ };
70
+ }
71
+
72
+ export function buildReport(sdlcRoot) {
73
+ const changes = [];
74
+
75
+ const changesDir = path.join(sdlcRoot, 'changes');
76
+ if (fs.existsSync(changesDir)) {
77
+ for (const d of fs.readdirSync(changesDir, { withFileTypes: true })) {
78
+ if (!d.isDirectory() || d.name === 'archive') continue;
79
+ changes.push(summarizeChange(path.join(changesDir, d.name), d.name, false));
80
+ }
81
+ const archiveDir = path.join(changesDir, 'archive');
82
+ if (fs.existsSync(archiveDir)) {
83
+ for (const d of fs.readdirSync(archiveDir, { withFileTypes: true })) {
84
+ if (!d.isDirectory()) continue;
85
+ changes.push(summarizeChange(path.join(archiveDir, d.name), d.name, true));
86
+ }
87
+ }
88
+ }
89
+
90
+ // Residency: constitution + always-steering effective lines vs budget.
91
+ let alwaysLines = 0;
92
+ const constitutionPath = path.join(sdlcRoot, 'context', 'constitution.md');
93
+ if (fs.existsSync(constitutionPath)) {
94
+ alwaysLines += countEffectiveLines(fs.readFileSync(constitutionPath, 'utf8'));
95
+ }
96
+ const steering = [];
97
+ const pointerHits = new Map();
98
+ for (const c of changes) {
99
+ const dir = c.archived
100
+ ? path.join(sdlcRoot, 'changes', 'archive', c.id)
101
+ : path.join(sdlcRoot, 'changes', c.id);
102
+ for (const e of readJournal(dir)) {
103
+ if (e.event === 'pointer' && e.steering) {
104
+ pointerHits.set(e.steering, (pointerHits.get(e.steering) ?? 0) + 1);
105
+ }
106
+ }
107
+ }
108
+ // Global journal (events with no active change) counts too.
109
+ for (const e of readJournal(path.join(sdlcRoot, '.state'))) {
110
+ if (e.event === 'pointer' && e.steering) {
111
+ pointerHits.set(e.steering, (pointerHits.get(e.steering) ?? 0) + 1);
112
+ }
113
+ }
114
+
115
+ const steeringDir = path.join(sdlcRoot, 'context', 'steering');
116
+ if (fs.existsSync(steeringDir)) {
117
+ for (const f of fs.readdirSync(steeringDir).filter((n) => n.endsWith('.md')).sort()) {
118
+ const raw = fs.readFileSync(path.join(steeringDir, f), 'utf8');
119
+ const { data } = parseFrontmatter(raw);
120
+ const inclusion = data.inclusion ?? 'manual';
121
+ if (inclusion === 'always') alwaysLines += countEffectiveLines(raw);
122
+ steering.push({ file: f, inclusion, pointerHits: pointerHits.get(f) ?? 0 });
123
+ }
124
+ }
125
+
126
+ // Flags plain strings the playbook can surface with suggested fixes.
127
+ const flags = [];
128
+ if (alwaysLines > CAPS.alwaysBudget) {
129
+ flags.push(`residency: always-loaded is ${alwaysLines}/${CAPS.alwaysBudget} lines — distill via /sdlc:steer`);
130
+ }
131
+ const shipped = changes.filter((c) => c.archived);
132
+ for (const s of steering.filter((s) => s.inclusion === 'paths' && s.pointerHits === 0)) {
133
+ if (shipped.length >= 2) {
134
+ flags.push(`steering/${s.file}: zero pointer hits across ${shipped.length} shipped changes — demote or delete`);
135
+ }
136
+ }
137
+ for (const c of changes) {
138
+ if (c.compacts > 0) flags.push(`${c.id}: ${c.compacts} compact event(s) — context overflowed, find the resident artifact`);
139
+ if (c.verify.rounds > 3) flags.push(`${c.id}: ${c.verify.rounds} verify rounds — contract or routing needs attention`);
140
+ }
141
+ for (const c of shipped.filter((c) => c.digest === false)) {
142
+ flags.push(`${c.id}: archived without digest.md — ship playbook step 4 was skipped`);
143
+ }
144
+
145
+ const firstPassRuns = changes.filter((c) => c.verify.firstPass !== null);
146
+ const summary = {
147
+ active: changes.filter((c) => !c.archived).length,
148
+ shipped: shipped.length,
149
+ firstPassRate: firstPassRuns.length
150
+ ? Number((firstPassRuns.filter((c) => c.verify.firstPass).length / firstPassRuns.length).toFixed(2))
151
+ : null,
152
+ };
153
+
154
+ return {
155
+ summary,
156
+ changes,
157
+ residency: { alwaysLines, budget: CAPS.alwaysBudget },
158
+ steering,
159
+ flags,
160
+ };
161
+ }
162
+
163
+ const fmtK = (n) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M`
164
+ : n >= 1_000 ? `${(n / 1_000).toFixed(1)}k` : String(n));
165
+
166
+ export function renderReport(report) {
167
+ const lines = [];
168
+ const { summary, residency } = report;
169
+ lines.push(`changes: ${summary.active} active · ${summary.shipped} shipped`
170
+ + (summary.firstPassRate != null ? ` · first-pass ${(summary.firstPassRate * 100).toFixed(0)}%` : ''));
171
+ lines.push(`residency: ${residency.alwaysLines}/${residency.budget} always-loaded lines`);
172
+ for (const c of report.changes) {
173
+ const t = c.tokens;
174
+ const lead = c.leadTimeMs != null ? ` · lead ${(c.leadTimeMs / 3_600_000).toFixed(1)}h` : '';
175
+ lines.push(` ${c.archived ? '✓' : '·'} ${c.id} [${c.tier ?? '?'}]`
176
+ + ` ${fmtK(t.input)}in/${fmtK(t.output)}out`
177
+ + (c.costUsd != null ? ` $${c.costUsd}` : '')
178
+ + ` · verify×${c.verify.rounds}${c.selfJudged === true ? ' · self-judged' : ''}`
179
+ + (c.preauthorized > 0 ? ` · unattended×${c.preauthorized}` : '')
180
+ + `${lead}`);
181
+ }
182
+ for (const s of report.steering) {
183
+ lines.push(` steering/${s.file} [${s.inclusion}] hits:${s.pointerHits}`);
184
+ }
185
+ for (const f of report.flags) lines.push(` ⚠ ${f}`);
186
+ return lines.join('\n');
187
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warnyin/sdlc",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Spec-driven, AI-driven SDLC framework — token-lean specs, contract-first changes, autonomous pipeline with managed hooks. Operationalizes the Day-1 'New SDLC with Vibe Coding' work process.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
- ---
2
- description: Implement — conductor in-session or orchestrator fan-out per [P] wave
3
- argument-hint: "<change-id>"
4
- ---
5
- Read `sdlc/.playbook/build.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Implement — conductor in-session or orchestrator fan-out per [P] wave
3
+ argument-hint: "<change-id> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/build.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,5 +1,5 @@
1
- ---
2
- description: Write tests + evals BEFORE code, generate failing tests, adversarial check (the contract)
3
- argument-hint: "<change-id>"
4
- ---
5
- Read `sdlc/.playbook/contract.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Write tests + evals BEFORE code, generate failing tests, adversarial check (the contract)
3
+ argument-hint: "<change-id> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/contract.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,5 +1,5 @@
1
- ---
2
- description: Record design decisions & trade-offs (deep tier or on signal)
3
- argument-hint: "<change-id>"
4
- ---
5
- Read `sdlc/.playbook/design.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Record design decisions & trade-offs (deep tier or on signal)
3
+ argument-hint: "<change-id> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/design.md` and execute it now. Arguments: $ARGUMENTS
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Report a bug or request a feature in the framework itself — drafts a GitHub issue upstream, redacted, and files it only after you approve
3
+ argument-hint: "[bug|idea] <one line>"
4
+ ---
5
+ Read `sdlc/.playbook/feedback.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,5 +1,5 @@
1
- ---
2
- description: Open a change — triage tier, write Why + Delta spec + Tasks (Requirements)
3
- argument-hint: "<title>"
4
- ---
5
- Read `sdlc/.playbook/new.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Open a change — triage tier, write Why + Delta spec + Tasks (Requirements)
3
+ argument-hint: "<title> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/new.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,5 +1,5 @@
1
- ---
2
- description: Signal-triggered agent panel: architect / security / quality / ops (read-only)
3
- argument-hint: "<change-id>"
4
- ---
5
- Read `sdlc/.playbook/review.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Signal-triggered agent panel: architect / security / quality / ops (read-only)
3
+ argument-hint: "<change-id> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/review.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,5 +1,5 @@
1
- ---
2
- description: Merge Delta into living specs, archive, run the learner, write the digest
3
- argument-hint: "<change-id>"
4
- ---
5
- Read `sdlc/.playbook/ship.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Merge Delta into living specs, archive, run the learner, write the digest
3
+ argument-hint: "<change-id> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/ship.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,5 +1,5 @@
1
- ---
2
- description: Run the contract: full tests + eval rubric; failures route back to build (max 3 rounds)
3
- argument-hint: "<change-id>"
4
- ---
5
- Read `sdlc/.playbook/verify.md` and execute it now. Arguments: $ARGUMENTS
1
+ ---
2
+ description: Run the contract: full tests + eval rubric; failures route back to build (max 3 rounds)
3
+ argument-hint: "<change-id> [--auto]"
4
+ ---
5
+ Read `sdlc/.playbook/verify.md` and execute it now. Arguments: $ARGUMENTS
@@ -1,30 +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
- | /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 | 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). Non-Claude harnesses: `rules-card.md` is embedded in your
32
+ tool's rules file; `npx @warnyin/sdlc validate` is the enforcement floor.