@rungs/cli 0.3.0 → 0.4.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 (74) hide show
  1. package/README.md +6 -6
  2. package/dist/cli.js +2194 -488
  3. package/dist/cli.js.map +4 -4
  4. package/modules/README.md +25 -3
  5. package/modules/adr/files/{{path}}/README.md +1 -1
  6. package/modules/adr/gates/adr.toml +1 -1
  7. package/modules/adr/module.toml +1 -1
  8. package/modules/audit/fragments/AGENTS.md +2 -2
  9. package/modules/audit/module.toml +1 -1
  10. package/modules/audit/skills/assess/SKILL.md +1 -1
  11. package/modules/backlog/files/docs/{{root}}/BACKLOG.md +1 -1
  12. package/modules/backlog/files/docs/{{root}}/README.md +2 -2
  13. package/modules/backlog/files/docs/{{root}}/archive/README.md +1 -1
  14. package/modules/backlog/files/docs/{{root}}/items/README.md +1 -1
  15. package/modules/backlog/fragments/AGENTS.md +2 -2
  16. package/modules/backlog/module.toml +1 -1
  17. package/modules/backlog/skills/work-item/SKILL.md +1 -1
  18. package/modules/ci/files/{{workflow_path}} +3 -3
  19. package/modules/ci/module.toml +1 -1
  20. package/modules/concurrency/files/docs/concurrent-sessions.md +66 -18
  21. package/modules/concurrency/fragments/AGENTS.md +5 -4
  22. package/modules/concurrency/fragments/gitattributes +2 -2
  23. package/modules/concurrency/gates/concurrency.toml +3 -3
  24. package/modules/concurrency/module.toml +1 -1
  25. package/modules/doc-authority/files/{{registry_path}} +1 -1
  26. package/modules/doc-authority/module.toml +1 -1
  27. package/modules/findings/files/docs/{{backlog.root}}/FINDINGS.md +1 -1
  28. package/modules/findings/gates/findings.toml +5 -0
  29. package/modules/findings/module.toml +1 -1
  30. package/modules/findings/skills/record-finding/SKILL.md +1 -1
  31. package/modules/gates/files/.ai/gates.toml +1 -1
  32. package/modules/gates/fragments/AGENTS.md +6 -5
  33. package/modules/gates/module.toml +1 -1
  34. package/modules/instructions/files/.ai/rules/README.md +2 -2
  35. package/modules/instructions/files/.ai/rungs.mjs +52 -0
  36. package/modules/instructions/files/AGENTS.md +4 -2
  37. package/modules/instructions/files/CLAUDE.md +1 -1
  38. package/modules/instructions/fragments/AGENTS.md +2 -2
  39. package/modules/instructions/gates/core.toml +2 -2
  40. package/modules/instructions/module.toml +1 -1
  41. package/modules/release/files/{{changelog_dir}}/CONSUMED_THROUGH +1 -0
  42. package/modules/release/gates/release.toml +169 -17
  43. package/modules/release/module.toml +9 -5
  44. package/modules/release/skills/cut-release/SKILL.md +43 -15
  45. package/modules/session/files/{{archive}}/README.md +1 -1
  46. package/modules/session/files/{{path}} +2 -2
  47. package/modules/session/module.toml +1 -1
  48. package/modules/specs/files/{{path}}/README.md +2 -2
  49. package/modules/specs/module.toml +1 -1
  50. package/modules/workflows/module.toml +1 -1
  51. package/modules/workflows/rules/planning-tiers.md +1 -1
  52. package/package.json +3 -2
  53. package/src/add.ts +204 -48
  54. package/src/backlog.ts +354 -48
  55. package/src/check.ts +54 -33
  56. package/src/cli.ts +196 -69
  57. package/src/concurrency.ts +628 -42
  58. package/src/detect.ts +11 -3
  59. package/src/emitted-path.ts +274 -0
  60. package/src/engine-table.ts +66 -0
  61. package/src/engines.ts +40 -32
  62. package/src/engines2.ts +424 -29
  63. package/src/engines3.ts +115 -23
  64. package/src/explain.ts +3 -7
  65. package/src/help.ts +43 -0
  66. package/src/lifecycle.ts +95 -31
  67. package/src/manifest.ts +41 -5
  68. package/src/render.ts +106 -21
  69. package/src/selftest.ts +87 -10
  70. package/src/storage-key.ts +20 -0
  71. package/src/substitute.ts +47 -5
  72. package/src/text.ts +11 -0
  73. package/src/types.ts +16 -3
  74. package/src/version-source.ts +144 -0
package/src/cli.ts CHANGED
@@ -3,16 +3,18 @@ import { fileURLToPath } from 'node:url';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import { auditModules, loadAllModules } from './manifest.ts';
5
5
  import { detect, scanRepo } from './detect.ts';
6
- import { addModule, adoptableGates, blockedByParadigm, registerGates, resolveInstallOrder, writeInstallRecord } from './add.ts';
7
- import { render, writeReport, type Harness } from './render.ts';
6
+ import { addModule, adoptableGates, blockedByConflict, blockedByParadigm, moduleEmissionCandidates, preflightModuleEmissions, prospectiveRuleEmissions, type ConflictBlock, registerGates, resolveInstallOrder, writeInstallRecord } from './add.ts';
7
+ import { preflightRender, render, writeReport, type Harness } from './render.ts';
8
8
  import { resolveParams } from './substitute.ts';
9
9
  import { appendLedger, type GateRun, ledgerQuestions, loadRegistry, runGates, UnknownTierError } from './check.ts';
10
10
  import { applyUpgrade, eject, planUpgrade, PROFILES, readRecord, setupGit } from './lifecycle.ts';
11
11
  import { explain, IN_SCOPE as EXPLAINABLE } from './explain.ts';
12
- import { applyArchive, planArchive } from './backlog.ts';
12
+ import { applyArchive, planArchive, resolveArchiveTree } from './backlog.ts';
13
13
  import { land, preflight, sessionStart, worktrees } from './concurrency.ts';
14
14
  import { existsSync } from 'node:fs';
15
15
  import type { DetectResult, Manifest } from './types.ts';
16
+ import { UnsafeEmittedPathError } from './emitted-path.ts';
17
+ import { COMMANDS, FLAGS } from './help.ts';
16
18
 
17
19
  const HERE = dirname(fileURLToPath(import.meta.url));
18
20
  const MODULES = join(HERE, '..', 'modules');
@@ -26,6 +28,12 @@ const c = {
26
28
  cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
27
29
  };
28
30
 
31
+ function pathRefusal(error: unknown): number {
32
+ if (!(error instanceof UnsafeEmittedPathError)) throw error;
33
+ console.log(c.red(`\n refused: ${error.message}\n`) + c.dim(' Nothing was written. Fix the named path or repository alias and retry.\n'));
34
+ return 1;
35
+ }
36
+
29
37
  const STATE_LABEL: Record<DetectResult['state'], string> = {
30
38
  absent: c.dim('absent'),
31
39
  'ours-current': c.green('ours'),
@@ -40,8 +48,17 @@ function cmdModules(showParams = false) {
40
48
  console.log(c.bold(`\n${mods.length} modules\n`));
41
49
  for (const m of mods) {
42
50
  const deps = m.requires.length ? c.dim(` ← ${m.requires.join(', ')}`) : '';
43
- console.log(` ${c.bold(m.name.padEnd(14))} rung ${m.rung}${deps}`);
51
+ // A `designed` module is marked wherever a module is named, because a
52
+ // distinction the manifest declares and no surface prints is the field
53
+ // being unread all over again (F-038, and F-037 nearly repeated it). The
54
+ // extracted case is unmarked: it is what every bundled module is, and a
55
+ // badge on all fifteen would carry no information.
56
+ const designed = m.provenance.kind === 'designed' ? c.yellow(' designed') : '';
57
+ console.log(` ${c.bold(m.name.padEnd(14))} rung ${m.rung}${deps}${designed}`);
44
58
  console.log(` ${' '.repeat(14)} ${c.dim(m.summary)}`);
59
+ if (designed) {
60
+ console.log(` ${' '.repeat(14)} ${c.dim(`not extracted — ${firstSentence(m.provenance.rationale ?? '')}`)}`);
61
+ }
45
62
  // Rendered from the manifest at the moment it is asked for, never written down. A committed
46
63
  // parameter table would be correct the day it was generated and silently wrong the day a
47
64
  // default moved — which is the failure this flag exists to answer (WI-006).
@@ -84,7 +101,12 @@ function cmdDoctor(target: string, doExplain = false) {
84
101
  console.log(c.bold(`\nrungs doctor — ${root}\n`));
85
102
 
86
103
  const files = scanRepo(root);
87
- const record = readRecord(root);
104
+ let record;
105
+ try {
106
+ record = readRecord(root);
107
+ } catch (error) {
108
+ return pathRefusal(error);
109
+ }
88
110
  console.log(
89
111
  c.dim(` scanned ${files.length} files`) +
90
112
  (record ? c.dim(` · installed ${Object.keys(record.modules).length} module(s)`) : c.dim(' · not a rungs repo')) +
@@ -95,10 +117,15 @@ function cmdDoctor(target: string, doExplain = false) {
95
117
  Object.entries(record?.modules ?? {}).flatMap(([n, e]) => (e.params ? [[n, e.params]] : [])),
96
118
  ), root);
97
119
  const skillsDir = record?.harnesses.includes('claude') === false ? '.agents/skills' : '.claude/skills';
98
- const results = mods.map((m) => {
99
- const installed = record?.modules[m.name];
100
- return detect(m, root, files, installed ? { ...installed, skillsDir, params_all: params } : undefined);
101
- });
120
+ let results;
121
+ try {
122
+ results = mods.map((m) => {
123
+ const installed = record?.modules[m.name];
124
+ return detect(m, root, files, installed ? { ...installed, skillsDir, params_all: params } : undefined);
125
+ });
126
+ } catch (error) {
127
+ return pathRefusal(error);
128
+ }
102
129
  const byState = (s: DetectResult['state']) => results.filter((r) => r.state === s);
103
130
 
104
131
  for (const r of results) {
@@ -386,6 +413,41 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
386
413
  console.log(c.dim(' You will have two systems for one job. That is a choice, not a merge.\n'));
387
414
  }
388
415
 
416
+ // F-038: a module's `[conflicts]` was parsed and read by nothing, so a
417
+ // declared incompatibility installed in silence. It refuses the same way a
418
+ // paradigm does — state it and stop — but it is computed separately, because
419
+ // a paradigm is *inferred from files* and a conflict is *declared by an
420
+ // author*, and a refusal should say which of the two it is.
421
+ //
422
+ // What counts as present: what the repo already has, plus everything else in
423
+ // this install set. Two modules that cannot coexist cannot arrive together
424
+ // either.
425
+ let record;
426
+ try {
427
+ record = readRecord(root);
428
+ } catch (error) {
429
+ return pathRefusal(error);
430
+ }
431
+ const present = new Set([...Object.keys(record?.modules ?? {}), ...order.map((m) => m.name)]);
432
+ const declared = blockedByConflict(order, present, mods);
433
+ const conflictOverride = flags.has('--confirm-conflict');
434
+
435
+ // Computed even when overridden, and printed — for the reason the paradigm
436
+ // override just above says: an override that prints nothing is
437
+ // indistinguishable from finding nothing, and the two want opposite
438
+ // follow-ups.
439
+ if (conflictOverride && declared.size) {
440
+ for (const [name, clash] of declared) {
441
+ if (clash.cause !== name) continue;
442
+ console.log(
443
+ c.yellow(` ${name}: installing alongside ${clash.with}, which it declares a conflict with`) +
444
+ c.dim(' — --confirm-conflict'),
445
+ );
446
+ }
447
+ console.log(c.dim(' The author said these two do not coexist. You are overruling them, not merging them.\n'));
448
+ }
449
+ const conflicts = conflictOverride ? new Map<string, ConflictBlock>() : declared;
450
+
389
451
  // Re-resolve from what survives rather than filtering `order` in place. A
390
452
  // dependency is only ever pulled in *for* something; `add backlog` on an
391
453
  // issue-tracker repo was still writing `instructions` and `gates`, which
@@ -393,7 +455,7 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
393
455
  // refused. Recomputing the closure drops them, and keeps anything a *surviving*
394
456
  // request still needs.
395
457
  let toInstall = order;
396
- if (blocked.size) {
458
+ if (blocked.size || conflicts.size) {
397
459
  for (const mod of order) {
398
460
  const cause = blocked.get(mod.name);
399
461
  if (!cause) continue;
@@ -407,18 +469,68 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
407
469
  console.log(c.yellow(` ${mod.name}: not installed — it requires ${cause}.`));
408
470
  }
409
471
  }
410
- toInstall = resolveInstallOrder(names.filter((n) => !blocked.has(n)), mods).order;
411
- const dropped = order.filter((m) => !toInstall.includes(m) && !blocked.has(m.name));
472
+ for (const mod of order) {
473
+ const clash = conflicts.get(mod.name);
474
+ // A module refused by both is reported once, under the paradigm, which
475
+ // carries the more specific evidence — a matched path.
476
+ if (!clash || blocked.has(mod.name)) continue;
477
+ if (clash.cause === mod.name) {
478
+ const declarer = mod.conflicts.includes(clash.with) ? mod.name : clash.with;
479
+ console.log(c.yellow(` ${mod.name}: conflicts with ${clash.with}`));
480
+ console.log(
481
+ c.dim(` declared by ${declarer}`) +
482
+ c.dim(present.has(clash.with) && !order.some((m) => m.name === clash.with) ? ', which this repo already has' : ', and both were requested'),
483
+ );
484
+ } else {
485
+ console.log(c.yellow(` ${mod.name}: not installed — it requires ${clash.cause}.`));
486
+ }
487
+ }
488
+ const refused = new Set([...blocked.keys(), ...conflicts.keys()]);
489
+ toInstall = resolveInstallOrder(names.filter((n) => !refused.has(n)), mods).order;
490
+ const dropped = order.filter((m) => !toInstall.includes(m) && !refused.has(m.name));
412
491
  if (dropped.length) {
413
492
  console.log(c.dim(` ${dropped.map((m) => m.name).join(', ')} not written — pulled in only for the above`));
414
493
  }
494
+ const escapes = [blocked.size ? '--confirm-paradigm' : '', conflicts.size ? '--confirm-conflict' : ''].filter(Boolean);
415
495
  console.log(
416
- c.dim(`\n Pass --confirm-paradigm to install anyway.`) +
496
+ c.dim(`\n Pass ${escapes.join(' / ')} to install anyway.`) +
417
497
  (toInstall.length ? c.dim(' Continuing with the rest.\n') : c.dim(' Nothing was written.\n')),
418
498
  );
419
499
  if (!toInstall.length) return 1;
420
500
  }
421
501
 
502
+ // A module nobody paid for is a different thing to install than one extracted
503
+ // from a repo that did, and the person typing `add` is who should be told
504
+ // (F-037). One line, at the only moment it changes a decision.
505
+ for (const mod of toInstall.filter((m) => m.provenance.kind === 'designed')) {
506
+ console.log(
507
+ c.yellow(` ${mod.name}: designed, not extracted`) +
508
+ c.dim(` — ${firstSentence(mod.provenance.rationale ?? '')}`),
509
+ );
510
+ }
511
+
512
+ // This is one operation, even though files, gates, the install record and
513
+ // harness renderings are applied in phases. Validate every selected module
514
+ // and every post-phase output before the first module can write.
515
+ const actualInstall = toInstall.filter(
516
+ (mod) => !(mod.threshold?.confirm && !dryRun && !flags.has('--confirm-threshold')),
517
+ );
518
+ try {
519
+ preflightModuleEmissions(actualInstall, root, params, skillsDir);
520
+ preflightRender(
521
+ root,
522
+ harnesses,
523
+ prospectiveRuleEmissions(actualInstall, params, skillsDir),
524
+ [
525
+ ...moduleEmissionCandidates(actualInstall, params, skillsDir),
526
+ { moduleName: 'rungs', target: '.ai/gates.toml', shared: true, writeExisting: true },
527
+ { moduleName: 'rungs', target: '.ai/rungs.toml', writeExisting: true },
528
+ ],
529
+ );
530
+ } catch (error) {
531
+ return pathRefusal(error);
532
+ }
533
+
422
534
  const installed: Manifest[] = [];
423
535
  const wrote = new Map<string, Set<string>>();
424
536
  for (const mod of toInstall) {
@@ -463,7 +575,7 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
463
575
  }
464
576
 
465
577
  if (!dryRun) {
466
- writeInstallRecord(root, order, params, harnesses, stamp, skillsDir, wrote);
578
+ writeInstallRecord(root, installed, params, harnesses, stamp, skillsDir, wrote);
467
579
  const entries = render(root, harnesses);
468
580
  writeReport(root, entries, harnesses, stamp);
469
581
  console.log(
@@ -477,6 +589,11 @@ function cmdAdd(names: string[], root: string, dryRun: boolean, harnesses: Harne
477
589
  }
478
590
 
479
591
  function cmdRender(root: string, harnesses: Harness[], stamp: string) {
592
+ try {
593
+ preflightRender(root, harnesses);
594
+ } catch (error) {
595
+ return pathRefusal(error);
596
+ }
480
597
  const entries = render(root, harnesses);
481
598
  writeReport(root, entries, harnesses, stamp);
482
599
  console.log(c.bold(`\nrungs render — ${root}\n`));
@@ -514,11 +631,18 @@ function landRunner(dir: string, only?: ReadonlySet<string>) {
514
631
  const failing = runs.filter((r) => r.status === 'fail' || r.status === 'error');
515
632
  return {
516
633
  pass: runs.filter((r) => r.status === 'pass').length,
517
- // `file: message`, so the same broken link in the same file is the same
518
- // finding across two runs, and a *new* one is visibly not.
634
+ // Keep a stable comparison identity separate from the operator-facing
635
+ // diagnostic. `file: identity` makes the same broken link in the same file
636
+ // the same finding across runs, while command diagnostics can stay complete.
519
637
  failing: failing.map((r) => ({
520
638
  id: r.id,
521
- findings: r.findings.map((f) => `${f.file ? `${f.file}: ` : ''}${f.message}`),
639
+ findings: r.findings.map((f) => {
640
+ const prefix = f.file ? `${f.file}: ` : '';
641
+ return {
642
+ identity: `${prefix}${f.identity ?? f.message}`,
643
+ diagnostic: `${prefix}${f.message}`,
644
+ };
645
+ }),
522
646
  })),
523
647
  };
524
648
  }
@@ -588,7 +712,7 @@ function cmdCheck(root: string, tier: string | undefined, stamp: string) {
588
712
  (r.examined ? c.dim(` ${r.examined} examined`) : ''),
589
713
  );
590
714
  for (const f of r.findings.slice(0, 4)) {
591
- console.log(` ${c.dim(f.file ? `${f.file}: ` : '')}${f.message}`);
715
+ console.log(` ${c.dim(f.file ? `${f.file}: ` : '')}${f.message.replace(/\n/g, '\n ')}`);
592
716
  }
593
717
  if (r.findings.length > 4) console.log(c.dim(` …and ${r.findings.length - 4} more`));
594
718
  }
@@ -640,16 +764,29 @@ function reportLedger(root: string) {
640
764
  }
641
765
 
642
766
  function cmdBacklogArchive(root: string, dryRun: boolean) {
643
- const record = readRecord(root);
767
+ let record;
768
+ try {
769
+ record = readRecord(root);
770
+ } catch (error) {
771
+ return pathRefusal(error);
772
+ }
644
773
  const configured = record?.modules['backlog']?.params?.root;
645
774
  const backlogRoot = `docs/${configured ?? 'backlog'}`;
646
775
 
647
- if (!existsSync(join(root, ...backlogRoot.split('/'), 'items'))) {
776
+ let tree;
777
+ let plan;
778
+ try {
779
+ tree = resolveArchiveTree(root, backlogRoot);
780
+ plan = planArchive(root, backlogRoot);
781
+ } catch (error) {
782
+ return pathRefusal(error);
783
+ }
784
+
785
+ if (!tree.itemsExists) {
648
786
  console.log(c.red(`\n no backlog at ${backlogRoot}/items\n`));
649
787
  return 1;
650
788
  }
651
789
 
652
- const plan = planArchive(root, backlogRoot);
653
790
  console.log(c.bold(`\nrungs backlog archive → ${root}${dryRun ? c.yellow(' (dry run)') : ''}\n`));
654
791
 
655
792
  for (const h of plan.held) console.log(c.yellow(` held ${h.file}`) + c.dim(` — ${h.reason}`));
@@ -679,14 +816,24 @@ function cmdBacklogArchive(root: string, dryRun: boolean) {
679
816
  return 0;
680
817
  }
681
818
 
682
- applyArchive(root, plan);
819
+ try {
820
+ applyArchive(root, plan);
821
+ } catch (error) {
822
+ return pathRefusal(error);
823
+ }
683
824
  console.log(c.green(`\n archived ${plan.moves.length} item(s)`) + c.dim(' — ids stay spent and every citation still resolves.'));
684
825
  console.log(c.dim(' Run `rungs check` to confirm.\n'));
685
826
  return 0;
686
827
  }
687
828
 
688
829
  function cmdInit(root: string, profile: string, dryRun: boolean, harnesses: Harness[], stamp: string) {
689
- if (readRecord(root)) {
830
+ let existing;
831
+ try {
832
+ existing = readRecord(root);
833
+ } catch (error) {
834
+ return pathRefusal(error);
835
+ }
836
+ if (existing) {
690
837
  console.log(
691
838
  c.yellow('\n this repo is already initialised.') +
692
839
  c.dim(' Use `rungs add <module>` to install more, or `rungs upgrade`.\n'),
@@ -703,13 +850,23 @@ function cmdInit(root: string, profile: string, dryRun: boolean, harnesses: Harn
703
850
  }
704
851
 
705
852
  function cmdUpgrade(root: string, apply: boolean) {
706
- const record = readRecord(root);
853
+ let record;
854
+ try {
855
+ record = readRecord(root);
856
+ } catch (error) {
857
+ return pathRefusal(error);
858
+ }
707
859
  if (!record) {
708
860
  console.log(c.yellow('\n not a rungs repo — nothing to upgrade.\n'));
709
861
  return 1;
710
862
  }
711
863
  const mods = loadAllModules(MODULES);
712
- const plan = planUpgrade(root, mods, record);
864
+ let plan;
865
+ try {
866
+ plan = planUpgrade(root, mods, record);
867
+ } catch (error) {
868
+ return pathRefusal(error);
869
+ }
713
870
  console.log(c.bold(`\nrungs upgrade — ${root}${apply ? '' : c.yellow(' (preview)')}\n`));
714
871
 
715
872
  let stale = 0;
@@ -730,7 +887,13 @@ function cmdUpgrade(root: string, apply: boolean) {
730
887
  // old block — F-016, measured on a scratch consumer where `session` 1.1.0 →
731
888
  // 1.2.0 added a gate and `rungs check` went on running the previous twenty.
732
889
  if (apply) {
733
- const { written, gates, recorded } = applyUpgrade(root, mods, record, plan);
890
+ let result;
891
+ try {
892
+ result = applyUpgrade(root, mods, record, plan);
893
+ } catch (error) {
894
+ return pathRefusal(error);
895
+ }
896
+ const { written, gates, recorded } = result;
734
897
  const parts = [
735
898
  written ? `${written} file(s)` : '',
736
899
  gates ? `${gates} gate registration(s)` : '',
@@ -747,7 +910,13 @@ function cmdUpgrade(root: string, apply: boolean) {
747
910
  }
748
911
 
749
912
  function cmdEject(root: string, dryRun: boolean) {
750
- if (!readRecord(root)) {
913
+ let record;
914
+ try {
915
+ record = readRecord(root);
916
+ } catch (error) {
917
+ return pathRefusal(error);
918
+ }
919
+ if (!record) {
751
920
  console.log(c.yellow('\n not a rungs repo — nothing to eject.\n'));
752
921
  return 1;
753
922
  }
@@ -775,48 +944,6 @@ function cmdEject(root: string, dryRun: boolean) {
775
944
  */
776
945
  const VALUE_FLAGS = new Set(['--set']);
777
946
 
778
- /**
779
- * The command surface, defined once and rendered into `--help`.
780
- *
781
- * It was a template literal listing eight of the nine commands — `setup git` was missing entirely —
782
- * beside a README table listing all nine, which is two hand-kept inventories of one fact. They had
783
- * already drifted, in both directions: help omitted a real command, and three real flags appeared
784
- * in neither. Keep this table beside the switch it describes, and add a row when you add a `case`.
785
- *
786
- * The README's table is still hand-kept and still a second inventory. That is a known cost, not an
787
- * oversight — see WI-004.
788
- */
789
- const COMMANDS: [usage: string, blurb: string][] = [
790
- ['init [path] [profile]', 'scaffold a repo — minimal · tracked · disciplined · hardened · fleet'],
791
- ['doctor [path]', 'detect what a repo already has, installed or not'],
792
- ['add <module…> [--into p]', 'install modules, resolving dependencies and adopting what exists'],
793
- ['check [path] [tier]', 'run the registered gates and record the ledger'],
794
- ['render [path]', 're-emit path-scoped rules per harness'],
795
- ['upgrade [path]', 'move to newer module versions, never touching what you edited'],
796
- ['eject [path]', 'materialise the engines; stop depending on rungs'],
797
- ['setup git [path]', 'install the merge drivers .gitattributes names'],
798
- ['modules', 'list the module set and audit the manifests'],
799
- ['backlog archive [path]', 'move finished items to archive/, repointing every link'],
800
- ['session start <branch>', 'cut a branch and worktree from the last verified merge'],
801
- ['preflight [path]', 'did the integration branch change files you changed?'],
802
- ['land <branch>', 'merge → verify the merged tree → advance, or refuse and park it'],
803
- ['worktrees [path]', 'which worktrees are merged, prunable, or merged and still dirty'],
804
- ];
805
-
806
- /** Every flag the parser honours. A flag absent here is a flag nobody can find. */
807
- const FLAGS: [flag: string, blurb: string][] = [
808
- ['--dry-run', 'report what would happen, write nothing'],
809
- ['--explain', "doctor: also run the detectors over what this repo already has"],
810
- ['--confirm-paradigm', 'add: install a module this repo already solves another way'],
811
- ['--into <path>', 'add: install into this repo instead of the working directory'],
812
- ['--set m.param=value', 'add/init: override a module parameter. Repeatable'],
813
- ['--confirm-threshold', 'add: install a module whose rung is above this repo'],
814
- ['--apply', 'upgrade: write the changes, rather than preview them'],
815
- ['--fast, --full', 'check: pick the gate tier, as the positional also does'],
816
- ['--params', 'modules: show every module parameter, its default and its allowed values'],
817
- ['--copilot', 'also emit Copilot instruction files'],
818
- ];
819
-
820
947
  function renderHelp(): string {
821
948
  const pad = Math.max(...COMMANDS.map(([u]) => u.length)) + 2;
822
949
  const fpad = Math.max(...FLAGS.map(([f]) => f.length)) + 2;