simt-dev 0.7.0 → 0.7.1

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 (112) hide show
  1. package/CHANGELOG.md +158 -872
  2. package/README.md +19 -133
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  5. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  6. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  7. package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
  8. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  9. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  10. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  11. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  12. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  13. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  14. package/package.json +10 -2
  15. package/rules/core.md +7 -0
  16. package/rules/mdc/slice-commit.mdc +57 -4
  17. package/src/agenthooks/prompt.mjs +5 -2
  18. package/src/agenthooks/shim.mjs +1 -1
  19. package/src/api.mjs +29 -0
  20. package/src/argv.mjs +29 -3
  21. package/src/bench/adapters.json +16 -0
  22. package/src/bench/corpus.mjs +150 -0
  23. package/src/bench/grade.mjs +23 -49
  24. package/src/bench/mine.mjs +199 -35
  25. package/src/bench/probes.mjs +99 -62
  26. package/src/bench/process.mjs +128 -0
  27. package/src/bench/report.mjs +17 -4
  28. package/src/bench/run.mjs +242 -142
  29. package/src/brief/assemble.mjs +38 -3
  30. package/src/ci/workflow.mjs +30 -7
  31. package/src/cli.mjs +45 -0
  32. package/src/commands/agent.mjs +3 -4
  33. package/src/commands/agenthook.mjs +4 -5
  34. package/src/commands/bench.mjs +190 -17
  35. package/src/commands/brief.mjs +5 -4
  36. package/src/commands/ci.mjs +2 -1
  37. package/src/commands/compare.mjs +42 -42
  38. package/src/commands/completions.mjs +2 -6
  39. package/src/commands/decide.mjs +4 -4
  40. package/src/commands/distill.mjs +4 -3
  41. package/src/commands/doctor.mjs +11 -7
  42. package/src/commands/evidence.mjs +6 -9
  43. package/src/commands/explain.mjs +0 -1
  44. package/src/commands/gate.mjs +16 -12
  45. package/src/commands/graph.mjs +2 -3
  46. package/src/commands/harness.mjs +3 -2
  47. package/src/commands/help.mjs +125 -17
  48. package/src/commands/hooks.mjs +100 -30
  49. package/src/commands/ignore.mjs +2 -1
  50. package/src/commands/impact.mjs +4 -7
  51. package/src/commands/index.mjs +273 -188
  52. package/src/commands/init.mjs +2 -1
  53. package/src/commands/lease.mjs +6 -7
  54. package/src/commands/lessons.mjs +10 -9
  55. package/src/commands/local.mjs +3 -3
  56. package/src/commands/map.mjs +14 -9
  57. package/src/commands/mcp.mjs +0 -1
  58. package/src/commands/pack.mjs +9 -6
  59. package/src/commands/plan.mjs +6 -5
  60. package/src/commands/plugin.mjs +2 -2
  61. package/src/commands/prbody.mjs +3 -3
  62. package/src/commands/repoindex.mjs +4 -3
  63. package/src/commands/review.mjs +2 -2
  64. package/src/commands/rules.mjs +33 -16
  65. package/src/commands/shared.mjs +2 -3
  66. package/src/commands/skills.mjs +28 -26
  67. package/src/commands/skillscompile.mjs +4 -5
  68. package/src/commands/skillsinstall.mjs +2 -2
  69. package/src/commands/slice.mjs +21 -10
  70. package/src/commands/spec.mjs +151 -0
  71. package/src/commands/status.mjs +4 -7
  72. package/src/commands/sync.mjs +13 -11
  73. package/src/commands/tests.mjs +6 -9
  74. package/src/commands/tokens.mjs +2 -2
  75. package/src/commands/undo.mjs +5 -15
  76. package/src/commands/upgrade.mjs +7 -3
  77. package/src/commands/why.mjs +9 -8
  78. package/src/compare/row.mjs +0 -2
  79. package/src/context.mjs +64 -0
  80. package/src/decisions/index-doc.mjs +46 -0
  81. package/src/diff.mjs +1 -1
  82. package/src/distill/distill.mjs +14 -1
  83. package/src/distill/engine.mjs +5 -1
  84. package/src/graph/load.mjs +4 -1
  85. package/src/hooks/capability.mjs +3 -3
  86. package/src/hooks/dispatcher.mjs +198 -43
  87. package/src/hooks/gates.mjs +0 -43
  88. package/src/hooks/resolve.mjs +2 -7
  89. package/src/hooks/shim.mjs +5 -2
  90. package/src/identity.mjs +29 -1
  91. package/src/journal.mjs +78 -9
  92. package/src/ledger/index.mjs +30 -1
  93. package/src/local/client.mjs +4 -6
  94. package/src/locate.mjs +33 -9
  95. package/src/manifest/schema.mjs +213 -0
  96. package/src/mcp/resources.mjs +7 -10
  97. package/src/mcp/server.mjs +70 -16
  98. package/src/mcp/tools.mjs +45 -7
  99. package/src/measure/session.mjs +3 -2
  100. package/src/node-version.mjs +37 -0
  101. package/src/output/index.mjs +111 -3
  102. package/src/packs/manage.mjs +17 -3
  103. package/src/packs.mjs +20 -2
  104. package/src/projection/lock.mjs +11 -1
  105. package/src/projection/manifest.mjs +20 -34
  106. package/src/projection/rules.mjs +4 -3
  107. package/src/repo-root.mjs +19 -0
  108. package/src/resolve/fingerprint.mjs +19 -15
  109. package/src/review/critic.mjs +2 -1
  110. package/src/toolwords.mjs +3 -0
  111. package/src/workspace.mjs +55 -10
  112. package/docs/decisions/README.md +0 -600
@@ -12,7 +12,8 @@
12
12
  // DEFAULT location, which core.hooksPath being unset does not reveal, so a
13
13
  // foreign hook is found by asking git rather than by reading config.
14
14
 
15
- import { mkdir, readFile, realpath } from 'node:fs/promises';
15
+ import { mkdir, readFile, readdir, realpath, rm, stat } from 'node:fs/promises';
16
+ import { repoContext } from '../context.mjs';
16
17
  import { commitJournal, journaledGitConfig, journaledRemoveFile, journaledWriteFile } from '../journal.mjs';
17
18
  import { diffField, emitDiffs, readBefore } from '../diff.mjs';
18
19
  import { invalidateCheckCache } from '../status.mjs';
@@ -20,19 +21,23 @@ import { isAbsolute, join, relative } from 'node:path';
20
21
 
21
22
  import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
22
23
  import { MANIFEST, readManifest } from '../projection/manifest.mjs';
23
- import { EVENTS, HOOKS_DIR, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderEvidenceShim, renderShim } from '../hooks/shim.mjs';
24
+ import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_DIR_LEGACY, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderEvidenceShim, renderShim } from '../hooks/shim.mjs';
24
25
  import { GATE_KEY, gateCount, readHooksPath, readHooksState, wantsShim } from '../compare/hooksstate.mjs';
25
26
  import { GATES } from '../hooks/gates.mjs';
26
- import { knownGates } from '../hooks/resolve.mjs';
27
+ import { gateNames } from '../hooks/gates.mjs';
27
28
  import { renderGateReport, runGates } from '../hooks/dispatcher.mjs';
28
29
  import { CLI } from '../name.mjs';
29
30
  import { MANIFEST_FILE } from '../name.mjs';
30
31
  import { packageVersion, hasNpm } from '../package-version.mjs';
32
+ import { commandRoot } from '../repo-root.mjs';
31
33
 
32
34
  export async function hooksCommand(sub, args, flags) {
33
- const root = flags.root ?? args[0] ?? process.cwd();
34
- if (sub === 'install') return installHooks(root, flags);
35
- if (sub === 'status') return hooksStatus(root, flags);
35
+ const root = commandRoot(flags, args);
36
+ // One context for the subcommand: install and status each ask about the same
37
+ // repository, and `doctor --fix` asks alongside them (review 0003 Q67).
38
+ const ctx = repoContext(root);
39
+ if (sub === 'install') return installHooks(root, flags, ctx);
40
+ if (sub === 'status') return hooksStatus(root, flags, ctx);
36
41
  if (sub === 'run') return runHook(args, flags);
37
42
  return fail(`unknown hooks subcommand ${sub}`, EXIT.USAGE, `run: ${CLI} hooks status`);
38
43
  }
@@ -40,11 +45,12 @@ export async function hooksCommand(sub, args, flags) {
40
45
  /** The shim execs this; the event name is baked into the shim, so it is arg 0. */
41
46
  async function runHook(args, flags) {
42
47
  const event = args[0];
43
- const root = flags.root ?? process.cwd();
48
+ const root = commandRoot(flags);
49
+ const ctx = repoContext(root);
44
50
  if (!EVENTS.includes(event)) {
45
51
  return fail(`unknown hook event ${event}`, EXIT.USAGE, `known events: ${EVENTS.join(', ')}`);
46
52
  }
47
- const { manifest, exists, error } = await readManifest(root);
53
+ const { manifest, exists, error } = await ctx.manifest();
48
54
  if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG);
49
55
  if (!exists) {
50
56
  return fail(`${root} has no ${MANIFEST_FILE}; the hook cannot know its gate list`, EXIT.CONFIG, `run: ${CLI} init --root ${root}`);
@@ -64,7 +70,7 @@ async function runHook(args, flags) {
64
70
  }
65
71
 
66
72
  for (const n of result.notes ?? []) note(`note ${n}`);
67
- emit(renderGateReport(event, manifest.gates[GATE_KEY[event]] ?? [], result).join('\n'), { maxBytes: flags.maxBytes ?? 0 });
73
+ emit(renderGateReport(event, manifest.gates[GATE_KEY[event]] ?? [], result).join('\n'));
68
74
  return result.failed ? EXIT.FAIL : EXIT.OK;
69
75
  }
70
76
 
@@ -135,7 +141,7 @@ async function relativeTarget(root, target) {
135
141
  * caller enforces in its own voice (`takeover`, and the `wanted`/`obsolete`
136
142
  * emptiness the command refuses on)
137
143
  */
138
- export async function planHooksInstall(root, { manifest, exists }) {
144
+ export async function planHooksInstall(root, { manifest, exists }, ctx = null) {
139
145
  const declaresGate = (event) => wantsShim(manifest, exists, event);
140
146
  const state = await readHooksState(root, { declaresGate });
141
147
  const current = state.current;
@@ -159,8 +165,10 @@ export async function planHooksInstall(root, { manifest, exists }) {
159
165
  } catch {
160
166
  // Unreadable here leaves the npm default; the manifest path reports it.
161
167
  }
162
- const { fingerprint } = await import('../resolve/fingerprint.mjs');
163
- const detected = await fingerprint(root);
168
+ // The dynamic import stays: a hook run must not pull the fingerprint module in
169
+ // just by loading this one. A caller that already holds a context passes it and
170
+ // the walk is shared (review 0003 Q67).
171
+ const detected = ctx ? await ctx.fingerprint() : await (await import('../resolve/fingerprint.mjs')).fingerprint(root);
164
172
  manager = resolveNodeRunner({ facts: detected.facts, packageManager }).id;
165
173
  }
166
174
 
@@ -190,8 +198,8 @@ export async function planHooksInstall(root, { manifest, exists }) {
190
198
  //
191
199
  // `foreignHooks` lists every executable file in the directory git reads hooks from -
192
200
  // and when that directory is this tool's own, a file a person dropped in there is
193
- // listed too. Chaining it means writing a shim at `.simt-hooks/<name>` whose TARGET is
194
- // `.simt-hooks/<name>`: the same path. The shim overwrites the hook it was written to
201
+ // listed too. Chaining it means writing a shim at `.simt/hooks/<name>` whose TARGET is
202
+ // `.simt/hooks/<name>`: the same path. The shim overwrites the hook it was written to
195
203
  // preserve and then runs itself, so the hook stops working and git recurses in a loop
196
204
  // nothing reports. Confirmed at the previous release, which printed
197
205
  // `UPDATED .simt-hooks/mine (chained: runs .simt-hooks/mine)`.
@@ -298,9 +306,50 @@ export async function planHooksInstall(root, { manifest, exists }) {
298
306
  evidenceExisting,
299
307
  routed,
300
308
  takeover,
309
+ // The legacy directory's removal rides on the same planned write, so the
310
+ // scan is here and the deletion is in `applyHooksInstall` (ADR-122).
311
+ legacy: await legacyHooksScan(root),
301
312
  };
302
313
  }
303
314
 
315
+ /**
316
+ * Scan the legacy top-level shim directory (ADR-122). A repository installed
317
+ * before the move into the state directory still carries it, and it is
318
+ * removable only when every entry in it is a regular file that carries a
319
+ * simt-dev marker: the install re-routes git to the new directory, and a
320
+ * file this tool cannot recognise as its own is never deleted on its way.
321
+ *
322
+ * @returns {Promise<{path: string, removable: boolean}|null>} null when the
323
+ * directory is absent or empty; otherwise the directory and whether the
324
+ * install may take it down. `applyHooksInstall` acts on this, so the
325
+ * judgment is made once, in the plan. Module-local: one caller needs no
326
+ * export, and an export is a promise to a caller (deadcode).
327
+ */
328
+ async function legacyHooksScan(root) {
329
+ const dir = join(root, HOOKS_DIR_LEGACY);
330
+ let entries;
331
+ try {
332
+ entries = await readdir(dir);
333
+ } catch {
334
+ return null;
335
+ }
336
+ if (entries.length === 0) return null;
337
+ let removable = true;
338
+ for (const name of entries) {
339
+ const st = await stat(join(dir, name));
340
+ if (!st.isFile()) {
341
+ removable = false;
342
+ break;
343
+ }
344
+ const text = await readFile(join(dir, name), 'utf8');
345
+ if (!text.includes(SHIM_MARKER) && !text.includes(CHAIN_MARKER)) {
346
+ removable = false;
347
+ break;
348
+ }
349
+ }
350
+ return { path: HOOKS_DIR_LEGACY, removable };
351
+ }
352
+
304
353
  /**
305
354
  * Write a planned install: shims, chains, removals, and the git routing (plan 0003 R47).
306
355
  *
@@ -309,10 +358,11 @@ export async function planHooksInstall(root, { manifest, exists }) {
309
358
  * names the run that actually changed something. Reporting stays with the
310
359
  * caller for the same reason it does in `planHooksInstall`.
311
360
  *
312
- * @returns {{wrote: string[]}} the shim paths written, removed, or refreshed
361
+ * @returns {{wrote: string[], removed: string[]}} the shim paths written,
362
+ * removed or refreshed, and the legacy-directory files taken down
313
363
  */
314
364
  export async function applyHooksInstall(root, planned, journal) {
315
- const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current } =
365
+ const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current, legacy } =
316
366
  planned;
317
367
  // Shim and evidence bytes are journaled so `undo` restores them (R49), with
318
368
  // the executable bit they are written with: a shim the run chmodded is one
@@ -322,6 +372,7 @@ export async function applyHooksInstall(root, planned, journal) {
322
372
  // at their directory silently stops the repository's own hooks (review 0003
323
373
  // Q22).
324
374
  const wrote = [];
375
+ const removed = [];
325
376
  await mkdir(join(root, HOOKS_DIR), { recursive: true });
326
377
  for (const event of wanted) {
327
378
  const path = join(root, fileFor(event));
@@ -352,7 +403,19 @@ export async function applyHooksInstall(root, planned, journal) {
352
403
  } else if (current === wants) {
353
404
  await journaledGitConfig(root, journal, HOOKS_PATH_KEY, null);
354
405
  }
355
- return { wrote };
406
+ // The move into the state directory (ADR-122): the routing now points
407
+ // elsewhere, so a legacy directory left behind is the only place that looks
408
+ // like a hooks directory while holding nothing git reads. The scan in the
409
+ // plan already judged it removable — every file a simt-dev marker — so
410
+ // deletion is journaled per file, and `undo` restores the directory.
411
+ if (legacy?.removable) {
412
+ for (const name of await readdir(join(root, HOOKS_DIR_LEGACY))) {
413
+ await journaledRemoveFile(root, journal, join(root, HOOKS_DIR_LEGACY, name));
414
+ removed.push(`${HOOKS_DIR_LEGACY}/${name}`);
415
+ }
416
+ await rm(join(root, HOOKS_DIR_LEGACY), { recursive: true });
417
+ }
418
+ return { wrote, removed };
356
419
  }
357
420
 
358
421
  /**
@@ -382,8 +445,8 @@ export async function plannedDiffs(root, planned) {
382
445
  return pairs;
383
446
  }
384
447
 
385
- async function installHooks(root, flags) {
386
- const { manifest, exists, error } = await readManifest(root);
448
+ async function installHooks(root, flags, ctx = repoContext(root)) {
449
+ const { manifest, exists, error } = await ctx.manifest();
387
450
  if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG);
388
451
  if (!exists) {
389
452
  return fail(`${root} has no ${MANIFEST_FILE}; gates must be declared before hooks can run them`, EXIT.CONFIG, `run: ${CLI} init --root ${root}`);
@@ -409,7 +472,8 @@ async function installHooks(root, flags) {
409
472
  evidenceExisting,
410
473
  routed,
411
474
  takeover,
412
- } = await planHooksInstall(root, { manifest, exists });
475
+ legacy,
476
+ } = await planHooksInstall(root, { manifest, exists }, ctx);
413
477
 
414
478
  // Read through `writeMode` like every other writer, and only now: this command
415
479
  // used to consult `flags.yes` and `flags.dryRun` directly and write when neither
@@ -438,7 +502,6 @@ async function installHooks(root, flags) {
438
502
  ' and replace it with a gate that checks nothing.',
439
503
  `REMEDY add the gates this repo can actually run to ${MANIFEST_FILE}, then re-run.`,
440
504
  ].join('\n'),
441
- { maxBytes: flags.maxBytes ?? 0 },
442
505
  );
443
506
  return EXIT.FAIL;
444
507
  }
@@ -487,11 +550,21 @@ async function installHooks(root, flags) {
487
550
  note: 'commit-msg checks a verification claim against the ledger',
488
551
  });
489
552
  }
553
+ if (legacy?.removable) {
554
+ plan.push({
555
+ file: HOOKS_DIR_LEGACY,
556
+ action: 'removed',
557
+ // Named in the plan for the same reason the obsolete shims are: the
558
+ // reader approving this write is about to lose a directory, and the
559
+ // routing line above does not say the old one goes too (ADR-122).
560
+ note: `legacy shim directory; the shims now live in ${HOOKS_DIR}`,
561
+ });
562
+ }
490
563
 
491
564
  // The routing this install leaves behind, which is not always `wants`. A repository
492
565
  // that removed its last gate has no shims to route to, and leaving `core.hooksPath`
493
566
  // pointed at the directory would stop its own hooks running - the plan has to say so,
494
- // because a plan that prints "core.hooksPath updated .simt-hooks -> .simt-hooks" while
567
+ // because a plan that prints "core.hooksPath updated .simt/hooks -> .simt/hooks" while
495
568
  // the command unsets it is worse than no plan.
496
569
  const leaves = routed ? wants : null;
497
570
  const pathAction =
@@ -521,7 +594,6 @@ async function installHooks(root, flags) {
521
594
  ' Taking over replaces that routing. Inspect the existing hooks first,',
522
595
  ' then re-run with --yes to accept, or set core.hooksPath yourself.',
523
596
  ].join('\n'),
524
- { maxBytes: flags.maxBytes ?? 0 },
525
597
  );
526
598
  return EXIT.FAIL;
527
599
  }
@@ -566,7 +638,6 @@ async function installHooks(root, flags) {
566
638
  : `PLAN nothing written; apply with: ${CLI} hooks install --root ${root} --yes`,
567
639
  manifest,
568
640
  ),
569
- { maxBytes: flags.maxBytes ?? 0 },
570
641
  );
571
642
  // After the report: the report says what, the diff shows it (R50).
572
643
  emitDiffs(diffs, flags);
@@ -578,7 +649,7 @@ async function installHooks(root, flags) {
578
649
  const journal = [];
579
650
  await applyHooksInstall(
580
651
  root,
581
- { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current },
652
+ { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current, legacy },
582
653
  journal,
583
654
  );
584
655
  await commitJournal(root, 'hooks install', journal);
@@ -655,7 +726,6 @@ async function installHooks(root, flags) {
655
726
  routed ? 'WROTE shims and set core.hooksPath' : 'WROTE no shims remain; core.hooksPath unset',
656
727
  manifest,
657
728
  ),
658
- { maxBytes: flags.maxBytes ?? 0 },
659
729
  );
660
730
  // After the report: the report says what, the diff shows it (R50).
661
731
  emitDiffs(diffs, flags);
@@ -684,8 +754,8 @@ function describeGates(manifest, key) {
684
754
  }).join(', ');
685
755
  }
686
756
 
687
- async function hooksStatus(root, flags) {
688
- const { manifest, exists, error } = await readManifest(root);
757
+ async function hooksStatus(root, flags, ctx = repoContext(root)) {
758
+ const { manifest, exists, error } = await ctx.manifest();
689
759
  if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG);
690
760
 
691
761
  const state = await readHooksState(root, { declaresGate: (e) => wantsShim(manifest, exists, e) });
@@ -770,14 +840,14 @@ async function hooksStatus(root, flags) {
770
840
  const unknown = [...(manifest?.gates?.preCommit ?? []), ...(manifest?.gates?.prePush ?? [])].filter((n) => !GATES[n]);
771
841
  if (unknown.length > 0) {
772
842
  lines.push('', `unknown gates (the hook will fail): ${unknown.join(', ')}`);
773
- lines.push(`known gates: ${knownGates().join(', ')}`);
843
+ lines.push(`known gates: ${gateNames().join(', ')}`);
774
844
  }
775
845
  }
776
846
  if (problems.length > 0) {
777
847
  lines.push('', 'problems:');
778
848
  for (const p of problems) lines.push(` ${p}`);
779
849
  }
780
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
850
+ emit(lines.join('\n'));
781
851
  return code;
782
852
  }
783
853
 
@@ -14,9 +14,10 @@ import { CLI } from '../name.mjs';
14
14
  import { commitJournal, journaledWriteFile } from '../journal.mjs';
15
15
  import { diffField, emitDiffs, readBefore } from '../diff.mjs';
16
16
  import { CLAUDE_SETTINGS, IGNORE_FILE, parseIgnoreList, planIgnore } from '../ignore.mjs';
17
+ import { commandRoot } from '../repo-root.mjs';
17
18
 
18
19
  export async function ignoreCommand(args, flags) {
19
- const root = flags.root ?? process.cwd();
20
+ const root = commandRoot(flags);
20
21
 
21
22
  let source;
22
23
  try {
@@ -9,7 +9,7 @@ import { readFile } from 'node:fs/promises';
9
9
  import { basename, join, resolve } from 'node:path';
10
10
  import { promisify } from 'node:util';
11
11
 
12
- import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
12
+ import { EXIT, fail, present } from '../output/index.mjs';
13
13
  import { CLI } from '../name.mjs';
14
14
  import { discover } from '../compare/discover.mjs';
15
15
  import { extract } from '../index/extract.mjs';
@@ -25,6 +25,7 @@ import {
25
25
  import { decisionHits, scopeHits, sortHits, sourceRoots } from '../index/govern.mjs';
26
26
  import { decisionRecords, ruleScopes } from './why.mjs';
27
27
  import { resolveWorkspace } from '../workspace.mjs';
28
+ import { commandRoot } from '../repo-root.mjs';
28
29
 
29
30
  const exec = promisify(execFile);
30
31
  const TEXT_EXT = new Set(['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'yaml', 'yml', 'md', 'toml', 'xml', 'py', 'go', 'kt', 'java', 'rs']);
@@ -35,7 +36,7 @@ export async function impactCommand(args, flags, commands = []) {
35
36
  if (args.length > 1) return fail(`unknown impact argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} impact <file|symbol>`);
36
37
  const target = args[0];
37
38
  if (!target) return fail(`a file or a symbol is required`, EXIT.USAGE, `run: ${CLI} impact <file|symbol>`);
38
- const root = flags.root ?? process.cwd();
39
+ const root = commandRoot(flags);
39
40
  const listed = await tracked(root);
40
41
  if (listed.error) return listed.error;
41
42
  const files = new Set(listed.files);
@@ -63,11 +64,7 @@ export async function impactCommand(args, flags, commands = []) {
63
64
  consumers: consumers.references,
64
65
  truncated,
65
66
  };
66
- if (isJson(flags)) {
67
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
68
- return EXIT.OK;
69
- }
70
- emit(render(payload), { maxBytes: flags.maxBytes ?? 0 });
67
+ present(payload, () => render(payload), flags);
71
68
  return EXIT.OK;
72
69
  }
73
70