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
@@ -47,6 +47,7 @@ ${CLI} rules list the rule catalogue (--root marks what applies to a
47
47
  ${CLI} rules show <id> one rule body
48
48
  ${CLI} rules why <id|ADR-NNN> the decision that settled it, and whether it is still in force
49
49
  (--all prints every ruling, not the first two)
50
+ ${CLI} rules decisions every known decision, shipped and repo, and which layer each came from
50
51
  ${CLI} hooks install [--yes] install the shared dispatcher; report what it would run
51
52
  (dry-run unless --yes; refuses to take over routing git
52
53
  already uses, and --yes is that consent)
@@ -58,11 +59,16 @@ ${CLI} agent-hook <event> the entry point a harness calls: pre-tool-use dec
58
59
  (reads one JSON payload on stdin and answers in that
59
60
  harness's dialect; exit 2 blocks; needs --harness)
60
61
  ${CLI} agent-hooks install write this repo's policies into the harnesses that
61
- read a hooks file
62
+ read a hooks file (--harness <id> writes one, not all)
62
63
  (dry-run unless --yes; merges into the file, keeps every
63
64
  key it does not own; Codex needs the hook trusted)
64
65
  ${CLI} agent-hooks status what each harness will run, and what is not trusted yet
65
- ${CLI} plugin <claude|gemini|cursor> generate a harness package from the same tables
66
+ ${CLI} agents install write the Simt-Agent into the harnesses that read an
67
+ agents file (--harness <id> writes one, not all; dry-run
68
+ unless --yes; markdown with a frontmatter the harness
69
+ documents, TOML for Codex)
70
+ ${CLI} agents status what each harness carries, and what is not installed yet
71
+ ${CLI} plugin <claude|gemini|cursor|opencode> generate a harness package from the same tables
66
72
  (dry-run unless --yes; writes a self-contained runtime, the
67
73
  hooks and MCP registration, and for cursor the kernel rules;
68
74
  the skill registry is workspace data and is not shipped)
@@ -77,17 +83,24 @@ ${CLI} plan status count tracker rows
77
83
  ${CLI} plan next [<id>] one ready slice's section, or the one named; ~1 KB
78
84
  ${CLI} plan done <id> [<sha>] mark one row; dry-run unless --yes
79
85
  ${CLI} slice start <id> give a slice its own worktree and branch, preload the
80
- brief, and mark the row In progress (dry-run unless
81
- --yes; the worktree is ${STATE_DIR}/worktrees/<id> on
82
- branch slice/<id>, the brief ${STATE_DIR}/brief.md in it)
86
+ brief, and mark the row started (dry-run unless
87
+ --yes; the worktree is ${STATE_DIR}/local/worktrees/<id>
88
+ on branch slice/<id>, the brief ${STATE_DIR}/local/briefs/<id>.md
89
+ beside it; --session <id> leases the slice)
83
90
  ${CLI} slice finish <id> run the pre-commit gates, commit what is staged with a
84
91
  Conventional subject from the row's title, then record
85
92
  the SHA and the --test command on the row (dry-run
86
- unless --yes; --type picks the type, default feat)
93
+ unless --yes; --type picks the type, default feat;
94
+ refuses unless HEAD is the slice branch; gates the
95
+ ledger already proves for this tree are not re-run)
96
+ ${CLI} slice abandon <id> remove the worktree, delete the branch, and return
97
+ the row to ready (dry-run unless --yes; refuses from
98
+ inside the worktree)
87
99
  ${CLI} lease [<paths>...] claim files for one session and a TTL, or list the
88
100
  claims (dry-run unless --yes; the session is --session
89
101
  <id> or ${SESSION_ENV}; a pre-edit hook refuses an edit
90
- to a path another session holds — ADR-101)
102
+ to a path another session holds — ADR-101;
103
+ release and renew end and extend your own claims)
91
104
  ${CLI} graph lint check the skill registry against the skills on disk
92
105
  (byte counts, dangling edges, unresolvable variants)
93
106
  ${CLI} ci simt-dev's own .github files: create or refresh them
@@ -100,7 +113,8 @@ ${CLI} ignore [--yes] render the one .simtignore list into .cursorignore
100
113
  Claude settings file is merged, never replaced)
101
114
  ${CLI} compare org-wide matrix across every sibling repo
102
115
  (read-only; exits non-zero on defects, not on
103
- repos that have not adopted yet)
116
+ repos that have not adopted yet;
117
+ --concurrency <n> caps the rows built at once)
104
118
  ${CLI} index rebuild the local file index from git blob hashes
105
119
  ${CLI} why [path] what governs a file: a decision whose Affects line names
106
120
  it, a rule whose scope matches it, a skill, a gate
@@ -127,8 +141,9 @@ ${CLI} pr-body [--base <ref>] a pull request body: the evidence block for this
127
141
  ${CLI} evidence the runs recorded against the tree on disk
128
142
  ${CLI} evidence check <file> a commit message that claims a run this tree does not
129
143
  have: a warning, or a block when gates.evidence is "block"
130
- ${CLI} lessons list deterministic lesson candidates in .simt/lessons.jsonl
131
- and promoted lessons in docs/lessons/*.md
144
+ ${CLI} lessons list deterministic lesson candidates in .simt/local/lessons.jsonl
145
+ and promoted lessons in docs/lessons/*.md (reads nothing else)
146
+ ${CLI} lessons capture scan the ledger and git history for new candidates (the write half of list)
132
147
  ${CLI} lessons review [<id>] promote a lesson candidate to docs/lessons/NNNN-<slug>.md
133
148
  (dry-run unless --yes; writes Appendix A frontmatter)
134
149
  ${CLI} explain <CODE> what a check finding means: the decision behind it, how
@@ -147,7 +162,7 @@ ${CLI} local doctor probe configured local model endpoints (chat and
147
162
  unset; refuses non-loopback URLs without --allow-remote)
148
163
  ${CLI} distill <skill|doc> a budgeted proposal summary of a skill body or doc,
149
164
  every claim citing the source line range it rests on
150
- (writes .simt/distilled/<digest>.md; a proposal a
165
+ (writes .simt/local/distilled/<digest>.md; a proposal a
151
166
  human merges - never projected or committed;
152
167
  deterministic outline by default; --local proposes
153
168
  from the configured chat model instead; the cache is
@@ -157,7 +172,7 @@ ${CLI} review [--local] an ADR-008 verdict skeleton for the staged diff,
157
172
  (deterministic without --local: the brief and its
158
173
  governing ids only; --local proposes from the
159
174
  configured chat model and writes
160
- .simt/reviews/<digest>.md — a proposal a human
175
+ .simt/local/reviews/<digest>.md — a proposal a human
161
176
  reads, verified against the verdict vocabulary and
162
177
  the brief's ids; advisory always, never a gate)
163
178
  ${CLI} shared plan what a workspace's repeated rules cost to carry
@@ -197,10 +212,10 @@ ${CLI} bench probes [--yes] 12 synthetic rule-compliance probes with determin
197
212
  (dry-run unless --yes; grades git state, transcript and ledger,
198
213
  and reports compliance per arm)
199
214
  ${CLI} bench variants [--yes] evaluate simt-variants rule slots on the canary suite and lock
200
- winning variants in simt-dev.lock.json (dry-run unless --yes;
215
+ winning variants in simt.lock.json (dry-run unless --yes;
201
216
  re-opens when model or harness changes)
202
217
  ${CLI} bench critic precision and recall of the local critic over a labelled
203
- set of historical diffs (.simt/critic-labels.jsonl;
218
+ set of historical diffs (.simt/local/critic-labels.jsonl;
204
219
  overrides --labels; labels are human-written verdicts
205
220
  under ADR-008; the critic is advisory until precision
206
221
  reaches 0.8 on 30 labelled diffs)
@@ -240,6 +255,12 @@ const FLAGS = `--json structured output (compact; one document,
240
255
  --workspace <path> where the skill sources live; "off" declines it
241
256
  (default: SIMT_DEV_WORKSPACE, then the user config
242
257
  file, then the sibling heuristic if it verifies)
258
+ --registry <path> which skill registry to read (skills commands and
259
+ graph lint; default: SIMT_REGISTRY, then the checkout's
260
+ own graph/skills.graph.json, then the one member of the
261
+ workspace that holds one)
262
+ --concurrency <n> compare: rows built at once (default 4, or
263
+ SIMT_CONCURRENCY; order-preserving whatever it is)
243
264
  --dry-run report what would change, change nothing
244
265
  --yes assume yes; required in automation, never prompts
245
266
  --check report drift and write nothing (sync, ci)
@@ -12,13 +12,11 @@
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 { execFile } from 'node:child_process';
16
- import { chmod, mkdir, readFile, realpath } from 'node:fs/promises';
17
- import { commitJournal, journaledRemoveFile, journaledWriteFile } from '../journal.mjs';
15
+ import { mkdir, readFile, realpath } from 'node:fs/promises';
16
+ import { commitJournal, journaledGitConfig, journaledRemoveFile, journaledWriteFile } from '../journal.mjs';
18
17
  import { diffField, emitDiffs, readBefore } from '../diff.mjs';
19
18
  import { invalidateCheckCache } from '../status.mjs';
20
19
  import { isAbsolute, join, relative } from 'node:path';
21
- import { promisify } from 'node:util';
22
20
 
23
21
  import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
24
22
  import { MANIFEST, readManifest } from '../projection/manifest.mjs';
@@ -31,8 +29,6 @@ import { CLI } from '../name.mjs';
31
29
  import { MANIFEST_FILE } from '../name.mjs';
32
30
  import { packageVersion, hasNpm } from '../package-version.mjs';
33
31
 
34
- const exec = promisify(execFile);
35
-
36
32
  export async function hooksCommand(sub, args, flags) {
37
33
  const root = flags.root ?? args[0] ?? process.cwd();
38
34
  if (sub === 'install') return installHooks(root, flags);
@@ -54,7 +50,12 @@ async function runHook(args, flags) {
54
50
  return fail(`${root} has no ${MANIFEST_FILE}; the hook cannot know its gate list`, EXIT.CONFIG, `run: ${CLI} init --root ${root}`);
55
51
  }
56
52
 
57
- const result = await runGates(event, { root, manifest });
53
+ // git feeds pre-push its ref list on stdin, and the docs-only skip (ADR-113)
54
+ // needs it. A manual run from a terminal has a TTY and reads empty, which the
55
+ // dispatcher treats as "no push to classify" and answers with the full suite.
56
+ const pushed = event === 'pre-push' ? await readStdin() : undefined;
57
+
58
+ const result = await runGates(event, { root, manifest, pushed });
58
59
  if (result.error) return fail(result.error, EXIT.USAGE);
59
60
 
60
61
  if (isJson(flags)) {
@@ -67,6 +68,29 @@ async function runHook(args, flags) {
67
68
  return result.failed ? EXIT.FAIL : EXIT.OK;
68
69
  }
69
70
 
71
+ /**
72
+ * Read all of stdin, or '' when there is none to read.
73
+ *
74
+ * The pre-push shim execs this command, so git's ref list arrives on the
75
+ * dispatcher's stdin. A TTY (a manual run from a terminal) has no ref list and
76
+ * reads empty - and empty is the fail-closed answer: the dispatcher cannot
77
+ * classify a push it was never told about, so the suite runs.
78
+ *
79
+ * @returns {Promise<string>}
80
+ */
81
+ function readStdin() {
82
+ return new Promise((resolve) => {
83
+ if (process.stdin.isTTY) {
84
+ resolve('');
85
+ return;
86
+ }
87
+ const chunks = [];
88
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
89
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
90
+ process.stdin.resume();
91
+ });
92
+ }
93
+
70
94
  /**
71
95
  * The path to write into a chained shim: relative to the repository's top level
72
96
  * wherever the hook lives inside it.
@@ -123,6 +147,22 @@ export async function planHooksInstall(root, { manifest, exists }) {
123
147
  // install where there is no node_modules to pin in.
124
148
  const version = packageVersion();
125
149
  const npm = await hasNpm(root);
150
+ // Q31: installed shims name the repo's own installer in their remedy.
151
+ // Lockfile first, `packageManager` next, npm default — the one resolver.
152
+ let manager = null;
153
+ if (npm) {
154
+ const { resolveNodeRunner } = await import('../ecosystem/node-runner.mjs');
155
+ let packageManager = null;
156
+ try {
157
+ const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
158
+ if (typeof pkg.packageManager === 'string') packageManager = pkg.packageManager;
159
+ } catch {
160
+ // Unreadable here leaves the npm default; the manifest path reports it.
161
+ }
162
+ const { fingerprint } = await import('../resolve/fingerprint.mjs');
163
+ const detected = await fingerprint(root);
164
+ manager = resolveNodeRunner({ facts: detected.facts, packageManager }).id;
165
+ }
126
166
 
127
167
  // Only install a shim for an event that declares at least one gate. A shim with
128
168
  // an empty gate list enforces nothing, yet merely existing it REPLACES whatever
@@ -224,7 +264,7 @@ export async function planHooksInstall(root, { manifest, exists }) {
224
264
  const evidenceDeclared = manifest?.gates?.evidence === 'warn' || manifest?.gates?.evidence === 'block';
225
265
  const evidenceFile = fileFor('commit-msg');
226
266
  const evidenceExisting = await readIfExists(join(root, evidenceFile));
227
- const evidenceSource = evidenceDeclared ? renderEvidenceShim({ npm, version }) : null;
267
+ const evidenceSource = evidenceDeclared ? renderEvidenceShim({ npm, manager, version }) : null;
228
268
  const evidenceOurs = evidenceExisting !== null && evidenceExisting.includes(SHIM_MARKER);
229
269
  if (!evidenceDeclared && evidenceOurs) obsolete.push({ event: 'commit-msg' });
230
270
  // A foreign commit-msg would otherwise be chained onto the same path. The chain
@@ -243,6 +283,7 @@ export async function planHooksInstall(root, { manifest, exists }) {
243
283
  current,
244
284
  wants,
245
285
  npm,
286
+ manager,
246
287
  version,
247
288
  wanted,
248
289
  chained,
@@ -271,23 +312,25 @@ export async function planHooksInstall(root, { manifest, exists }) {
271
312
  * @returns {{wrote: string[]}} the shim paths written, removed, or refreshed
272
313
  */
273
314
  export async function applyHooksInstall(root, planned, journal) {
274
- const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, version, routed, wants, current } =
315
+ const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current } =
275
316
  planned;
276
- // Shim and evidence bytes are journaled so `undo` restores them (R49). The
277
- // `core.hooksPath` config below is deliberately not journaled: it is git
278
- // config, not a repo file, and `undo` restores files.
317
+ // Shim and evidence bytes are journaled so `undo` restores them (R49), with
318
+ // the executable bit they are written with: a shim the run chmodded is one
319
+ // change, and `undo` must restore the mode it found, not just the bytes.
320
+ // The `core.hooksPath` routing below is journaled as a `git-config` entry
321
+ // for the same reason — unwinding the shims while the config still points
322
+ // at their directory silently stops the repository's own hooks (review 0003
323
+ // Q22).
279
324
  const wrote = [];
280
325
  await mkdir(join(root, HOOKS_DIR), { recursive: true });
281
326
  for (const event of wanted) {
282
327
  const path = join(root, fileFor(event));
283
- await journaledWriteFile(root, journal, path, renderShim(event, { npm, version }));
284
- await chmod(path, 0o755);
328
+ await journaledWriteFile(root, journal, path, renderShim(event, { npm, manager, version }), { mode: 0o755 });
285
329
  wrote.push(fileFor(event));
286
330
  }
287
331
  for (const c of chained) {
288
332
  const path = join(root, fileFor(c.event));
289
- await journaledWriteFile(root, journal, path, renderChainedShim(c.event, c.target));
290
- await chmod(path, 0o755);
333
+ await journaledWriteFile(root, journal, path, renderChainedShim(c.event, c.target), { mode: 0o755 });
291
334
  wrote.push(fileFor(c.event));
292
335
  }
293
336
  for (const h of obsolete) {
@@ -297,8 +340,7 @@ export async function applyHooksInstall(root, planned, journal) {
297
340
  // After the chain loop, so a chained commit-msg cannot replace the claim check.
298
341
  if (evidenceSource) {
299
342
  const path = join(root, evidenceFile);
300
- await journaledWriteFile(root, journal, path, evidenceSource);
301
- await chmod(path, 0o755);
343
+ await journaledWriteFile(root, journal, path, evidenceSource, { mode: 0o755 });
302
344
  wrote.push(evidenceFile);
303
345
  }
304
346
  // `core.hooksPath` tells git to read ONE directory for EVERY event, so leaving it
@@ -306,9 +348,9 @@ export async function applyHooksInstall(root, planned, journal) {
306
348
  // `.git/hooks` that predate this tool - from running, silently. Unsetting it is the
307
349
  // only way back, and this is the only place that knows the last shim just left.
308
350
  if (routed) {
309
- await exec('git', ['config', HOOKS_PATH_KEY, wants], { cwd: root });
351
+ await journaledGitConfig(root, journal, HOOKS_PATH_KEY, wants);
310
352
  } else if (current === wants) {
311
- await exec('git', ['config', '--unset', HOOKS_PATH_KEY], { cwd: root });
353
+ await journaledGitConfig(root, journal, HOOKS_PATH_KEY, null);
312
354
  }
313
355
  return { wrote };
314
356
  }
@@ -320,11 +362,11 @@ export async function applyHooksInstall(root, planned, journal) {
320
362
  * `doctor --fix` reuses it rather than deriving a second answer.
321
363
  */
322
364
  export async function plannedDiffs(root, planned) {
323
- const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, version } = planned;
365
+ const { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version } = planned;
324
366
  const pairs = [];
325
367
  for (const event of wanted) {
326
368
  const path = fileFor(event);
327
- pairs.push({ path, before: await readBefore(join(root, path)), after: renderShim(event, { npm, version }) });
369
+ pairs.push({ path, before: await readBefore(join(root, path)), after: renderShim(event, { npm, manager, version }) });
328
370
  }
329
371
  for (const c of chained) {
330
372
  const path = fileFor(c.event);
@@ -355,6 +397,7 @@ async function installHooks(root, flags) {
355
397
  current,
356
398
  wants,
357
399
  npm,
400
+ manager,
358
401
  version,
359
402
  wanted,
360
403
  chained,
@@ -403,7 +446,7 @@ async function installHooks(root, flags) {
403
446
  const plan = [];
404
447
  for (const event of wanted) {
405
448
  const path = join(root, fileFor(event));
406
- const source = renderShim(event, { npm, version });
449
+ const source = renderShim(event, { npm, manager, version });
407
450
  const existing = await readIfExists(path);
408
451
  if (existing === source) {
409
452
  plan.push({ file: fileFor(event), action: 'unchanged' });
@@ -487,7 +530,7 @@ async function installHooks(root, flags) {
487
530
  // true in preview and in apply alike (R50).
488
531
  const diffs =
489
532
  flags.diff === true
490
- ? await plannedDiffs(root, { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, version })
533
+ ? await plannedDiffs(root, { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version })
491
534
  : [];
492
535
 
493
536
  if (!mode.apply) {
@@ -535,7 +578,7 @@ async function installHooks(root, flags) {
535
578
  const journal = [];
536
579
  await applyHooksInstall(
537
580
  root,
538
- { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, version, routed, wants, current },
581
+ { wanted, chained, obsolete, evidenceSource, evidenceFile, npm, manager, version, routed, wants, current },
539
582
  journal,
540
583
  );
541
584
  await commitJournal(root, 'hooks install', journal);
@@ -191,8 +191,10 @@ function textExt(file) {
191
191
 
192
192
  async function tracked(root) {
193
193
  try {
194
- const { stdout } = await exec('git', ['ls-files'], { cwd: root, maxBuffer: 16 << 20 });
195
- return { files: stdout.split('\n').filter(Boolean) };
194
+ // `-z`: without it git C-quotes a non-ASCII path and the reader's real
195
+ // filename never matches it (review 0003 Q39).
196
+ const { stdout } = await exec('git', ['ls-files', '-z'], { cwd: root, maxBuffer: 16 << 20 });
197
+ return { files: stdout.split('\0').filter(Boolean) };
196
198
  } catch (error) {
197
199
  return {
198
200
  error: fail(
@@ -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 {
@@ -63,7 +64,7 @@ import { testsCommand } from './tests.mjs';
63
64
  import { whyCommand } from './why.mjs';
64
65
  import { gateCommand } from './gate.mjs';
65
66
  import { impactCommand } from './impact.mjs';
66
- import { lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
67
+ import { lessonsCaptureCommand, lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
67
68
  import { decideCommand } from './decide.mjs';
68
69
  import { explainCommand } from './explain.mjs';
69
70
  import { undoCommand } from './undo.mjs';
@@ -95,6 +96,10 @@ const GROUPS = new Map([
95
96
  // error before anything is created — the one default here that cannot mutate.
96
97
  ['slice', 'start'],
97
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'],
98
103
  ['harness', 'detect'],
99
104
  ['bench', 'mine'],
100
105
  // ADR-084. `shared` is the one group whose default writes nothing, and it is `plan` for
@@ -137,6 +142,7 @@ export const IMPLEMENTED = {
137
142
  'skills eval': skillsEval,
138
143
  'skills install': installCommand,
139
144
  'skills compile': compileCommand,
145
+ 'rules decisions': rulesDecisions,
140
146
  'rules list': rulesList,
141
147
  'rules show': rulesShow,
142
148
  'rules why': rulesWhy,
@@ -146,10 +152,12 @@ export const IMPLEMENTED = {
146
152
  'plan next': (args, flags) => planCommand('next', args, flags),
147
153
  'plan done': (args, flags) => planCommand('done', args, flags),
148
154
  // Plan 0003 R53. A group: `start` isolates the work on a branch with its brief,
149
- // `finish` runs the gates, commits and records the evidence. Both take the slice
150
- // id as a positional.
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.
151
158
  'slice start': (args, flags) => sliceCommand('start', args, flags),
152
159
  'slice finish': (args, flags) => sliceCommand('finish', args, flags),
160
+ 'slice abandon': (args, flags) => sliceCommand('abandon', args, flags),
153
161
  'shared plan': sharedPlanCommand,
154
162
  'shared install': sharedInstallCommand,
155
163
  evidence: evidenceCommand,
@@ -159,6 +167,7 @@ export const IMPLEMENTED = {
159
167
  why: (args, flags) => whyCommand(args, flags, COMMANDS),
160
168
  gate: gateCommand,
161
169
  impact: (args, flags) => impactCommand(args, flags, COMMANDS),
170
+ 'lessons capture': lessonsCaptureCommand,
162
171
  'lessons list': lessonsListCommand,
163
172
  'lessons review': lessonsReviewCommand,
164
173
  decide: decideCommand,
@@ -192,6 +201,10 @@ export const IMPLEMENTED = {
192
201
  'agent-hook': agentHookCommand,
193
202
  'agent-hooks install': (args, flags) => agentHooksCommand('install', args, flags),
194
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),
195
208
  compare: compareCommand,
196
209
  tokens: tokensCommand,
197
210
  'harness detect': harnessCommand,