discipline-md 0.1.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/discipline.js +587 -0
  4. package/package.json +40 -0
  5. package/templates/.claude/settings.json +58 -0
  6. package/templates/AGENTS.md +463 -0
  7. package/templates/AGENT_TRACKER.md +138 -0
  8. package/templates/API_REFERENCE.md +131 -0
  9. package/templates/ARCHITECTURE.md +89 -0
  10. package/templates/ASSETS.md +90 -0
  11. package/templates/AUTONOMOUS_QUEUE.md +119 -0
  12. package/templates/BUILD_PLAN.md +89 -0
  13. package/templates/CHANGELOG.md +90 -0
  14. package/templates/CLAUDE.md +89 -0
  15. package/templates/CREDITS.md +109 -0
  16. package/templates/DATA_MODEL.md +88 -0
  17. package/templates/DECISIONS.md +120 -0
  18. package/templates/DEPLOYMENT.md +342 -0
  19. package/templates/HANDOFF.md +289 -0
  20. package/templates/IMPROVEMENT_LOOP.md +103 -0
  21. package/templates/INVESTIGATION.md +154 -0
  22. package/templates/LICENSE +68 -0
  23. package/templates/NOTICE +55 -0
  24. package/templates/OPEN_DECISIONS.md +61 -0
  25. package/templates/PLAYBOOK_FEEDBACK.md +87 -0
  26. package/templates/PROJECT_CONTEXT.md +91 -0
  27. package/templates/README.md +60 -0
  28. package/templates/ROADMAP.md +96 -0
  29. package/templates/SECURITY_AUDIT.md +235 -0
  30. package/templates/SETUP.md +162 -0
  31. package/templates/SPEC.md +105 -0
  32. package/templates/SPEC_WORKFLOW.md +173 -0
  33. package/templates/TODO.md +118 -0
  34. package/templates/USAGE.md +153 -0
  35. package/templates/VERIFICATION_GATE.md +68 -0
  36. package/templates/agents/CROSS_REPO_SYNC.md +124 -0
  37. package/templates/agents/DEBUGGER.md +112 -0
  38. package/templates/agents/PLANNER.md +111 -0
  39. package/templates/agents/README.md +64 -0
  40. package/templates/agents/RECON.md +99 -0
  41. package/templates/agents/SECURITY_REVIEWER.md +123 -0
  42. package/templates/agents/SPEC_ARCHITECT.md +133 -0
  43. package/templates/agents/STAKEHOLDER.md +197 -0
  44. package/templates/agents/_TEMPLATE.md +116 -0
  45. package/templates/agents/optional/ARCHITECT.md +109 -0
  46. package/templates/agents/optional/BACKEND_IMPACT.md +108 -0
  47. package/templates/agents/optional/DOC_AUDIT.md +108 -0
  48. package/templates/agents/optional/FRONTEND_IMPACT.md +109 -0
  49. package/templates/agents/optional/QUEUE_CURATOR.md +114 -0
  50. package/templates/agents/optional/TEST_STRATEGIST.md +107 -0
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, readFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const TEMPLATES_DIR = resolve(__dirname, '..', 'templates');
9
+
10
+ // ── Templates split: 11 core, 16 optional ───────────────────────────────
11
+ // The Spec & Design phase (SPEC_WORKFLOW + SPEC + BUILD_PLAN) is CORE, not
12
+ // optional: a good spec is the load-bearing input to every build, and the
13
+ // framework's whole point is to get you there. See docs/DECISIONS.md 2026-06-14.
14
+ const CORE = [
15
+ 'README.md',
16
+ 'HANDOFF.md',
17
+ 'AGENTS.md',
18
+ 'PROJECT_CONTEXT.md',
19
+ 'TODO.md',
20
+ 'DECISIONS.md',
21
+ 'ROADMAP.md',
22
+ 'CHANGELOG.md',
23
+ 'SPEC_WORKFLOW.md',
24
+ 'SPEC.md',
25
+ 'BUILD_PLAN.md',
26
+ ];
27
+
28
+ const OPTIONAL = [
29
+ 'API_REFERENCE.md',
30
+ 'ARCHITECTURE.md',
31
+ 'ASSETS.md',
32
+ 'AUTONOMOUS_QUEUE.md',
33
+ 'AGENT_TRACKER.md',
34
+ 'CREDITS.md',
35
+ 'DATA_MODEL.md',
36
+ 'DEPLOYMENT.md',
37
+ 'INVESTIGATION.md',
38
+ 'OPEN_DECISIONS.md',
39
+ 'PLAYBOOK_FEEDBACK.md',
40
+ 'IMPROVEMENT_LOOP.md',
41
+ 'VERIFICATION_GATE.md',
42
+ 'SECURITY_AUDIT.md',
43
+ 'USAGE.md',
44
+ 'CLAUDE.md',
45
+ ];
46
+
47
+ // ── Agent role split: 6 core, 6 optional ─────────────────────────────────
48
+ // Core role contracts get installed by `discipline-md init`. Optional roles
49
+ // (the retired-from-default-set roles per docs/DECISIONS.md 2026-05-09)
50
+ // stay in templates/agents/optional/ and are only installed on demand
51
+ // via `discipline-md add-role <NAME>` for projects that need them.
52
+ const CORE_ROLES = [
53
+ 'RECON.md',
54
+ 'PLANNER.md',
55
+ 'DEBUGGER.md',
56
+ 'SECURITY_REVIEWER.md',
57
+ 'CROSS_REPO_SYNC.md',
58
+ 'SPEC_ARCHITECT.md',
59
+ ];
60
+
61
+ const OPTIONAL_ROLES = [
62
+ 'ARCHITECT.md',
63
+ 'DOC_AUDIT.md',
64
+ 'TEST_STRATEGIST.md',
65
+ 'BACKEND_IMPACT.md',
66
+ 'FRONTEND_IMPACT.md',
67
+ 'QUEUE_CURATOR.md',
68
+ ];
69
+
70
+ // Meta files inside templates/agents/ that always travel with the core
71
+ // install (they're not roles themselves — they're scaffolding for adding
72
+ // project-local roles).
73
+ const AGENT_META_FILES = ['_TEMPLATE.md', 'README.md', 'STAKEHOLDER.md'];
74
+
75
+ const REPO_ROOT_FILES = ['LICENSE', 'NOTICE'];
76
+
77
+ // ── helpers ─────────────────────────────────────────────────────────────
78
+ function args() {
79
+ const argv = process.argv.slice(2);
80
+ const command = argv[0] ?? 'help';
81
+ const flags = {};
82
+ const positional = [];
83
+ for (let i = 1; i < argv.length; i++) {
84
+ const a = argv[i];
85
+ if (a.startsWith('--')) {
86
+ flags[a.slice(2)] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
87
+ } else {
88
+ positional.push(a);
89
+ }
90
+ }
91
+ return { command, flags, positional };
92
+ }
93
+
94
+ function copyOne(src, dest) {
95
+ if (existsSync(dest)) {
96
+ console.log(` skip (exists): ${dest}`);
97
+ return false;
98
+ }
99
+ mkdirSync(dirname(dest), { recursive: true });
100
+ copyFileSync(src, dest);
101
+ console.log(` wrote: ${dest}`);
102
+ return true;
103
+ }
104
+
105
+ // Copy only the immediate children of a directory (files only, no subdirs).
106
+ // Used to copy templates/agents/ without dragging the optional/ subfolder along.
107
+ function copyTopLevelFiles(srcDir, destDir) {
108
+ if (!existsSync(srcDir)) return;
109
+ for (const entry of readdirSync(srcDir)) {
110
+ const src = join(srcDir, entry);
111
+ if (statSync(src).isDirectory()) continue;
112
+ copyOne(src, join(destDir, entry));
113
+ }
114
+ }
115
+
116
+ // ── commands ────────────────────────────────────────────────────────────
117
+ function cmdInit(flags) {
118
+ const target = flags.target ? resolve(flags.target) : process.cwd();
119
+ console.log(`\nDiscipline init -> ${target}\n`);
120
+
121
+ console.log('Core 11 templates -> docs/');
122
+ for (const f of CORE) {
123
+ const src = join(TEMPLATES_DIR, f);
124
+ const dest = join(target, 'docs', f);
125
+ if (existsSync(src)) copyOne(src, dest);
126
+ }
127
+
128
+ // Core role contracts — top level of templates/agents/ (excluding optional/)
129
+ const agentsSrc = join(TEMPLATES_DIR, 'agents');
130
+ if (existsSync(agentsSrc)) {
131
+ console.log('\nCore 6 role contracts + meta files -> docs/agents/');
132
+ copyTopLevelFiles(agentsSrc, join(target, 'docs', 'agents'));
133
+ }
134
+
135
+ console.log('\nRepo-root files (LICENSE, NOTICE) -> repo root');
136
+ for (const f of REPO_ROOT_FILES) {
137
+ const src = join(TEMPLATES_DIR, f);
138
+ const dest = join(target, f);
139
+ if (existsSync(src)) copyOne(src, dest);
140
+ }
141
+
142
+ console.log('\nDone. Next:');
143
+ console.log(' 1. Fill in placeholders in docs/ (search for "[" or "<" markers).');
144
+ console.log(' 2. Pick a license in LICENSE (default ships as All Rights Reserved).');
145
+ console.log(' 3. Point your AI agent at docs/AGENTS.md.');
146
+ console.log(' 4. Add optional templates with: npx discipline-md add <name>');
147
+ console.log(` (available: ${OPTIONAL.join(', ')})`);
148
+ console.log(' 5. Add optional agent roles with: npx discipline-md add-role <NAME>');
149
+ console.log(` (available: ${OPTIONAL_ROLES.map((r) => r.replace('.md', '')).join(', ')})`);
150
+ console.log();
151
+ }
152
+
153
+ function cmdAdd(positional) {
154
+ const target = process.cwd();
155
+ if (!positional.length) {
156
+ console.log('Usage: npx discipline-md add <template> [<template>...]');
157
+ console.log('Available optional templates:');
158
+ for (const f of OPTIONAL) console.log(` ${f}`);
159
+ return;
160
+ }
161
+ console.log(`\nDiscipline add -> ${target}/docs/\n`);
162
+ for (const arg of positional) {
163
+ const name = arg.endsWith('.md') ? arg : `${arg}.md`;
164
+ if (!OPTIONAL.includes(name) && !CORE.includes(name)) {
165
+ console.log(` skip (unknown template): ${name}`);
166
+ continue;
167
+ }
168
+ const src = join(TEMPLATES_DIR, name);
169
+ const dest = join(target, 'docs', name);
170
+ if (existsSync(src)) copyOne(src, dest);
171
+ }
172
+ console.log();
173
+ }
174
+
175
+ function cmdAddRole(positional) {
176
+ const target = process.cwd();
177
+ if (!positional.length) {
178
+ console.log('Usage: npx discipline-md add-role <ROLE> [<ROLE>...]');
179
+ console.log('Available optional agent roles:');
180
+ for (const f of OPTIONAL_ROLES) console.log(` ${f.replace('.md', '')}`);
181
+ console.log('\nThese roles were retired from the lean default set on 2026-05-09');
182
+ console.log('(see docs/DECISIONS.md). Add them only if your project needs them.');
183
+ return;
184
+ }
185
+ console.log(`\nDiscipline add-role -> ${target}/docs/agents/\n`);
186
+ for (const arg of positional) {
187
+ const upper = arg.toUpperCase();
188
+ const name = upper.endsWith('.MD') ? upper : `${upper}.md`;
189
+ const properName = name.endsWith('.md') ? name : `${name}.md`;
190
+ if (!OPTIONAL_ROLES.includes(properName) && !CORE_ROLES.includes(properName)) {
191
+ console.log(` skip (unknown role): ${arg}`);
192
+ continue;
193
+ }
194
+ // Optional roles live in templates/agents/optional/; core roles at templates/agents/.
195
+ const subdir = OPTIONAL_ROLES.includes(properName) ? 'optional' : '';
196
+ const src = subdir
197
+ ? join(TEMPLATES_DIR, 'agents', subdir, properName)
198
+ : join(TEMPLATES_DIR, 'agents', properName);
199
+ const dest = join(target, 'docs', 'agents', properName);
200
+ if (existsSync(src)) copyOne(src, dest);
201
+ }
202
+ console.log();
203
+ }
204
+
205
+ // ── lint ────────────────────────────────────────────────────────────────
206
+ // Mechanically checkable slices of the framework's gates: a gate a weak
207
+ // model can forget is not a gate. Lints the TARGET's docs/ only — never
208
+ // this package's own templates/ (templates legitimately contain
209
+ // placeholders and example residue).
210
+
211
+ // Allowed tag values per the legends in templates/AGENTS.md, templates/TODO.md
212
+ // and templates/AUTONOMOUS_QUEUE.md (union of the Claude-specific and
213
+ // model-agnostic spellings those legends document).
214
+ const TAG_VOCAB = {
215
+ autonomy: ['safe', 'review', 'needs-human-collab'],
216
+ size: ['XS', 'S', 'M', 'L', 'XL'],
217
+ risk: ['low', 'med', 'high'],
218
+ scope: ['isolated', 'cross-repo', 'cross-cutting', 'infra'],
219
+ tier: ['haiku', 'sonnet', 'opus', 'frontier', 'workhorse', 'recon'],
220
+ };
221
+
222
+ // Hot-path size budgets (bytes). Over budget = cold content leaking in.
223
+ const SIZE_BUDGETS = [
224
+ ['docs/AGENTS.md', 36 * 1024],
225
+ ['docs/HANDOFF.md', 24 * 1024],
226
+ ['docs/TODO.md', 16 * 1024],
227
+ ['CLAUDE.md', 12 * 1024],
228
+ ['docs/CLAUDE.md', 12 * 1024],
229
+ ];
230
+
231
+ // Placeholder patterns confirmed present in the shipped templates.
232
+ const PLACEHOLDER_PATTERNS = [
233
+ /\[TBD\]/,
234
+ /<repo-path>/,
235
+ /(?<!_)<YYYY-MM-DD>/, // `SECURITY_AUDIT_<YYYY-MM-DD>.md` is a filename convention, not residue
236
+ /<e\.g\.[^>]*>/,
237
+ /<docs\/[^>]*>/,
238
+ ];
239
+
240
+ function lintRead(path) {
241
+ if (!existsSync(path)) return null;
242
+ return readFileSync(path, 'utf8').split(/\r?\n/);
243
+ }
244
+
245
+ // Per-line mask: true where the line is inside a fenced code block or an
246
+ // HTML comment — examples and commented-out scaffolding are not findings.
247
+ function maskedLines(lines) {
248
+ const mask = [];
249
+ let fence = false;
250
+ let comment = false;
251
+ for (const line of lines) {
252
+ const wasMasked = fence || comment;
253
+ if (/^\s*(```|~~~)/.test(line)) fence = !fence;
254
+ if (!fence) {
255
+ if (line.includes('<!--')) comment = true;
256
+ mask.push(wasMasked || fence || comment || line.includes('<!--'));
257
+ if (line.includes('-->')) comment = false;
258
+ } else {
259
+ mask.push(true);
260
+ }
261
+ }
262
+ return mask;
263
+ }
264
+
265
+ // Loose containment normalizer for cross-file title matching.
266
+ function normalize(s) {
267
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
268
+ }
269
+
270
+ // First plain title fragment of a queue/TODO bullet: text up to the first
271
+ // em-dash separator or tag block, stripped of markdown decoration.
272
+ function bulletTitle(text) {
273
+ let t = text.split(/\s+—\s+/)[0];
274
+ t = t.split('`[')[0];
275
+ return t.replace(/[*_`]/g, '').trim();
276
+ }
277
+
278
+ function cmdLint(flags) {
279
+ const target = flags.target ? resolve(flags.target) : process.cwd();
280
+ const docsDir = join(target, 'docs');
281
+ const findings = [];
282
+ const add = (file, line, level, rule, msg) => findings.push({ file, line, level, rule, msg });
283
+
284
+ if (!existsSync(docsDir)) {
285
+ console.log(`\nDiscipline lint: no docs/ directory at ${target} — nothing to lint.\n`);
286
+ return;
287
+ }
288
+
289
+ const todoPath = join(docsDir, 'TODO.md');
290
+ const queuePath = join(docsDir, 'AUTONOMOUS_QUEUE.md');
291
+ const todo = lintRead(todoPath);
292
+ const queue = lintRead(queuePath);
293
+ const roadmap = lintRead(join(docsDir, 'ROADMAP.md'));
294
+
295
+ // todo-done-residue (ERROR): the cleanup gate deletes shipped entries.
296
+ if (todo) {
297
+ const mask = maskedLines(todo);
298
+ todo.forEach((line, i) => {
299
+ if (!mask[i] && /^\s*[-*]\s*\[[xX]\]/.test(line)) {
300
+ add(todoPath, i + 1, 'ERROR', 'todo-done-residue',
301
+ 'checked-off entry — shipped TODO entries are DELETED once captured in CHANGELOG, never marked [x]');
302
+ }
303
+ });
304
+ }
305
+
306
+ // tag-validity (ERROR): unknown value in a [key: value] tag.
307
+ for (const [path, lines] of [[todoPath, todo], [join(docsDir, 'ROADMAP.md'), roadmap], [queuePath, queue]]) {
308
+ if (!lines) continue;
309
+ const mask = maskedLines(lines);
310
+ lines.forEach((line, i) => {
311
+ if (mask[i]) return;
312
+ for (const m of line.matchAll(/\[(autonomy|size|risk|scope|tier):\s*([^\]]+)\]/g)) {
313
+ const [, key, raw] = m;
314
+ const value = raw.trim();
315
+ // Legend lines spell the alternatives ("safe|review|…") — skip those.
316
+ if (value.includes('|') || value.includes('…') || value.includes('<')) continue;
317
+ const allowed = TAG_VOCAB[key];
318
+ const ok = key === 'size'
319
+ ? allowed.includes(value.toUpperCase())
320
+ : allowed.includes(value.toLowerCase());
321
+ if (!ok) {
322
+ add(path, i + 1, 'ERROR', 'tag-validity',
323
+ `[${key}: ${value}] is not a documented value (allowed: ${allowed.join('|')})`);
324
+ }
325
+ }
326
+ });
327
+ }
328
+
329
+ // queue-orphan (WARN): Active Queue entry with no trace in TODO/ROADMAP.
330
+ if (queue && (todo || roadmap)) {
331
+ const haystack = normalize([...(todo ?? []), ...(roadmap ?? [])].join('\n'));
332
+ const mask = maskedLines(queue);
333
+ let inActive = false;
334
+ queue.forEach((line, i) => {
335
+ if (/^##\s/.test(line)) inActive = /^##\s+Active Queue/i.test(line);
336
+ if (!inActive || mask[i]) return;
337
+ const m = line.match(/^\s*-\s*\[ \]\s*(.+)/);
338
+ if (!m || m[1].startsWith('(')) return;
339
+ const title = normalize(bulletTitle(m[1]));
340
+ if (title.length < 8) return; // too short to match meaningfully
341
+ if (!haystack.includes(title)) {
342
+ add(queuePath, i + 1, 'WARN', 'queue-orphan',
343
+ `queue entry "${bulletTitle(m[1])}" not found in docs/TODO.md or docs/ROADMAP.md — the queue is pointers, not a second backlog`);
344
+ }
345
+ });
346
+ }
347
+
348
+ // decisions-structure (WARN): entry missing the pinned fields from
349
+ // templates/DECISIONS.md (Status / Decision / Context / Consequences;
350
+ // bold-label and H3 spellings both accepted).
351
+ const decisionsPath = join(docsDir, 'DECISIONS.md');
352
+ const decisions = lintRead(decisionsPath);
353
+ if (decisions) {
354
+ const headings = [];
355
+ decisions.forEach((line, i) => {
356
+ if (/^##\s+\d{4}-\d{2}-\d{2}\b/.test(line)) headings.push(i);
357
+ });
358
+ headings.forEach((start, idx) => {
359
+ const end = idx + 1 < headings.length ? headings[idx + 1] : decisions.length;
360
+ const body = decisions.slice(start + 1, end).join('\n');
361
+ const missing = ['Status', 'Decision', 'Context', 'Consequences'].filter((f) =>
362
+ !new RegExp(`(^|\\n)\\s*(\\*\\*)?${f}(\\*\\*)?\\s*:|(^|\\n)###\\s+${f}`).test(body));
363
+ if (missing.length) {
364
+ add(decisionsPath, start + 1, 'WARN', 'decisions-structure',
365
+ `entry missing pinned field(s): ${missing.join(', ')}`);
366
+ }
367
+ });
368
+ }
369
+
370
+ // hot-doc-size (WARN)
371
+ for (const [rel, budget] of SIZE_BUDGETS) {
372
+ const path = join(target, rel);
373
+ if (!existsSync(path)) continue;
374
+ const size = statSync(path).size;
375
+ if (size > budget) {
376
+ add(path, null, 'WARN', 'hot-doc-size',
377
+ `${(size / 1024).toFixed(1)}KB exceeds the ${budget / 1024}KB hot-path budget — cold content may be leaking into the hot path`);
378
+ }
379
+ }
380
+
381
+ // placeholder-residue (WARN): unfilled scaffold placeholders in docs/.
382
+ const walk = (dir) => readdirSync(dir).flatMap((entry) => {
383
+ const p = join(dir, entry);
384
+ return statSync(p).isDirectory() ? walk(p) : (p.endsWith('.md') ? [p] : []);
385
+ });
386
+ for (const path of walk(docsDir)) {
387
+ const lines = lintRead(path);
388
+ const mask = maskedLines(lines);
389
+ lines.forEach((line, i) => {
390
+ if (mask[i]) return;
391
+ const hit = PLACEHOLDER_PATTERNS.find((re) => re.test(line));
392
+ if (hit) {
393
+ add(path, i + 1, 'WARN', 'placeholder-residue',
394
+ `unfilled scaffold placeholder (${line.match(hit)[0]})`);
395
+ }
396
+ });
397
+ }
398
+
399
+ // handoff-stale (WARN)
400
+ const handoffPath = join(docsDir, 'HANDOFF.md');
401
+ const handoff = lintRead(handoffPath);
402
+ if (handoff) {
403
+ handoff.forEach((line, i) => {
404
+ const m = line.match(/last updated\s*[:—–-]?\s*\**\s*(\d{4}-\d{2}-\d{2})/i);
405
+ if (!m) return;
406
+ const age = (Date.now() - new Date(m[1]).getTime()) / 86400000;
407
+ if (Number.isFinite(age) && age > 60) {
408
+ add(handoffPath, i + 1, 'WARN', 'handoff-stale',
409
+ `Last updated ${m[1]} is ${Math.floor(age)} days old — treat as a smell, verify before trusting`);
410
+ }
411
+ });
412
+ }
413
+
414
+ // todo-two-gate (WARN): [autonomy: safe] is necessary but NOT sufficient —
415
+ // the item must also be in the curated queue. Skipped when the project
416
+ // has no AUTONOMOUS_QUEUE.md at all.
417
+ if (todo && queue) {
418
+ const queueText = normalize(queue.join('\n'));
419
+ const mask = maskedLines(todo);
420
+ todo.forEach((line, i) => {
421
+ if (mask[i] || !line.includes('[autonomy: safe]')) return;
422
+ const m = line.match(/^\s*[-*]\s*(?:\[.\]\s*)?(.+)/);
423
+ if (!m) return;
424
+ const title = normalize(bulletTitle(m[1]));
425
+ if (title.length < 8) return;
426
+ if (!queueText.includes(title)) {
427
+ add(todoPath, i + 1, 'WARN', 'todo-two-gate',
428
+ 'tagged [autonomy: safe] but not listed in docs/AUTONOMOUS_QUEUE.md — the tag alone is not sufficient for unattended execution');
429
+ }
430
+ });
431
+ }
432
+
433
+ // ── Spec & Design phase rules (SPEC.md / BUILD_PLAN.md) ──
434
+ // These only fire when a project has opted into the spec phase (the files
435
+ // exist); they skip gracefully otherwise.
436
+
437
+ // spec-req-untagged (WARN): every requirement in SPEC.md ## Requirements must
438
+ // carry [AUTO] or [HUMAN] — an untagged requirement has no verification
439
+ // posture, and the verifier suite can't know whether to check it.
440
+ // spec-auto-coverage (WARN): each [AUTO] requirement needs an entry in
441
+ // ## Acceptance Tests — that section is the seed of the verifier suite.
442
+ const specPath = join(docsDir, 'SPEC.md');
443
+ const spec = lintRead(specPath);
444
+ if (spec) {
445
+ const mask = maskedLines(spec);
446
+ const autoReqs = new Set();
447
+ let section = '';
448
+ spec.forEach((line, i) => {
449
+ const h = line.match(/^##\s+(.+?)\s*$/);
450
+ if (h) section = normalize(h[1]);
451
+ if (mask[i]) return;
452
+ const m = line.match(/^\s*[-*]\s*\*\*(R\d+)\*\*\s*(.*)$/);
453
+ if (!m || section !== 'requirements') return;
454
+ const [, id, rest] = m;
455
+ const hasAuto = /\[AUTO\]/.test(rest);
456
+ const hasHuman = /\[HUMAN\]/.test(rest);
457
+ if (!hasAuto && !hasHuman) {
458
+ add(specPath, i + 1, 'WARN', 'spec-req-untagged',
459
+ `requirement ${id} is not tagged [AUTO] or [HUMAN] — every requirement needs a verification posture`);
460
+ }
461
+ if (hasAuto) autoReqs.add(id);
462
+ });
463
+ const covered = new Set();
464
+ let inAccept = false;
465
+ spec.forEach((line) => {
466
+ const h = line.match(/^##\s+(.+?)\s*$/);
467
+ if (h) inAccept = normalize(h[1]) === 'acceptance tests';
468
+ if (!inAccept) return;
469
+ for (const m of line.matchAll(/\bR\d+\b/g)) covered.add(m[0]);
470
+ });
471
+ for (const id of autoReqs) {
472
+ if (!covered.has(id)) {
473
+ add(specPath, null, 'WARN', 'spec-auto-coverage',
474
+ `[AUTO] requirement ${id} has no entry in ## Acceptance Tests — it cannot become a verifier check`);
475
+ }
476
+ }
477
+ }
478
+
479
+ // story-dep-tag (WARN): every BUILD_PLAN.md story carries a [dep: …] marker
480
+ // (the dependency graph IS the build schedule — [dep: none] = parallel-safe).
481
+ // story-traceability (WARN): every story traces back to a SPEC.md requirement
482
+ // via "satisfies: R#".
483
+ const planPath = join(docsDir, 'BUILD_PLAN.md');
484
+ const plan = lintRead(planPath);
485
+ if (plan) {
486
+ const mask = maskedLines(plan);
487
+ const heads = [];
488
+ plan.forEach((line, i) => {
489
+ if (!mask[i] && /^###\s+S-\d+\b/.test(line)) heads.push(i);
490
+ });
491
+ heads.forEach((start, idx) => {
492
+ const end = idx + 1 < heads.length ? heads[idx + 1] : plan.length;
493
+ const title = plan[start].replace(/^###\s+/, '').trim();
494
+ const body = plan.slice(start + 1, end).join('\n');
495
+ if (!/\[dep:\s*[^\]]+\]/.test(body)) {
496
+ add(planPath, start + 1, 'WARN', 'story-dep-tag',
497
+ `story "${title}" has no [dep: …] marker — mark [dep: none] (parallel-safe) or [dep: S-xx] (serial)`);
498
+ }
499
+ if (!/satisfies\**\s*:\s*\**\s*R\d+/i.test(body)) {
500
+ add(planPath, start + 1, 'WARN', 'story-traceability',
501
+ `story "${title}" has no "satisfies: R#" — every story must trace to a SPEC.md requirement`);
502
+ }
503
+ });
504
+ }
505
+
506
+ // ── report ──
507
+ const errors = findings.filter((f) => f.level === 'ERROR').length;
508
+ const warnings = findings.length - errors;
509
+ console.log(`\nDiscipline lint -> ${docsDir}\n`);
510
+ const byFile = new Map();
511
+ for (const f of findings) {
512
+ if (!byFile.has(f.file)) byFile.set(f.file, []);
513
+ byFile.get(f.file).push(f);
514
+ }
515
+ for (const [file, list] of byFile) {
516
+ console.log(file);
517
+ for (const f of list) {
518
+ console.log(` ${f.line ? `line ${f.line}` : '(file)'} [${f.level}] ${f.rule}: ${f.msg}`);
519
+ }
520
+ console.log();
521
+ }
522
+ console.log(`${errors} errors, ${warnings} warnings`);
523
+ if (errors > 0 || (flags.strict && warnings > 0)) process.exitCode = 1;
524
+ }
525
+
526
+ function cmdList() {
527
+ console.log('\nCore 11 templates (always installed by `discipline-md init`):');
528
+ for (const f of CORE) console.log(` ${f}`);
529
+ console.log('\nOptional templates (opt-in via `discipline-md add <name>`):');
530
+ for (const f of OPTIONAL) console.log(` ${f}`);
531
+ console.log('\nCore 6 agent role contracts (always installed by `discipline-md init`):');
532
+ for (const f of CORE_ROLES) console.log(` ${f.replace('.md', '')}`);
533
+ console.log('\nOptional agent roles (opt-in via `discipline-md add-role <NAME>`):');
534
+ for (const f of OPTIONAL_ROLES) console.log(` ${f.replace('.md', '')}`);
535
+ console.log('\nRepo-root files (installed by `discipline-md init`):');
536
+ for (const f of REPO_ROOT_FILES) console.log(` ${f}`);
537
+ console.log('\nHygiene: `discipline-md lint [--target <path>] [--strict]` checks docs/ against');
538
+ console.log('the cleanup gate, tag legend, and two-gate autonomy rule.');
539
+ console.log();
540
+ }
541
+
542
+ function cmdHelp() {
543
+ console.log(`
544
+ Discipline — opinionated agentic-workflow framework
545
+
546
+ Usage:
547
+ npx discipline-md init [--target <path>] Scaffold Core 11 + Core 6 roles into <path>
548
+ npx discipline-md add <template> Add an optional template to ./docs/
549
+ npx discipline-md add-role <ROLE> Add an optional agent role to ./docs/agents/
550
+ npx discipline-md lint [--target <path>] Lint <path>/docs/ for gate violations
551
+ [--strict] (--strict: warnings also fail the run)
552
+ npx discipline-md list Show all templates and roles
553
+ npx discipline-md help This message
554
+
555
+ Lint checks the mechanical half of the framework's gates: [x] residue in
556
+ TODO.md (cleanup gate), unknown tag values, queue entries orphaned from
557
+ TODO/ROADMAP, [autonomy: safe] items missing from AUTONOMOUS_QUEUE.md
558
+ (two-gate rule), oversized hot-path docs, unfilled placeholders, stale
559
+ handoffs. It also checks the Spec & Design phase: SPEC.md requirements are
560
+ [AUTO]/[HUMAN]-tagged and acceptance-covered, and BUILD_PLAN.md stories carry
561
+ [dep: …] markers and trace to a requirement. Exit 1 on any error (or any
562
+ warning with --strict).
563
+
564
+ Lean defaults: Core 11 templates + Core 6 roles ship by default — including the
565
+ Spec & Design phase (SPEC_WORKFLOW + SPEC + BUILD_PLAN + the SPEC_ARCHITECT
566
+ role), which is core because a good spec is the load-bearing input to every
567
+ build. The remaining 16 templates and 6 retired roles are opt-in for projects
568
+ that need them — see docs/DECISIONS.md for the rationale.
569
+
570
+ Docs: https://github.com/realmwalk/discipline
571
+ `);
572
+ }
573
+
574
+ function main() {
575
+ const { command, flags, positional } = args();
576
+ switch (command) {
577
+ case 'init': return cmdInit(flags);
578
+ case 'add': return cmdAdd(positional);
579
+ case 'add-role': return cmdAddRole(positional);
580
+ case 'lint': return cmdLint(flags);
581
+ case 'list': return cmdList();
582
+ case 'help':
583
+ default: return cmdHelp();
584
+ }
585
+ }
586
+
587
+ main();
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "discipline-md",
3
+ "version": "0.1.0",
4
+ "description": "Opinionated agentic-workflow framework for teams running AI coding agents that have hit the maintenance wall.",
5
+ "keywords": [
6
+ "agents",
7
+ "ai-coding",
8
+ "claude-code",
9
+ "cursor",
10
+ "documentation",
11
+ "framework",
12
+ "playbook",
13
+ "scaffold"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/realmwalk/discipline.git"
18
+ },
19
+ "homepage": "https://github.com/realmwalk/discipline#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/realmwalk/discipline/issues"
22
+ },
23
+ "license": "MIT",
24
+ "author": "John Hardy",
25
+ "type": "module",
26
+ "files": [
27
+ "bin/",
28
+ "templates/",
29
+ "LICENSE",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "test": "echo \"No tests yet\" && exit 0"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "private": false,
39
+ "bin": "bin/discipline.js"
40
+ }