simt-dev 0.6.3 → 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 (137) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -2
  3. package/agents/simt-agent.md +84 -0
  4. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  5. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
  6. package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
  7. package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
  8. package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
  9. 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
  10. 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
  11. package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
  12. package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
  13. package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
  14. 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
  15. 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
  16. package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
  17. package/docs/decisions/README.md +9 -0
  18. package/package.json +4 -2
  19. package/rules/core.md +2 -2
  20. package/rules/mdc/test-gate.mdc +2 -0
  21. package/rules/stacks/android.md +3 -3
  22. package/rules/stacks/connectiq.md +2 -2
  23. package/rules/stacks/dart.md +1 -1
  24. package/rules/stacks/deno.md +1 -1
  25. package/rules/stacks/dotnet.md +1 -1
  26. package/rules/stacks/elixir.md +1 -1
  27. package/rules/stacks/go.md +1 -1
  28. package/rules/stacks/jvm.md +3 -2
  29. package/rules/stacks/node.md +2 -2
  30. package/rules/stacks/python.md +7 -6
  31. package/rules/stacks/ruby.md +1 -1
  32. package/rules/stacks/rust.md +1 -1
  33. package/rules/stacks/swift.md +1 -1
  34. package/rules/stacks/typescript.md +2 -2
  35. package/rules/stacks/uv.md +3 -2
  36. package/skills/simt-commands/SKILL.md +70 -0
  37. package/src/advice/advice.mjs +7 -3
  38. package/src/agenthooks/decide.mjs +141 -9
  39. package/src/agenthooks/opencode.mjs +72 -0
  40. package/src/agenthooks/prompt.mjs +6 -2
  41. package/src/agenthooks/render.mjs +186 -19
  42. package/src/agenthooks/shim.mjs +55 -0
  43. package/src/agents/render.mjs +277 -0
  44. package/src/argv.mjs +8 -0
  45. package/src/bench/ablate.mjs +31 -14
  46. package/src/bench/mine.mjs +48 -1
  47. package/src/bench/probes.mjs +5 -6
  48. package/src/bench/report.mjs +243 -80
  49. package/src/bench/run.mjs +12 -11
  50. package/src/bench/variants.mjs +112 -40
  51. package/src/brief/assemble.mjs +6 -0
  52. package/src/ci/workflow.mjs +16 -7
  53. package/src/commands/agent.mjs +206 -0
  54. package/src/commands/agenthook.mjs +109 -15
  55. package/src/commands/bench.mjs +2 -1
  56. package/src/commands/compare.mjs +8 -7
  57. package/src/commands/decide.mjs +6 -5
  58. package/src/commands/evidence.mjs +13 -3
  59. package/src/commands/gate.mjs +85 -19
  60. package/src/commands/graph.mjs +18 -5
  61. package/src/commands/help.mjs +35 -14
  62. package/src/commands/hooks.mjs +68 -25
  63. package/src/commands/impact.mjs +4 -2
  64. package/src/commands/index.mjs +17 -4
  65. package/src/commands/init.mjs +169 -37
  66. package/src/commands/lease.mjs +203 -35
  67. package/src/commands/lessons.mjs +65 -9
  68. package/src/commands/map.mjs +33 -18
  69. package/src/commands/plugin.mjs +1 -1
  70. package/src/commands/repoindex.mjs +1 -0
  71. package/src/commands/rules.mjs +92 -18
  72. package/src/commands/skills.mjs +60 -25
  73. package/src/commands/skillscompile.mjs +8 -7
  74. package/src/commands/skillsinstall.mjs +12 -0
  75. package/src/commands/slice.mjs +377 -18
  76. package/src/commands/sync.mjs +37 -22
  77. package/src/commands/tests.mjs +5 -2
  78. package/src/commands/undo.mjs +24 -16
  79. package/src/commands/upgrade.mjs +33 -1
  80. package/src/commands/why.mjs +10 -4
  81. package/src/compare/hooksstate.mjs +23 -1
  82. package/src/compare/learn.mjs +82 -57
  83. package/src/compare/parallel.mjs +50 -0
  84. package/src/compare/row.mjs +26 -1
  85. package/src/completions.mjs +5 -0
  86. package/src/decisions/dir.mjs +84 -0
  87. package/src/decisions/index.mjs +109 -11
  88. package/src/distill/distill.mjs +3 -1
  89. package/src/ecosystem/node-runner.mjs +144 -0
  90. package/src/explain.mjs +18 -6
  91. package/src/graph/load.mjs +38 -19
  92. package/src/hooks/capability.mjs +14 -1
  93. package/src/hooks/dispatcher.mjs +335 -13
  94. package/src/hooks/resolve.mjs +9 -3
  95. package/src/hooks/shim.mjs +12 -7
  96. package/src/identity.mjs +67 -14
  97. package/src/index/extract.mjs +192 -11
  98. package/src/index/persist.mjs +307 -47
  99. package/src/index/store.mjs +263 -60
  100. package/src/index/tests.mjs +6 -35
  101. package/src/journal.mjs +296 -77
  102. package/src/kernel/card.mjs +37 -12
  103. package/src/kernel/checks.mjs +44 -9
  104. package/src/kernel/variants.mjs +27 -16
  105. package/src/lease.mjs +97 -15
  106. package/src/ledger/index.mjs +91 -25
  107. package/src/mcp/resources.mjs +5 -3
  108. package/src/mcp/server.mjs +81 -22
  109. package/src/mcp/tools.mjs +45 -9
  110. package/src/memory/decide.mjs +98 -43
  111. package/src/memory/lessons.mjs +168 -33
  112. package/src/name.mjs +9 -0
  113. package/src/plan/index.mjs +31 -8
  114. package/src/plan/template.mjs +27 -5
  115. package/src/plugins.mjs +6 -2
  116. package/src/prbody.mjs +7 -5
  117. package/src/projection/baselines.mjs +1 -1
  118. package/src/projection/disclosure.mjs +2 -2
  119. package/src/projection/loaders.mjs +41 -1
  120. package/src/projection/lock.mjs +21 -14
  121. package/src/projection/manifest.mjs +69 -19
  122. package/src/projection/markers.mjs +43 -3
  123. package/src/projection/mdc.mjs +34 -7
  124. package/src/projection/project.mjs +162 -18
  125. package/src/projection/rules.mjs +54 -3
  126. package/src/projection/surfaces.mjs +9 -1
  127. package/src/repo-root.mjs +69 -0
  128. package/src/resolve/body.mjs +4 -0
  129. package/src/resolve/fingerprint.mjs +5 -0
  130. package/src/resolve/local.mjs +58 -0
  131. package/src/review/critic.mjs +4 -2
  132. package/src/secrets.mjs +165 -22
  133. package/src/state.mjs +76 -0
  134. package/src/status.mjs +9 -4
  135. package/src/toolwords.mjs +5 -0
  136. package/src/tsconfig.mjs +39 -99
  137. package/templates/plan/tracker.json +1 -1
@@ -18,7 +18,7 @@
18
18
  import { execFile } from 'node:child_process';
19
19
  import { promisify } from 'node:util';
20
20
  import { mkdir, readFile, symlink } from 'node:fs/promises';
21
- import { commitJournal, journaledWriteFile } from '../journal.mjs';
21
+ import { commitJournal, ignoreJournal, journaledWriteFile } from '../journal.mjs';
22
22
  import { invalidateCheckCache } from '../status.mjs';
23
23
  import { dirname, join } from 'node:path';
24
24
 
@@ -31,10 +31,12 @@ import { readCapability } from '../hooks/capability.mjs';
31
31
  import { readHooksState } from '../compare/hooksstate.mjs';
32
32
  import { planWorkflow, renderWorkflow, WORKFLOWS_DIR } from '../ci/workflow.mjs';
33
33
  import { planDependabot, renderDependabot } from '../ci/dependabot.mjs';
34
- import { planTsconfigBase, renderTsconfigBase, TSCONFIG_BASE_FILE } from '../tsconfig.mjs';
34
+ import { planTsconfigBase } from '../tsconfig.mjs';
35
35
  import {
36
36
  IDENTITY_FILE,
37
37
  INCLUDE_KEY,
38
+ gitignoreWithIdentity,
39
+ identityIgnored,
38
40
  ignoreIdentity,
39
41
  includeOwnIdentity,
40
42
  includesOwnIdentity,
@@ -44,10 +46,15 @@ import {
44
46
  import { pointerState, pointerTargets } from '../projection/project.mjs';
45
47
  import { surfacesLine } from '../projection/surfaces.mjs';
46
48
  import { plan, provenanceDigest, PRIMARY_TARGET, LOCAL_PROFILE_DIR } from '../projection/project.mjs';
49
+ import { planAgents, writeAgentPlan, writeSkillFile } from '../agents/render.mjs';
50
+ import { computeLock } from '../projection/lock.mjs';
47
51
  import { parse as parseRegion } from '../projection/markers.mjs';
52
+ import { reindex, SQLITE_FILE } from '../index/store.mjs';
48
53
  import { CLI } from '../name.mjs';
49
54
  import { MANIFEST_FILE } from '../name.mjs';
50
55
  import { packageVersion, hasNpm } from '../package-version.mjs';
56
+ import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
57
+ import { PACKAGE_NAME } from '../name.mjs';
51
58
 
52
59
  const run = promisify(execFile);
53
60
 
@@ -68,6 +75,20 @@ export async function initCommand(positional, flags) {
68
75
  // a repository without node_modules obtains the binary in CI.
69
76
  const version = packageVersion();
70
77
  const npm = await hasNpm(root);
78
+ // Q31: the CI install step and the plan row that names it read the same
79
+ // resolver as the card and the gates — lockfile first, `packageManager` next.
80
+ // `manager` is the Node manager id or null (no package.json: binary channel).
81
+ let manager = null;
82
+ if (npm) {
83
+ let packageManager = null;
84
+ try {
85
+ const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
86
+ if (typeof pkg.packageManager === 'string') packageManager = pkg.packageManager;
87
+ } catch {
88
+ // Unreadable here is reported by the manifest path; the default stays npm.
89
+ }
90
+ manager = resolveNodeRunner({ facts: detected.facts, packageManager }).id;
91
+ }
71
92
  const existing = await readManifest(root);
72
93
  const { manifest: current, exists, error } = existing;
73
94
  if (error) {
@@ -87,9 +108,15 @@ export async function initCommand(positional, flags) {
87
108
  const manifest = buildManifest({ root, stack: detected.stack, sha, version, existing: current, capability, facts: detected.facts });
88
109
  const findings = await scanRepo(root);
89
110
  const summary = summarise(findings);
111
+ // Every repo-file write is journaled, so `undo` can restore this run (R49).
112
+ // Created before `plan()` because the skill file must be on disk when
113
+ // `installedSkills` reads the directory for the region's disclosure section.
114
+ const journal = [];
115
+ await writeSkillFile(root, journal);
90
116
  const projected = await plan(root, flags);
91
117
  const region = projected.targets[0];
92
118
  const mdcProblems = projected.mdcProblems ?? [];
119
+ const crossRefs = projected.crossRefs ?? [];
93
120
 
94
121
  // Which editors this repo's rules reach. `--surfaces` wins for this run and is
95
122
  // recorded in the manifest so the choice survives; with nothing declared the
@@ -105,7 +132,30 @@ export async function initCommand(positional, flags) {
105
132
  // not a line to save.
106
133
  else pointerPlan.push({ ...p, state: st.state });
107
134
  }
108
- manifest.surfaces = projected.surfaces;
135
+ // Record the choice only when it was made: a flag for this run, or a key the
136
+ // manifest already carries with a real value. A fresh adoption with neither gets
137
+ // no surfaces of its own - "absent" means the global standard applies, and writing
138
+ // today's default into every fresh manifest would freeze it for every release after
139
+ // (review 0003 Q30). With a declared array, `projected.surfaces` is that same array
140
+ // resolved, so the assignment records rather than rewrites.
141
+ if (flags.surfaces !== undefined || Array.isArray(existing?.surfaces)) {
142
+ manifest.surfaces = projected.surfaces;
143
+ }
144
+
145
+ // The lock's input digest includes the manifest this run writes. On a fresh adoption
146
+ // there is no manifest on disk yet, so the lock `plan` computed is the one for a repo
147
+ // that has none - and the adoption would be red on `check --frozen` the moment the
148
+ // manifest lands. Recompute it with the manifest about to write, so the bytes in the
149
+ // plan, the diff and the write are the same (Q29). A re-init preserves every lock
150
+ // input, so its `plan` bytes are already right and are left alone. After the
151
+ // surfaces line: that is the last mutation of the manifest, and the digest must
152
+ // cover the bytes that land on disk, not an earlier draft of them.
153
+ const lockTarget = projected.targets.find((t) => t.kind === 'lock');
154
+ if (lockTarget && !exists) {
155
+ const lock = await computeLock(root, flags, manifest);
156
+ lockTarget.text = `${JSON.stringify(lock, null, 2)}\n`;
157
+ lockTarget.bytes = Buffer.byteLength(lockTarget.text);
158
+ }
109
159
 
110
160
  const adoption = exists || Boolean(region?.exists);
111
161
  // One decider for `--yes --dry-run`: the inline copy used to agree with it today,
@@ -120,10 +170,25 @@ export async function initCommand(positional, flags) {
120
170
  const ci = await planWorkflow(root);
121
171
  const dependabot = await planDependabot(root);
122
172
  const tsconfigBase = await planTsconfigBase(root);
173
+ // ADR-111. The one agent this tool ships, planned with the rest so the operator sees
174
+ // it in the same preview that lists everything else. Written in the apply path with
175
+ // everything else, journaled the same way.
176
+ const agentPlan = await planAgents(root);
123
177
  // Per-developer, and planned with the rest so the operator sees it in the same
124
178
  // preview that lists everything else. `exists` short-circuits the write: a
125
179
  // repository that already has one keeps it exactly as it is.
126
180
  const identity = await resolveIdentity(root);
181
+ // The .gitignore edit and the include repoint are writes too, so they get plan
182
+ // lines of their own: a line the run does not print is a write the reader
183
+ // approves from the diff afterwards (review 0003 Q30). The before-bytes are read
184
+ // once here and reused by the --diff pair below, so the preview's before side is
185
+ // the same bytes the plan's verb was decided from.
186
+ let gitignoreBefore = null;
187
+ let gitignoreAction = null;
188
+ if (!identity.exists && identity.from !== null && !(await identityIgnored(root))) {
189
+ gitignoreBefore = await readBefore(join(root, '.gitignore'));
190
+ gitignoreAction = gitignoreBefore === null ? 'create' : 'update';
191
+ }
127
192
  const identityPlan = {
128
193
  file: IDENTITY_FILE,
129
194
  action: identity.exists ? 'unchanged' : identity.from === null ? 'skip' : 'created',
@@ -139,6 +204,9 @@ export async function initCommand(positional, flags) {
139
204
  // pointing at another repository's identity file works here and breaks for the
140
205
  // next person who clones without that sibling checkout.
141
206
  include: await includesOwnIdentity(root),
207
+ // The .gitignore edit, planned like every other write: null when the entry is
208
+ // already there or no file is being created, 'create'/'update' otherwise.
209
+ gitignore: gitignoreAction,
142
210
  };
143
211
 
144
212
  const payload = {
@@ -182,8 +250,18 @@ export async function initCommand(positional, flags) {
182
250
  bytes: tsconfigBase.bytes,
183
251
  reason: tsconfigBase.reason,
184
252
  },
253
+ // ADR-111. The agent files the apply path will write, one per harness that documents
254
+ // an agents directory. `text` is stripped from the emitted payload the way the other
255
+ // plans strip their rendered bytes; the apply path writes from `agentPlan`, which
256
+ // keeps it.
257
+ agents: agentPlan.map(({ text, ...rest }) => rest),
185
258
  adoption,
186
259
  manifest: { file: MANIFEST, action: manifestAction, path: manifestFile },
260
+ // Local state, not a repository file: gitignored, rebuildable from `git ls-files -s`,
261
+ // and not journaled. The plan names it because a file that appears after the run
262
+ // without being in the plan is the same silent write as any other. `build` means
263
+ // "built when --yes is applied"; the apply path replaces this with the result.
264
+ index: { file: SQLITE_FILE, action: 'build' },
187
265
  region: region && {
188
266
  file: region.file,
189
267
  action: region.action,
@@ -245,6 +323,10 @@ export async function initCommand(positional, flags) {
245
323
  );
246
324
  }
247
325
 
326
+ // A dangling cross-reference does not stop the adoption: the region renders, and one
327
+ // unreachable reference is a defect to report, not a reason to withhold the rest.
328
+ for (const p of crossRefs) note(`PROBLEM ${p}`);
329
+
248
330
  // Step 3: block on a real conflict before doing anything else. A duplicate is
249
331
  // reported but never blocks - restating a ruling is a hygiene problem, while
250
332
  // instructing an agent to do the forbidden thing is a correctness problem.
@@ -254,7 +336,7 @@ export async function initCommand(positional, flags) {
254
336
  if (isJson(flags)) {
255
337
  emitJson(payload);
256
338
  } else {
257
- emit(render(payload, manifest, { npm, version }));
339
+ emit(render(payload, manifest, { npm, manager, version }));
258
340
  note('');
259
341
  note(
260
342
  `blocked ${summary.conflicts} conflict${summary.conflicts === 1 ? '' : 's'}; nothing written`,
@@ -275,7 +357,7 @@ export async function initCommand(positional, flags) {
275
357
  payload.ok = false;
276
358
  payload.blockedBy = 'malformed-region';
277
359
  if (isJson(flags)) emitJson(payload);
278
- else emit(render(payload, manifest, { npm, version }));
360
+ else emit(render(payload, manifest, { npm, manager, version }));
279
361
  return fail(
280
362
  `${malformed.length} target(s) have a malformed generated region (${malformed[0].file}: ${malformed[0].reason})`,
281
363
  EXIT.FAIL,
@@ -304,11 +386,18 @@ export async function initCommand(positional, flags) {
304
386
  after: renderIdentityFile(identity),
305
387
  });
306
388
  }
389
+ if (payload.identity.gitignore !== null) {
390
+ diffs.push({
391
+ path: '.gitignore',
392
+ before: gitignoreBefore,
393
+ after: gitignoreWithIdentity(gitignoreBefore ?? ''),
394
+ });
395
+ }
307
396
  if (payload.ci.action === 'create') {
308
397
  diffs.push({
309
398
  path: payload.ci.file,
310
399
  before: await readBefore(join(root, payload.ci.file)),
311
- after: renderWorkflow({ own: payload.ci.own, npm, version }),
400
+ after: renderWorkflow({ own: payload.ci.own, npm, manager, version }),
312
401
  });
313
402
  }
314
403
  if (payload.dependabot.action === 'create') {
@@ -321,13 +410,6 @@ export async function initCommand(positional, flags) {
321
410
  }),
322
411
  });
323
412
  }
324
- if (payload.tsconfigBase.action === 'create') {
325
- diffs.push({
326
- path: payload.tsconfigBase.file,
327
- before: await readBefore(join(root, payload.tsconfigBase.file)),
328
- after: renderTsconfigBase(),
329
- });
330
- }
331
413
  for (const target of projected.targets) {
332
414
  if (target.action === 'unchanged' || target.action === 'malformed') continue;
333
415
  diffs.push({
@@ -343,7 +425,7 @@ export async function initCommand(positional, flags) {
343
425
  if (isJson(flags)) {
344
426
  if (!apply) emitJson(payload);
345
427
  } else {
346
- emit(render(payload, manifest, { npm, version }));
428
+ emit(render(payload, manifest, { npm, manager, version }));
347
429
  // After the report: the report says what, the diff shows it (R50).
348
430
  emitDiffs(diffs, flags);
349
431
  }
@@ -394,7 +476,6 @@ export async function initCommand(positional, flags) {
394
476
 
395
477
  // Every repo-file write below is journaled, so `undo` can restore this run
396
478
  // (R49). Pointer symlinks carry no bytes and are not journaled.
397
- const journal = [];
398
479
  await writeManifest(root, manifest, journal);
399
480
  note(`wrote ${MANIFEST_FILE} (${manifestAction})`);
400
481
 
@@ -410,7 +491,14 @@ export async function initCommand(positional, flags) {
410
491
  // a change.
411
492
  if (payload.identity.action === 'created') {
412
493
  await journaledWriteFile(root, journal, join(root, IDENTITY_FILE), renderIdentityFile(identity));
413
- await ignoreIdentity(root);
494
+ // Both .gitignore edits are journaled, and the journal line lands first: the two
495
+ // entries fold to one (first before, last after), so undo restores the pre-run
496
+ // bytes — or removes the file — in one step. If the journal line landed after,
497
+ // unrecorded, the file would no longer match the recorded after-bytes and undo
498
+ // would refuse (review 0003 Q30). The `ignoreJournal` inside `commitJournal`
499
+ // below is then a no-op.
500
+ await ignoreJournal(root, journal);
501
+ await ignoreIdentity(root, journal);
414
502
  note(`wrote ${IDENTITY_FILE} (${identity.from === 'last-commit' ? 'from the last commit in this repository' : 'from the identity configured on this machine'}); gitignored, because it is yours and not the repository's`);
415
503
  } else if (payload.identity.action === 'unchanged') {
416
504
  note(`kept ${IDENTITY_FILE} (${payload.identity.reason})`);
@@ -418,7 +506,7 @@ export async function initCommand(positional, flags) {
418
506
  note(`skipped ${IDENTITY_FILE} ${payload.identity.reason}`);
419
507
  }
420
508
  if (!payload.identity.include) {
421
- await includeOwnIdentity(root);
509
+ await includeOwnIdentity(root, journal);
422
510
  note(`set ${INCLUDE_KEY} = ../${IDENTITY_FILE} this repository's own identity file`);
423
511
  }
424
512
 
@@ -433,7 +521,7 @@ export async function initCommand(positional, flags) {
433
521
  // guaranteed to be misled.
434
522
  if (payload.ci.action === 'create') {
435
523
  await mkdir(dirname(join(root, payload.ci.file)), { recursive: true });
436
- await journaledWriteFile(root, journal, join(root, payload.ci.file), renderWorkflow({ own: payload.ci.own, npm, version }));
524
+ await journaledWriteFile(root, journal, join(root, payload.ci.file), renderWorkflow({ own: payload.ci.own, npm, manager, version }));
437
525
  note(`wrote ${payload.ci.file} (create; yours to edit from here, simt-dev will not rewrite it)`);
438
526
  }
439
527
  if (payload.dependabot.action === 'create') {
@@ -455,13 +543,6 @@ export async function initCommand(positional, flags) {
455
543
  `wrote ${payload.dependabot.file} (create; watches ${payload.dependabot.ecosystems.join(', ') || 'github actions'} — yours to edit, simt-dev will not rewrite it)`,
456
544
  );
457
545
  }
458
- if (payload.tsconfigBase.action === 'create') {
459
- await journaledWriteFile(root, journal, join(root, payload.tsconfigBase.file), renderTsconfigBase());
460
- note(
461
- `wrote ${payload.tsconfigBase.file} (create; ${TSCONFIG_BASE_FILE} is JSON, so it cannot say this itself — add "extends": "./${TSCONFIG_BASE_FILE}" to your tsconfig.json or nothing reads it)`,
462
- );
463
- }
464
-
465
546
  // Every planned target is written, not just the AGENTS.md region. An adoption that
466
547
  // scaffolds the manifest and one region but leaves the `.mdc` rules for a later
467
548
  // `sync` produces a repo whose `check` fails on files the user was never told
@@ -507,12 +588,34 @@ export async function initCommand(positional, flags) {
507
588
  for (const p of pointerBlocked) {
508
589
  note(`BLOCKED ${p.file}: ${p.detail} (${p.bytes} bytes); left as it is`);
509
590
  }
591
+ // ADR-111. The agent files, written with everything else and journaled the same way,
592
+ // so `undo` restores them with the rest of the adoption. Written from `agentPlan`,
593
+ // which keeps the rendered bytes the display payload strips.
594
+ const agentWritten = await writeAgentPlan(root, journal, agentPlan);
595
+ for (const f of agentWritten) note(`wrote ${f} (agent)`);
510
596
  await commitJournal(root, 'init', journal);
511
597
  // The tree changed, so the last `check` no longer describes it: the line
512
598
  // goes back to "no cache" rather than reporting the tree as it was (R48).
513
599
  await invalidateCheckCache(root);
514
600
  payload.written = true;
515
601
 
602
+ // The index is a cache of git blobs (ADR-089): gitignored, rebuildable, and not
603
+ // journaled. Building it here means the first `simt map` or `simt brief` in a fresh
604
+ // adoption answers from real files instead of walking the tree on the reader's first
605
+ // prompt. A failure is a note, not a failed adoption: the next read rebuilds what
606
+ // this one could not.
607
+ try {
608
+ const built = await reindex(root);
609
+ payload.index = { file: SQLITE_FILE, action: 'built', files: built.files, symbols: built.symbols, ms: built.ms };
610
+ note(
611
+ `index ${built.files} files, ${built.symbols} symbols in ${built.ms} ms; gitignored cache, ` +
612
+ `refreshed on read by ${CLI} map, brief and slice`,
613
+ );
614
+ } catch (error) {
615
+ payload.index = { file: SQLITE_FILE, action: 'failed', reason: error.message };
616
+ note(`index not built (${error.message}); ${CLI} index rebuilds it`);
617
+ }
618
+
516
619
  if (isJson(flags)) emitJson(payload);
517
620
 
518
621
  note('');
@@ -617,7 +720,7 @@ async function git(cwd, args) {
617
720
  }
618
721
 
619
722
  /** Every line is a decision with a reason. Nothing is silent. */
620
- function render(payload, manifest, { npm = true, version } = {}) {
723
+ function render(payload, manifest, { npm = true, manager = null, version } = {}) {
621
724
  const lines = [
622
725
  `repo ${payload.root}`,
623
726
  `stack ${payload.stack.length > 0 ? payload.stack.join(', ') : '(undetected)'}`,
@@ -649,6 +752,18 @@ function render(payload, manifest, { npm = true, version } = {}) {
649
752
  ? id.reason
650
753
  : `${id.name} <${id.email}> from ${id.from === 'last-commit' ? "this repository's last commit" : 'the identity configured on this machine'}`;
651
754
  lines.push(` ${verb.padEnd(9)} ${id.file}${detail ? `\n ${detail}` : ''}`);
755
+ // The two side writes of the same action, each with its own line: a dry run that
756
+ // says `created git.config` and stops is a plan missing two of its three writes
757
+ // (review 0003 Q30). The include line mirrors the apply note byte for byte, so
758
+ // preview and run cannot tell the reader two different stories.
759
+ if (id.gitignore !== null) {
760
+ lines.push(
761
+ ` ${id.gitignore.padEnd(9)} .gitignore ${IDENTITY_FILE} is per-developer, and committing it publishes one identity to everyone`,
762
+ );
763
+ }
764
+ if (id.include === false) {
765
+ lines.push(` set ${INCLUDE_KEY} = ../${IDENTITY_FILE} this repository's own identity file`);
766
+ }
652
767
  }
653
768
  // Every gate the stack suggested and this repo cannot run is named, with the reason.
654
769
  // A silently shorter gate list reads as "this repo needs no formatting check", when
@@ -687,7 +802,9 @@ function render(payload, manifest, { npm = true, version } = {}) {
687
802
  ? 'hand-written content outside the markers is never modified'
688
803
  : target.kind === 'baseline'
689
804
  ? 'a region placed first, so your own settings below it still win (ADR-025)'
690
- : 'generated file: frontmatter kept, body replaced (ADR-016)';
805
+ : target.kind === 'lock'
806
+ ? 'the two digests a frozen check compares (ADR-086)'
807
+ : 'generated file: frontmatter kept, body replaced (ADR-016)';
691
808
  lines.push(
692
809
  ` ${target.action.padEnd(9)} ${target.file} ${target.previousBytes} -> ${target.bytes} bytes ${caption}`,
693
810
  );
@@ -705,17 +822,30 @@ function render(payload, manifest, { npm = true, version } = {}) {
705
822
  lines.push(` remedy: move it aside, or delete it and rerun init`);
706
823
  }
707
824
 
825
+ // ADR-111. The agent files, one per harness that documents an agents directory.
826
+ // Printed as their own kind: the file is owned whole by the tool, so the action is
827
+ // created, unchanged or updated and there is no byte delta to show.
828
+ for (const a of payload.agents ?? []) {
829
+ lines.push(` ${a.action.padEnd(9)} ${a.path} agent (ADR-111)`);
830
+ }
831
+
708
832
  // The CI gate is listed as its own kind because it is the one plan entry whose
709
833
  // absence is a decision rather than a default. Skipped states its reason inline so
710
834
  // a reader can tell "this repo does not use GitHub Actions" from "this repo was
711
835
  // not offered the gate".
712
836
  if (payload.ci.action === 'create') {
713
- lines.push(` ${'create'.padEnd(9)} ${payload.ci.file} ${renderWorkflow({ own: payload.ci.own, npm, version }).length} bytes`);
837
+ lines.push(` ${'create'.padEnd(9)} ${payload.ci.file} ${renderWorkflow({ own: payload.ci.own, npm, manager, version }).length} bytes`);
714
838
  lines.push(
715
839
  payload.ci.own
716
840
  ? ` this repository is simt-dev's own; ${CLI} ci --yes refreshes this copy when the gate changes`
717
841
  : ` runs ${CLI} check where a pull request is merged; yours to edit after this`,
718
842
  );
843
+ // Q31: the workflow installs simt-dev as a devDependency, so the plan states
844
+ // the install the reader must run — `<pm> add -D`, never a bare npm guess.
845
+ if (!payload.ci.own && manager) {
846
+ const addDev = { npm: `npm i -D ${PACKAGE_NAME}`, pnpm: `pnpm add -D ${PACKAGE_NAME}`, yarn: `yarn add -D ${PACKAGE_NAME}`, bun: `bun add -d ${PACKAGE_NAME}` }[manager];
847
+ if (addDev) lines.push(` ${'needs'.padEnd(9)} ${addDev} (CI installs the pinned devDependency before it can run ${CLI} check)`);
848
+ }
719
849
  } else if (payload.ci.action === 'refresh') {
720
850
  // init does not write it. `simt ci` owns the template's own copy, and a
721
851
  // second writer would be a second answer to who owns it.
@@ -746,19 +876,21 @@ function render(payload, manifest, { npm = true, version } = {}) {
746
876
  lines.push(` create ${WORKFLOWS_DIR}/ first if this repo is on GitHub Actions, then rerun init`);
747
877
  }
748
878
 
749
- // Two states rather than three: there is no refresh, because `init` writes this file once
750
- // and there is no repository in this estate that owns the template. A third state here
751
- // would be a mechanism for keeping a copy current that nobody has asked for.
879
+ // One state, and it is the point: init no longer creates the file (review 0003 Q28),
880
+ // so the plan line says what it will not write and why, instead of a floor appearing
881
+ // after the run without being in the plan. An existing base file is repo-owned and
882
+ // left exactly as it is, which needs no line because nothing changes.
752
883
  const tb = payload.tsconfigBase;
753
- if (tb && tb.action === 'create') {
754
- lines.push(` ${'create'.padEnd(9)} ${tb.file} ${renderTsconfigBase().length} bytes`);
755
- lines.push(
756
- ` the strictness flags these repositories already agree on; add "extends": "./${tb.file}" to your tsconfig.json to use it`,
757
- );
758
- } else if (tb && tb.action === 'skip') {
884
+ if (tb && tb.action === 'skip') {
759
885
  lines.push(` ${'skip'.padEnd(9)} ${tb.file} ${tb.reason}`);
760
886
  }
761
887
 
888
+ // The index is local state, not a repository file: gitignored and rebuildable, so the
889
+ // plan line describes what apply does rather than a file written into the tree.
890
+ lines.push(
891
+ ` ${'build'.padEnd(9)} ${payload.index.file} gitignored cache of the git index; built on apply, refreshed on read by ${CLI} map, brief and slice (ADR-089)`,
892
+ );
893
+
762
894
  const { findings, summary } = payload;
763
895
  if (summary.conflicts > 0) {
764
896
  lines.push('', `CONFLICTS ${summary.conflicts} - blocking without --accept-conflicts`);