simt-dev 0.6.2 → 0.7.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 (170) hide show
  1. package/CHANGELOG.md +83 -1
  2. package/README.md +46 -17
  3. package/agents/simt-agent.md +84 -0
  4. package/bin/simt.mjs +19 -6
  5. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  6. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
  7. package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
  8. package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
  9. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
  10. package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
  11. package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
  12. package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
  13. package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
  14. package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
  15. package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
  16. package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
  17. package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
  18. package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
  19. package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
  20. package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
  21. package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
  22. package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
  23. package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
  24. package/docs/decisions/README.md +53 -0
  25. package/package.json +4 -2
  26. package/rules/core.md +2 -2
  27. package/rules/mdc/preview-postflight.mdc +5 -6
  28. package/rules/mdc/test-gate.mdc +2 -0
  29. package/rules/stacks/android.md +10 -11
  30. package/rules/stacks/cloudflare-workers.md +1 -1
  31. package/rules/stacks/connectiq.md +2 -2
  32. package/rules/stacks/container.md +2 -0
  33. package/rules/stacks/dart.md +1 -1
  34. package/rules/stacks/deno.md +1 -1
  35. package/rules/stacks/dotnet.md +1 -1
  36. package/rules/stacks/elixir.md +1 -1
  37. package/rules/stacks/go.md +1 -1
  38. package/rules/stacks/jvm.md +4 -3
  39. package/rules/stacks/node.md +2 -2
  40. package/rules/stacks/python.md +7 -6
  41. package/rules/stacks/react-native.md +3 -5
  42. package/rules/stacks/react-web.md +9 -9
  43. package/rules/stacks/ruby.md +1 -1
  44. package/rules/stacks/rust.md +1 -1
  45. package/rules/stacks/swift.md +1 -1
  46. package/rules/stacks/typescript.md +2 -2
  47. package/rules/stacks/uv.md +3 -2
  48. package/skills/simt-commands/SKILL.md +70 -0
  49. package/src/advice/advice.mjs +7 -3
  50. package/src/agenthooks/decide.mjs +238 -15
  51. package/src/agenthooks/opencode.mjs +72 -0
  52. package/src/agenthooks/policy.mjs +81 -5
  53. package/src/agenthooks/prompt.mjs +6 -2
  54. package/src/agenthooks/render.mjs +186 -19
  55. package/src/agenthooks/shell.mjs +523 -0
  56. package/src/agenthooks/shim.mjs +55 -0
  57. package/src/agents/render.mjs +277 -0
  58. package/src/argv.mjs +62 -9
  59. package/src/bench/ablate.mjs +31 -14
  60. package/src/bench/mine.mjs +48 -1
  61. package/src/bench/probes.mjs +5 -6
  62. package/src/bench/report.mjs +243 -80
  63. package/src/bench/run.mjs +12 -11
  64. package/src/bench/variants.mjs +112 -40
  65. package/src/brief/assemble.mjs +6 -0
  66. package/src/ci/workflow.mjs +16 -7
  67. package/src/commands/agent.mjs +206 -0
  68. package/src/commands/agenthook.mjs +133 -17
  69. package/src/commands/bench.mjs +2 -1
  70. package/src/commands/ci.mjs +28 -3
  71. package/src/commands/compare.mjs +8 -7
  72. package/src/commands/completions.mjs +27 -0
  73. package/src/commands/decide.mjs +20 -6
  74. package/src/commands/doctor.mjs +367 -0
  75. package/src/commands/evidence.mjs +13 -3
  76. package/src/commands/explain.mjs +52 -0
  77. package/src/commands/gate.mjs +85 -19
  78. package/src/commands/graph.mjs +18 -5
  79. package/src/commands/help.mjs +96 -26
  80. package/src/commands/hooks.mjs +258 -67
  81. package/src/commands/ignore.mjs +152 -0
  82. package/src/commands/impact.mjs +4 -2
  83. package/src/commands/index.mjs +69 -102
  84. package/src/commands/init.mjs +239 -37
  85. package/src/commands/lease.mjs +304 -0
  86. package/src/commands/lessons.mjs +77 -9
  87. package/src/commands/map.mjs +33 -18
  88. package/src/commands/mcp.mjs +2 -2
  89. package/src/commands/pack.mjs +9 -0
  90. package/src/commands/plan.mjs +13 -1
  91. package/src/commands/plugin.mjs +17 -2
  92. package/src/commands/prbody.mjs +45 -0
  93. package/src/commands/repoindex.mjs +4 -0
  94. package/src/commands/rules.mjs +219 -105
  95. package/src/commands/shared.mjs +36 -5
  96. package/src/commands/skills.mjs +329 -206
  97. package/src/commands/skillscompile.mjs +8 -7
  98. package/src/commands/skillsinstall.mjs +112 -30
  99. package/src/commands/slice.mjs +712 -0
  100. package/src/commands/status.mjs +62 -0
  101. package/src/commands/sync.mjs +146 -36
  102. package/src/commands/tests.mjs +5 -2
  103. package/src/commands/undo.mjs +114 -0
  104. package/src/commands/upgrade.mjs +103 -9
  105. package/src/commands/why.mjs +10 -4
  106. package/src/compare/hooksstate.mjs +23 -1
  107. package/src/compare/learn.mjs +82 -57
  108. package/src/compare/parallel.mjs +50 -0
  109. package/src/compare/row.mjs +26 -1
  110. package/src/completions.mjs +196 -0
  111. package/src/decisions/dir.mjs +84 -0
  112. package/src/decisions/index.mjs +109 -11
  113. package/src/diff.mjs +254 -0
  114. package/src/distill/distill.mjs +3 -1
  115. package/src/ecosystem/node-runner.mjs +144 -0
  116. package/src/explain.mjs +270 -0
  117. package/src/federation/config.mjs +44 -0
  118. package/src/graph/load.mjs +52 -24
  119. package/src/hooks/capability.mjs +14 -1
  120. package/src/hooks/dispatcher.mjs +441 -9
  121. package/src/hooks/gates.mjs +14 -3
  122. package/src/hooks/resolve.mjs +23 -3
  123. package/src/hooks/shim.mjs +12 -7
  124. package/src/identity.mjs +67 -14
  125. package/src/ignore.mjs +133 -0
  126. package/src/index/extract.mjs +192 -11
  127. package/src/index/persist.mjs +372 -55
  128. package/src/index/store.mjs +263 -60
  129. package/src/index/tests.mjs +6 -35
  130. package/src/journal.mjs +545 -0
  131. package/src/kernel/card.mjs +37 -12
  132. package/src/kernel/checks.mjs +44 -9
  133. package/src/kernel/variants.mjs +27 -16
  134. package/src/lease.mjs +195 -0
  135. package/src/ledger/index.mjs +91 -25
  136. package/src/mcp/resources.mjs +5 -3
  137. package/src/mcp/server.mjs +86 -13
  138. package/src/mcp/tools.mjs +340 -264
  139. package/src/memory/decide.mjs +98 -38
  140. package/src/memory/lessons.mjs +180 -33
  141. package/src/name.mjs +9 -0
  142. package/src/output/index.mjs +82 -12
  143. package/src/package-version.mjs +49 -1
  144. package/src/packs/manage.mjs +13 -4
  145. package/src/plan/index.mjs +37 -8
  146. package/src/plan/template.mjs +27 -5
  147. package/src/plugins.mjs +33 -8
  148. package/src/prbody.mjs +253 -0
  149. package/src/projection/baselines.mjs +1 -1
  150. package/src/projection/disclosure.mjs +2 -2
  151. package/src/projection/loaders.mjs +41 -1
  152. package/src/projection/lock.mjs +21 -14
  153. package/src/projection/manifest.mjs +82 -21
  154. package/src/projection/markers.mjs +43 -3
  155. package/src/projection/mdc.mjs +34 -7
  156. package/src/projection/project.mjs +162 -18
  157. package/src/projection/rules.mjs +54 -3
  158. package/src/projection/shared.mjs +4 -1
  159. package/src/projection/surfaces.mjs +9 -1
  160. package/src/repo-root.mjs +69 -0
  161. package/src/resolve/body.mjs +4 -0
  162. package/src/resolve/fingerprint.mjs +5 -0
  163. package/src/resolve/local.mjs +58 -0
  164. package/src/review/critic.mjs +4 -2
  165. package/src/secrets.mjs +374 -0
  166. package/src/state.mjs +76 -0
  167. package/src/status.mjs +157 -0
  168. package/src/toolwords.mjs +5 -0
  169. package/src/tsconfig.mjs +39 -99
  170. package/templates/plan/tracker.json +1 -1
@@ -31,6 +31,7 @@ import { initCommand } from './init.mjs';
31
31
  import { ciCommand } from './ci.mjs';
32
32
  import { hooksCommand } from './hooks.mjs';
33
33
  import { agentHookCommand, agentHooksCommand } from './agenthook.mjs';
34
+ import { agentsCommand } from './agent.mjs';
34
35
  import { compareCommand } from './compare.mjs';
35
36
  import { upgradeCommand } from './upgrade.mjs';
36
37
  import { mcpCommand } from './mcp.mjs';
@@ -38,7 +39,7 @@ import { pluginCommand } from './plugin.mjs';
38
39
  import { packCommand } from './pack.mjs';
39
40
  import { installCommand } from './skillsinstall.mjs';
40
41
  import { compileCommand } from './skillscompile.mjs';
41
- import { listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
42
+ import { decisionsCommand as rulesDecisions, listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
42
43
  import { tokensCommand } from './tokens.mjs';
43
44
  import { harnessCommand, harnessLoadersCommand } from './harness.mjs';
44
45
  import {
@@ -51,7 +52,11 @@ import {
51
52
  benchCriticCommand,
52
53
  } from './bench.mjs';
53
54
  import { planCommand } from './plan.mjs';
54
- import { sharedPlanCommand, sharedInstallCommand, sharedWarning } from './shared.mjs';
55
+ import { sliceCommand } from './slice.mjs';
56
+ import { leaseCommand } from './lease.mjs';
57
+ import { sharedPlanCommand, sharedInstallCommand } from './shared.mjs';
58
+ import { runDoctor } from './doctor.mjs';
59
+ import { statusCommand } from './status.mjs';
55
60
  import { evidenceCommand } from './evidence.mjs';
56
61
  import { indexCommand } from './repoindex.mjs';
57
62
  import { mapCommand } from './map.mjs';
@@ -59,15 +64,18 @@ import { testsCommand } from './tests.mjs';
59
64
  import { whyCommand } from './why.mjs';
60
65
  import { gateCommand } from './gate.mjs';
61
66
  import { impactCommand } from './impact.mjs';
62
- import { lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
67
+ import { lessonsCaptureCommand, lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
63
68
  import { decideCommand } from './decide.mjs';
69
+ import { explainCommand } from './explain.mjs';
70
+ import { undoCommand } from './undo.mjs';
71
+ import { prBodyCommand } from './prbody.mjs';
72
+ import { ignoreCommand } from './ignore.mjs';
73
+ import { completionsCommand } from './completions.mjs';
64
74
  import { reviewCommand } from './review.mjs';
65
75
  import { localDoctorCommand } from './local.mjs';
66
76
  import { distillCommand } from './distill.mjs';
67
- import { parse } from '../argv.mjs';
77
+ import { parse, rawJsonFlag } from '../argv.mjs';
68
78
  import { CLI } from '../name.mjs';
69
- import { fingerprint } from '../resolve/fingerprint.mjs';
70
- import { resolveWorkspace } from '../workspace.mjs';
71
79
 
72
80
  /** command path -> implementation sequence step (plan 0002 section 11). */
73
81
  // Every command named in plan 0002 section 5 is now implemented, so this map is empty.
@@ -83,7 +91,15 @@ const GROUPS = new Map([
83
91
  ['rules', 'list'],
84
92
  ['hooks', 'status'],
85
93
  ['plan', 'status'],
94
+ // Plan 0003 R53. Both halves write, so a bare `slice` has no read to default to.
95
+ // `start` is the default because with the id it is missing it fails with a usage
96
+ // error before anything is created — the one default here that cannot mutate.
97
+ ['slice', 'start'],
86
98
  ['agent-hooks', 'status'],
99
+ // A group like `agent-hooks`: `install` writes the agent file into each harness's
100
+ // own directory, `status` reports what each harness carries. The default is the
101
+ // read, the way `agent-hooks`'s is.
102
+ ['agents', 'status'],
87
103
  ['harness', 'detect'],
88
104
  ['bench', 'mine'],
89
105
  // ADR-084. `shared` is the one group whose default writes nothing, and it is `plan` for
@@ -114,6 +130,9 @@ const GROUPS = new Map([
114
130
  export const IMPLEMENTED = {
115
131
  brief: briefCommand,
116
132
  doctor: runDoctor,
133
+ // Plan 0003 R48. A bare command: the only mode is `--line`, so there is no
134
+ // group and no default subcommand to resolve.
135
+ status: statusCommand,
117
136
  'graph lint': lintCommand,
118
137
  'skills index': skillsIndex,
119
138
  'skills find': skillsFind,
@@ -123,6 +142,7 @@ export const IMPLEMENTED = {
123
142
  'skills eval': skillsEval,
124
143
  'skills install': installCommand,
125
144
  'skills compile': compileCommand,
145
+ 'rules decisions': rulesDecisions,
126
146
  'rules list': rulesList,
127
147
  'rules show': rulesShow,
128
148
  'rules why': rulesWhy,
@@ -131,6 +151,13 @@ export const IMPLEMENTED = {
131
151
  'plan status': (args, flags) => planCommand('status', args, flags),
132
152
  'plan next': (args, flags) => planCommand('next', args, flags),
133
153
  'plan done': (args, flags) => planCommand('done', args, flags),
154
+ // Plan 0003 R53. A group: `start` isolates the work on a branch with its brief,
155
+ // `finish` runs the gates, commits and records the evidence, and `abandon`
156
+ // (review 0003 Q23) takes the slice back down. All three take the slice id
157
+ // as a positional.
158
+ 'slice start': (args, flags) => sliceCommand('start', args, flags),
159
+ 'slice finish': (args, flags) => sliceCommand('finish', args, flags),
160
+ 'slice abandon': (args, flags) => sliceCommand('abandon', args, flags),
134
161
  'shared plan': sharedPlanCommand,
135
162
  'shared install': sharedInstallCommand,
136
163
  evidence: evidenceCommand,
@@ -140,9 +167,26 @@ export const IMPLEMENTED = {
140
167
  why: (args, flags) => whyCommand(args, flags, COMMANDS),
141
168
  gate: gateCommand,
142
169
  impact: (args, flags) => impactCommand(args, flags, COMMANDS),
170
+ 'lessons capture': lessonsCaptureCommand,
143
171
  'lessons list': lessonsListCommand,
144
172
  'lessons review': lessonsReviewCommand,
145
173
  decide: decideCommand,
174
+ // Plan 0003 R46. A bare command: the code is a positional, so there is no
175
+ // group and no default subcommand to resolve.
176
+ explain: explainCommand,
177
+ // Plan 0003 R49. A bare command: restoring the last journaled set needs no
178
+ // subcommand, so there is no group and no default to resolve.
179
+ undo: undoCommand,
180
+ // Plan 0003 R55. A bare command: the body is derived from the tree, the
181
+ // ledger and the last check, so there is no subcommand to resolve and no
182
+ // write to confirm.
183
+ 'pr-body': prBodyCommand,
184
+ // Plan 0003 R52. A bare command: the one list is `.simtignore`, so there is no
185
+ // group and no default subcommand to resolve.
186
+ ignore: ignoreCommand,
187
+ // Plan 0003 R54. A bare command: paths claim, no paths list, so the argument itself
188
+ // is the mode and there is no group to resolve.
189
+ lease: leaseCommand,
146
190
  distill: distillCommand,
147
191
  // R25 machinery needs the runnable command names for Affects command tokens,
148
192
  // same wiring `why` and `impact` get through their lambdas.
@@ -157,6 +201,10 @@ export const IMPLEMENTED = {
157
201
  'agent-hook': agentHookCommand,
158
202
  'agent-hooks install': (args, flags) => agentHooksCommand('install', args, flags),
159
203
  'agent-hooks status': (args, flags) => agentHooksCommand('status', args, flags),
204
+ // ADR-111. The one agent this tool ships, written into each harness's own agents
205
+ // directory. A group like `agent-hooks`: `install` writes, `status` reads.
206
+ 'agents install': (args, flags) => agentsCommand('install', args, flags),
207
+ 'agents status': (args, flags) => agentsCommand('status', args, flags),
160
208
  compare: compareCommand,
161
209
  tokens: tokensCommand,
162
210
  'harness detect': harnessCommand,
@@ -177,6 +225,9 @@ export const IMPLEMENTED = {
177
225
  // plan 0003 R43. A bare command: the kind is a positional, so there is no group and
178
226
  // no default subcommand to resolve.
179
227
  plugin: pluginCommand,
228
+ // Plan 0003 R51. A bare command: the shell is a positional. `COMMANDS` rides in
229
+ // from here so the engine keeps no second copy of the registry it completes.
230
+ completions: (args, flags) => completionsCommand(args, flags, COMMANDS),
180
231
  // Plan 0003 R45. A group, not a bare command: `new` and `publish` write (or
181
232
  // publish) and `lint` reads, and the dispatcher routes all three through one
182
233
  // subcommand switch the way `hooks` does.
@@ -192,7 +243,7 @@ export const IMPLEMENTED = {
192
243
  export const COMMANDS = Object.keys(IMPLEMENTED).sort();
193
244
 
194
245
  export async function dispatch(argv) {
195
- const { flags, positional } = parseOrExit(argv);
246
+ const { flags, positional, parseError } = parseOrExit(argv);
196
247
 
197
248
  // The JSON shape is decided once, here, and every command inherits it. Threading a
198
249
  // `--pretty` through twenty-five call sites would mean twenty-five places to forget,
@@ -205,6 +256,10 @@ export async function dispatch(argv) {
205
256
  jsonIndentDefault(flags.pretty === true);
206
257
  jsonFailMode(flags.json === true);
207
258
 
259
+ // A parse failure is already the whole answer: report it once, in the asked-for
260
+ // shape, rather than falling through to `unknown command` on the empty path.
261
+ if (parseError !== undefined) return fail(parseError.message, parseError.code);
262
+
208
263
  // Help is shown only when explicitly asked for. An unknown command must fail with
209
264
  // a usage code, not print 40 lines of prose and exit 0 — an agent that mistyped
210
265
  // needs to be told so, not handed a wall of text it may misread as output.
@@ -267,102 +322,14 @@ function commandPath(first, second) {
267
322
  }
268
323
 
269
324
  function parseOrExit(argv) {
325
+ // Unreachable from `bin` (it renders the failure itself and exits), but `dispatch`
326
+ // is also called directly: carry the failure as data so the dispatcher reports it
327
+ // once, in the asked-for shape, instead of printing here and again below.
270
328
  const parsed = parse(argv);
271
- if (!parsed.ok) return { flags: {}, positional: [] };
329
+ if (!parsed.ok) {
330
+ const json = rawJsonFlag(argv);
331
+ return { flags: { json }, positional: [], parseError: parsed.error };
332
+ }
272
333
  return parsed.value;
273
334
  }
274
335
 
275
- /**
276
- * Step 3 of the implementation sequence: classify a repo from marker files alone.
277
- * Deliberately shipped early — it needs no registry, no rules and no projection,
278
- * and it makes the tool useful before any content exists.
279
- */
280
- async function runDoctor(positional, flags) {
281
- const root = flags.root ?? positional[0] ?? process.cwd();
282
- const result = await fingerprint(root);
283
- // Which source answered the workspace question, and where it looked. A tool
284
- // that silently fell back to the wrong directory would make every downstream
285
- // answer confidently wrong, so the answer is reported rather than inferred
286
- // (ADR-014).
287
- const ws = await resolveWorkspace(flags);
288
- // The shared file this repository hands rules to (ADR-084). Asked here and nowhere else
289
- // because it is the one thing about a repository that is true of the reader's machine
290
- // rather than of the tree: `check` must not read `$HOME`, so `doctor` is the only command
291
- // that can notice a region that dropped rules into a file nobody has. Reported, never
292
- // blocking — a stale copy costs a reader some of their rules, which is worth knowing and
293
- // is not this repository's gate failing.
294
- const shared = await sharedWarning(root, result.manifest ?? null, flags);
295
-
296
- // Under `--json` the document is the whole report, so it has to carry both
297
- // failures the text path reports AND the workspace answer the text path prints.
298
- // This branch used to return before either check, with `ok` hardcoded true: a
299
- // caller asking whether their config is readable was told yes, and given exit
300
- // 0, while the same command in a terminal exited 3 and named the file. A
301
- // scripted consumer has only this document to go on, so `ok` is derived from
302
- // the exit code rather than written as a constant (S10).
303
- const failure = result.error
304
- ? { error: `${root}: ${result.error}`, remedy: null }
305
- : ws.error
306
- ? {
307
- error: `workspace config ${ws.detail}: ${ws.error}`,
308
- remedy: 'fix the JSON, or remove the file',
309
- }
310
- : null;
311
- const code = failure ? EXIT.CONFIG : EXIT.OK;
312
-
313
- if (isJson(flags)) {
314
- emitJson({
315
- ok: code === EXIT.OK,
316
- command: 'doctor',
317
- root,
318
- ...result,
319
- // The workspace answer is a fact about which directory the tool would
320
- // act on next (ADR-014). It is printed in the text report; leaving it
321
- // out of the JSON meant a machine-readable caller could not see it at
322
- // all, which is the one-question-two-answers defect again.
323
- workspace: {
324
- root: ws.root ?? null,
325
- source: ws.source,
326
- detail: ws.detail,
327
- error: ws.error ?? null,
328
- },
329
- shared,
330
- ...(failure ?? {}),
331
- });
332
- return code;
333
- }
334
- if (result.error) {
335
- return fail(`${root}: ${result.error}`, EXIT.CONFIG);
336
- }
337
- emit(
338
- [
339
- `root ${root}`,
340
- `stack ${result.stack.length > 0 ? result.stack.join(', ') : '(undetected)'}`,
341
- `evidence ${result.evidence.length > 0 ? result.evidence.join(' ') : '(none)'}`,
342
- // Facts are what a profile selects on when it wants to survive the
343
- // framework being replaced, so they are reported with the path that proved
344
- // each one rather than as a bare list (ADR-013).
345
- `facts ${
346
- result.facts.length > 0
347
- ? result.facts.map((f) => `${f}(${result.factEvidence[f] ?? '?'})`).join(' ')
348
- : '(none)'
349
- }`,
350
- `manifest ${result.manifest ? (result.manifest.error ?? result.manifest.id ?? 'present') : '(absent)'}`,
351
- `workspace ${ws.root ?? '(none)'} (${ws.source}: ${ws.detail})`,
352
- // Printed only when this repository handed rules to a workspace copy: a line in
353
- // every `doctor` is a line in every reader's terminal, and the reader who has
354
- // nothing to install is not served by one (ADR-084).
355
- ...(shared.declared ? [`shared ${shared.state} ${shared.detail}`] : []),
356
- ].join('\n'),
357
- { maxBytes: flags.maxBytes ?? 0 },
358
- );
359
- // An unconfigured workspace is a fact, not a failure: a single repo with no
360
- // workspace around it is a normal, supported case. A config file that EXISTS and
361
- // does not parse is a real mistake, and that does fail - with the parse error,
362
- // because naming the file without saying what is wrong with it sends the reader
363
- // to the file and gives them nothing to look for.
364
- if (ws.error) {
365
- return fail(`workspace config ${ws.detail}: ${ws.error}`, EXIT.CONFIG, 'fix the JSON, or remove the file');
366
- }
367
- return EXIT.OK;
368
- }