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
@@ -20,7 +20,8 @@ import { resolveVaultDir } from '../bench/mine.mjs';
20
20
  import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
21
21
  import { nearMatches } from '../resolve/near.mjs';
22
22
  import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
23
- import { CLI, STATE_DIR } from '../name.mjs';
23
+ import { CLI } from '../name.mjs';
24
+ import { LOCAL_DIR } from '../state.mjs';
24
25
 
25
26
  async function readSkillRawText(skill, workspaceRoot, root) {
26
27
  const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
@@ -44,7 +45,7 @@ async function readSkillRawText(skill, workspaceRoot, root) {
44
45
 
45
46
  async function loadGoldPathsByTask(root) {
46
47
  const map = new Map();
47
- const tasksDir = join(root, STATE_DIR, 'bench', 'tasks');
48
+ const tasksDir = join(root, '.simt', 'bench', 'tasks');
48
49
  const vaultRoot = resolveVaultDir(root);
49
50
  const entries = (await readdir(tasksDir).catch(() => [])).filter((f) => f.endsWith('.json')).sort();
50
51
  for (const entry of entries) {
@@ -172,26 +173,26 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
172
173
  triggerStats,
173
174
  });
174
175
 
175
- const outDir = join(root, STATE_DIR, 'compiled', 'skills', skill.name);
176
- const proposalPath = join(root, STATE_DIR, 'proposals', 'skills', `${skill.name}.json`);
176
+ const outDir = join(root, LOCAL_DIR, 'compiled', 'skills', skill.name);
177
+ const proposalPath = join(root, LOCAL_DIR, 'proposals', 'skills', `${skill.name}.json`);
177
178
  const writtenFiles = [];
178
179
 
179
180
  if (mode.apply) {
180
181
  await mkdir(outDir, { recursive: true });
181
182
  await writeFile(join(outDir, 'SKILL.md'), compiled.mainText, 'utf8');
182
- writtenFiles.push(`${STATE_DIR}/compiled/skills/${skill.name}/SKILL.md`);
183
+ writtenFiles.push(`${LOCAL_DIR}/compiled/skills/${skill.name}/SKILL.md`);
183
184
 
184
185
  for (const ref of compiled.references) {
185
186
  if (!ref.content) continue;
186
187
  const refFull = join(outDir, ref.path);
187
188
  await mkdir(dirname(refFull), { recursive: true });
188
189
  await writeFile(refFull, ref.content, 'utf8');
189
- writtenFiles.push(`${STATE_DIR}/compiled/skills/${skill.name}/${ref.path}`);
190
+ writtenFiles.push(`${LOCAL_DIR}/compiled/skills/${skill.name}/${ref.path}`);
190
191
  }
191
192
 
192
193
  await mkdir(dirname(proposalPath), { recursive: true });
193
194
  await writeFile(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`, 'utf8');
194
- writtenFiles.push(`${STATE_DIR}/proposals/skills/${skill.name}.json`);
195
+ writtenFiles.push(`${LOCAL_DIR}/proposals/skills/${skill.name}.json`);
195
196
  }
196
197
 
197
198
  if (isJson(flags)) {
@@ -20,15 +20,17 @@
20
20
  // 3. References come with the skill. A skill whose reference files are missing
21
21
  // is worse than one that was never installed, because it looks installed.
22
22
 
23
- import { cp, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
23
+ import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
24
+ import { commitJournal, journaledCopyDir, journaledWriteFile } from '../journal.mjs';
25
+ import { diffField, emitDiffs, readBefore } from '../diff.mjs';
24
26
  import { existsSync } from 'node:fs';
25
- import { dirname, join } from 'node:path';
27
+ import { dirname, join, relative, sep } from 'node:path';
26
28
 
27
29
  import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
28
30
  import { loadRegistry, measureSkill } from '../graph/load.mjs';
29
31
  import { fingerprint } from '../resolve/fingerprint.mjs';
30
32
  import { readCapability } from '../hooks/capability.mjs';
31
- import { MANIFEST, buildManifest, readManifest, writeManifest } from '../projection/manifest.mjs';
33
+ import { MANIFEST, buildManifest, manifestPath, manifestText, readManifest, writeManifest } from '../projection/manifest.mjs';
32
34
  import { provenanceDigest } from '../projection/project.mjs';
33
35
  import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
34
36
  import { nearMatches } from '../resolve/near.mjs';
@@ -87,6 +89,18 @@ export async function installCommand(args, flags) {
87
89
 
88
90
  const found = await locateSource(pick.source, { workspaceRoot: ws.root, root });
89
91
  if (!found.path) {
92
+ // A repo-side candidate with the wrong size is a third reason, and it needs
93
+ // its own wording: the file is on the reader's disk, so "this repo does not
94
+ // hold it" would send them looking for a file that is already there. The
95
+ // variant path calls the same failure an impostor (chooseSource).
96
+ const impostor = found.candidates.find((c) => c.how === 'repo');
97
+ if (impostor) {
98
+ return fail(
99
+ `${skill.name}: ${root}/${pick.source.path} exists but is not the declared body (${pick.source.bytes} bytes declared)`,
100
+ EXIT.USAGE,
101
+ `run: ${CLI} graph lint --workspace <path> to see what the registry expects, or delete the local copy`,
102
+ );
103
+ }
90
104
  // Two reasons land here and they need different wording: no workspace at all,
91
105
  // or a workspace that should hold this body and does not. An install that
92
106
  // cannot read a body has failed, so this is never a degraded mode - the
@@ -161,10 +175,18 @@ export async function installCommand(args, flags) {
161
175
  // `--yes --json` a command that reported a plan and changed nothing - the one
162
176
  // combination a script writes. The payload is now emitted once, at the end,
163
177
  // after the write it describes.
178
+ const mode = writeMode(flags);
179
+ // The `--diff` pairs, read before anything is written so the before side is
180
+ // true in preview and in apply alike (R50). Built before the early returns
181
+ // below so the shared `report()` can carry the field in every mode.
182
+ const diffs = flags.diff === true ? await skillDiffs(root, { src, dest, destRel: payload.dest, flat: measured.flat, name, record, repo }) : [];
164
183
  const report = () => {
165
- if (isJson(flags)) emitJson(payload);
184
+ // `--json` used to return here, before anything was written, which made
185
+ // `--yes --json` a command that reported a plan and changed nothing - the one
186
+ // combination a script writes. The payload is now emitted once, at the end,
187
+ // after the write it describes.
188
+ if (isJson(flags)) emitJson({ ...payload, ...diffField(diffs, flags) });
166
189
  };
167
- const mode = writeMode(flags);
168
190
 
169
191
  if (!isJson(flags)) {
170
192
  emit(
@@ -213,18 +235,22 @@ export async function installCommand(args, flags) {
213
235
  // "nothing written" and then wrote. It belongs on the path that does not write.
214
236
  if (!mode.apply) {
215
237
  note(`note dry-run nothing written; apply with: ${CLI} skills install ${name} --yes`);
238
+ // After the report: the report says what, the diff shows it (R50).
239
+ emitDiffs(diffs, flags);
216
240
  report();
217
241
  return EXIT.OK;
218
242
  }
219
243
 
244
+ // Every repo write below is journaled, so `undo` restores this run (R49).
245
+ const journal = [];
220
246
  await mkdir(dirname(join(dest, 'SKILL.md')), { recursive: true });
221
247
  try {
222
248
  if (measured.flat) {
223
- await writeFile(join(dest, 'SKILL.md'), await wrapFlat(src, name), 'utf8');
249
+ await journaledWriteFile(root, journal, join(dest, 'SKILL.md'), await wrapFlat(src, name));
224
250
  } else {
225
251
  // References come with the skill: a skill whose reference files are missing
226
252
  // is worse than one that was never installed, because it looks installed.
227
- await cp(src, dest, { recursive: true });
253
+ await journaledCopyDir(root, journal, src, dest);
228
254
  }
229
255
  } catch (error) {
230
256
  return fail(
@@ -236,12 +262,15 @@ export async function installCommand(args, flags) {
236
262
  payload.written = true;
237
263
  note(`wrote ${payload.dest}`);
238
264
 
239
- const recorded = await recordLocal(root, record, skill.name, repo);
265
+ const recorded = await recordLocal(root, record, skill.name, repo, journal);
240
266
  note(`${recorded.created ? 'created' : 'updated'} ${MANIFEST_FILE} localSkills`);
241
267
  if (recorded.created) {
242
268
  note(`note this repo had no manifest; ${CLI} init still needs to run to project the rules`);
243
269
  }
244
270
  note(`note ${name} is now repo-local and is excluded from central-drift accounting`);
271
+ await commitJournal(root, 'skills install', journal);
272
+ // After the report: the report says what, the diff shows it (R50).
273
+ emitDiffs(diffs, flags);
245
274
  report();
246
275
  return EXIT.OK;
247
276
  }
@@ -502,29 +531,82 @@ async function fileSha(path) {
502
531
  * default gates in it is a promise the repo cannot keep, and a command that installs one
503
532
  * skill is not entitled to make it.
504
533
  */
505
- async function recordLocal(root, record, name, repo) {
534
+ /** Every regular file under a directory, absolute paths. Missing dir → []. */
535
+ async function treeFiles(dir) {
536
+ let st;
537
+ try {
538
+ st = await stat(dir);
539
+ } catch (error) {
540
+ if (error.code === 'ENOENT') return [];
541
+ throw error;
542
+ }
543
+ if (!st.isDirectory()) return [dir];
544
+ const out = [];
545
+ for (const name of await readdir(dir)) {
546
+ out.push(...(await treeFiles(join(dir, name))));
547
+ }
548
+ return out;
549
+ }
550
+
551
+ function rel(root, path) {
552
+ return relative(root, path).split(sep).join('/');
553
+ }
554
+
555
+ /**
556
+ * The `--diff` pairs for an install: the skill files plus the manifest record
557
+ * (plan 0003 R50). The manifest pair needs the same object the write applies,
558
+ * so the plan is shared with `recordLocal` rather than derived twice.
559
+ */
560
+ async function skillDiffs(root, { src, dest, destRel, flat, name, record, repo }) {
561
+ const pairs = [];
562
+ if (flat) {
563
+ pairs.push({
564
+ path: `${destRel}/SKILL.md`,
565
+ before: await readBefore(join(dest, 'SKILL.md')),
566
+ after: await wrapFlat(src, name),
567
+ });
568
+ } else {
569
+ // References come with the skill, so the diff walks the whole tree: only
570
+ // changed files render, the rest filter out as equal.
571
+ for (const abs of await treeFiles(src)) {
572
+ const destAbs = join(dest, relative(src, abs));
573
+ pairs.push({ path: rel(root, destAbs), before: await readBefore(destAbs), after: await readFile(abs, 'utf8') });
574
+ }
575
+ }
576
+ const planned = await planLocalRecord(root, record, name, repo);
577
+ if (!planned.error) {
578
+ pairs.push({
579
+ path: rel(root, await manifestPath(root)),
580
+ before: planned.manifest === null ? null : await readBefore(await manifestPath(root)),
581
+ after: manifestText(planned.next),
582
+ });
583
+ }
584
+ return pairs;
585
+ }
586
+
587
+ /**
588
+ * What `recordLocal` will write, computed without writing (plan 0003 R50).
589
+ * The diff shares this so the pair shows the same object the write applies.
590
+ */
591
+ async function planLocalRecord(root, record, name, repo) {
506
592
  const { manifest, error } = await readManifest(root);
507
- if (error) return { written: false, reason: error };
508
-
509
- // A repo with no manifest gets one here, so this function writes a *promise* on that
510
- // repo's behalf: it decides which gates the repo claims to run. Without a capability
511
- // probe it wrote `format`, `lint`, `test` and `typecheck` into a repository with no
512
- // scripts and no formatter, and `check` then reported four UNRUNNABLE gates. The moment
513
- // hooks route through the manifest - which is the entire point of a gate - every commit
514
- // in that repo is blocked, over gates nobody declared. ADR-017 exists to prevent exactly
515
- // that, and a manifest written by a command that installs one skill was the way round it.
516
- // `init` probes for the same reason; a second writer that does not is a defect waiting
517
- // for the next command that needs a manifest.
518
- const base =
519
- manifest ??
520
- (await buildManifest({
521
- root,
522
- stack: repo.stack,
523
- sha: await provenanceDigest(),
524
- capability: await readCapability(root),
525
- }));
593
+ if (error) return { error };
594
+ const base = manifest ?? (await buildManifest({ root, stack: repo.stack, sha: await provenanceDigest(), capability: await readCapability(root) }));
526
595
  const existing = Array.isArray(base.localSkills) ? base.localSkills : [];
527
596
  const kept = existing.filter((e) => (typeof e === 'string' ? e : e?.name) !== name);
528
- await writeManifest(root, { ...base, localSkills: [...kept, record] });
529
- return { written: true, created: manifest === null };
597
+ return { manifest, next: { ...base, localSkills: [...kept, record] }, created: manifest === null };
598
+ }
599
+
600
+ async function recordLocal(root, record, name, repo, journal) {
601
+ // The manifest this writes is `planned.next`, built by `planLocalRecord`:
602
+ // a repo with no manifest gets one here, which is a promise about which
603
+ // gates the repo claims to run. Without the capability probe inside the
604
+ // plan it wrote `format`, `lint`, `test` and `typecheck` into a repository
605
+ // with no scripts and no formatter, and `check` then reported four
606
+ // UNRUNNABLE gates that blocked every commit (ADR-017). Deriving the object
607
+ // a second way here would be a second answer to what the repo promises.
608
+ const planned = await planLocalRecord(root, record, name, repo);
609
+ if (planned.error) return { written: false, reason: planned.error };
610
+ await writeManifest(root, planned.next, journal);
611
+ return { written: true, created: planned.created };
530
612
  }