simt-dev 0.7.0 → 0.8.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 (235) hide show
  1. package/CHANGELOG.md +191 -870
  2. package/README.md +188 -213
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
  5. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  7. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  8. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  9. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  10. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  11. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  12. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  13. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  14. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  15. 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
  16. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  17. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  18. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  19. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  20. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  21. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  22. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  23. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  24. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  25. package/ecosystems/bun.json +6 -0
  26. package/ecosystems/cargo.json +25 -0
  27. package/ecosystems/composer.json +6 -0
  28. package/ecosystems/dart.json +24 -0
  29. package/ecosystems/deno.json +21 -0
  30. package/ecosystems/devcontainers.json +6 -0
  31. package/ecosystems/docker-compose.json +6 -0
  32. package/ecosystems/docker.json +6 -0
  33. package/ecosystems/dotnet.json +25 -0
  34. package/ecosystems/elixir.json +23 -0
  35. package/ecosystems/gitsubmodule.json +6 -0
  36. package/ecosystems/go.json +25 -0
  37. package/ecosystems/gradle.json +13 -0
  38. package/ecosystems/maven.json +23 -0
  39. package/ecosystems/node.json +25 -0
  40. package/ecosystems/python.json +20 -0
  41. package/ecosystems/ruby.json +23 -0
  42. package/ecosystems/swift.json +25 -0
  43. package/ecosystems/terraform.json +6 -0
  44. package/ecosystems/uv.json +6 -0
  45. package/harnesses/antigravity.json +162 -0
  46. package/harnesses/claude.json +206 -0
  47. package/harnesses/codex.json +136 -0
  48. package/harnesses/copilot.json +88 -0
  49. package/harnesses/cursor.json +140 -0
  50. package/harnesses/deepseek.json +99 -0
  51. package/harnesses/gemini.json +122 -0
  52. package/harnesses/opencode.json +173 -0
  53. package/package.json +12 -2
  54. package/rules/core.md +7 -0
  55. package/rules/mdc/intake-questions.mdc +8 -0
  56. package/rules/mdc/slice-commit.mdc +57 -4
  57. package/src/advice/advice.mjs +3 -2
  58. package/src/agenthooks/prompt.mjs +5 -2
  59. package/src/agenthooks/render.mjs +86 -180
  60. package/src/agenthooks/shim.mjs +1 -1
  61. package/src/agents/render.mjs +15 -18
  62. package/src/api.mjs +31 -0
  63. package/src/argv.mjs +48 -3
  64. package/src/bench/ablate.mjs +3 -4
  65. package/src/bench/adapters.json +163 -105
  66. package/src/bench/adapters.mjs +466 -0
  67. package/src/bench/arms.mjs +333 -0
  68. package/src/bench/grade.mjs +33 -73
  69. package/src/bench/graders.mjs +42 -0
  70. package/src/bench/mine.mjs +121 -475
  71. package/src/bench/probe-ledger.mjs +149 -0
  72. package/src/bench/probes.catalog.mjs +918 -0
  73. package/src/bench/probes.mjs +79 -1064
  74. package/src/bench/render.mjs +111 -0
  75. package/src/bench/report.mjs +120 -962
  76. package/src/bench/run.mjs +185 -802
  77. package/src/bench/variants.mjs +4 -5
  78. package/src/bench/workspace.mjs +160 -0
  79. package/src/brief/assemble.mjs +48 -15
  80. package/src/ci/dependabot.mjs +25 -55
  81. package/src/ci/workflow.mjs +30 -7
  82. package/src/cli.mjs +45 -0
  83. package/src/commands/agent.mjs +4 -13
  84. package/src/commands/agenthook.mjs +70 -40
  85. package/src/commands/bench.mjs +191 -18
  86. package/src/commands/brief.mjs +5 -4
  87. package/src/commands/ci.mjs +13 -14
  88. package/src/commands/compare.mjs +42 -42
  89. package/src/commands/completions.mjs +2 -6
  90. package/src/commands/decide.mjs +4 -4
  91. package/src/commands/distill.mjs +4 -3
  92. package/src/commands/docs.mjs +88 -0
  93. package/src/commands/doctor.mjs +11 -7
  94. package/src/commands/evidence.mjs +6 -9
  95. package/src/commands/explain.mjs +0 -1
  96. package/src/commands/gate.mjs +26 -25
  97. package/src/commands/graph.mjs +2 -3
  98. package/src/commands/harness.mjs +6 -5
  99. package/src/commands/help.mjs +168 -19
  100. package/src/commands/hooks.mjs +119 -49
  101. package/src/commands/ignore.mjs +13 -8
  102. package/src/commands/impact.mjs +8 -15
  103. package/src/commands/index.mjs +318 -193
  104. package/src/commands/init-scan.mjs +90 -0
  105. package/src/commands/init.mjs +124 -105
  106. package/src/commands/lease.mjs +73 -25
  107. package/src/commands/lessons.mjs +10 -9
  108. package/src/commands/local.mjs +3 -3
  109. package/src/commands/map.mjs +24 -14
  110. package/src/commands/mcp.mjs +2 -18
  111. package/src/commands/pack.mjs +9 -6
  112. package/src/commands/plan.mjs +6 -5
  113. package/src/commands/plugin.mjs +2 -2
  114. package/src/commands/prbody.mjs +3 -3
  115. package/src/commands/repoindex.mjs +12 -11
  116. package/src/commands/review.mjs +2 -2
  117. package/src/commands/rules.mjs +37 -16
  118. package/src/commands/shared.mjs +14 -19
  119. package/src/commands/skills.mjs +33 -32
  120. package/src/commands/skillscompile.mjs +7 -8
  121. package/src/commands/skillsinstall.mjs +7 -6
  122. package/src/commands/slice.mjs +220 -74
  123. package/src/commands/spec.mjs +167 -0
  124. package/src/commands/status.mjs +27 -18
  125. package/src/commands/sync.mjs +27 -25
  126. package/src/commands/tests.mjs +18 -21
  127. package/src/commands/tokens.mjs +21 -26
  128. package/src/commands/undo.mjs +5 -15
  129. package/src/commands/upgrade.mjs +15 -7
  130. package/src/commands/why.mjs +15 -21
  131. package/src/compare/hooksstate.mjs +2 -6
  132. package/src/compare/learn.mjs +3 -1
  133. package/src/compare/row.mjs +0 -2
  134. package/src/context.mjs +64 -0
  135. package/src/decisions/index-doc.mjs +46 -0
  136. package/src/decisions/index.mjs +3 -2
  137. package/src/diff.mjs +1 -1
  138. package/src/distill/distill.mjs +14 -1
  139. package/src/distill/engine.mjs +5 -1
  140. package/src/docs/commands.mjs +65 -0
  141. package/src/ecosystem/records.mjs +219 -0
  142. package/src/explain.mjs +31 -0
  143. package/src/gates/index.mjs +385 -0
  144. package/src/graph/compile.mjs +8 -4
  145. package/src/graph/load.mjs +6 -2
  146. package/src/graph/spec.mjs +10 -8
  147. package/src/harness/records.mjs +143 -0
  148. package/src/hooks/capability.mjs +7 -5
  149. package/src/hooks/dispatcher.mjs +209 -60
  150. package/src/hooks/gates.mjs +10 -43
  151. package/src/hooks/resolve.mjs +4 -14
  152. package/src/hooks/shim.mjs +5 -2
  153. package/src/hooks/where.mjs +2 -7
  154. package/src/identity.mjs +31 -12
  155. package/src/index/coverage.mjs +58 -0
  156. package/src/index/extract.mjs +6 -16
  157. package/src/index/gate.mjs +1 -1
  158. package/src/index/govern.mjs +26 -9
  159. package/src/index/impact.mjs +4 -8
  160. package/src/index/map.mjs +14 -15
  161. package/src/index/open.mjs +173 -0
  162. package/src/index/persist.mjs +224 -57
  163. package/src/index/store.mjs +82 -39
  164. package/src/index/tests.mjs +2 -6
  165. package/src/journal.mjs +94 -20
  166. package/src/kernel/card.mjs +37 -63
  167. package/src/kernel/checks.mjs +4 -10
  168. package/src/lease.mjs +102 -7
  169. package/src/ledger/index.mjs +150 -14
  170. package/src/local/client.mjs +4 -6
  171. package/src/locate.mjs +33 -9
  172. package/src/manifest/schema.mjs +213 -0
  173. package/src/mcp/paginate.mjs +33 -0
  174. package/src/mcp/resources.mjs +7 -10
  175. package/src/mcp/schemas.mjs +512 -0
  176. package/src/mcp/server.mjs +119 -25
  177. package/src/mcp/tools-facts.mjs +326 -0
  178. package/src/mcp/tools-read.mjs +221 -0
  179. package/src/mcp/tools.mjs +4 -967
  180. package/src/measure/corpus.mjs +150 -0
  181. package/src/measure/metrics.mjs +236 -0
  182. package/src/measure/records.mjs +211 -0
  183. package/src/measure/session.mjs +3 -2
  184. package/src/measure/stats.mjs +538 -0
  185. package/src/measure/task-schema.mjs +125 -0
  186. package/src/measure/tasks.mjs +69 -0
  187. package/src/memory/decide.mjs +6 -15
  188. package/src/memory/lesson-text.mjs +61 -0
  189. package/src/memory/lessons.mjs +29 -82
  190. package/src/name.mjs +7 -0
  191. package/src/node-version.mjs +37 -0
  192. package/src/output/index.mjs +209 -10
  193. package/src/packs/manage.mjs +32 -12
  194. package/src/packs.mjs +20 -2
  195. package/src/plan/index.mjs +1 -1
  196. package/src/plugins.mjs +22 -16
  197. package/src/prbody.mjs +7 -11
  198. package/src/projection/loaders.mjs +7 -692
  199. package/src/projection/lock.mjs +11 -1
  200. package/src/projection/manifest.mjs +29 -468
  201. package/src/projection/mdc.mjs +7 -8
  202. package/src/projection/project.mjs +126 -221
  203. package/src/projection/region.mjs +561 -0
  204. package/src/projection/rules.mjs +20 -550
  205. package/src/projection/surfaces.mjs +43 -210
  206. package/src/projection/targets.mjs +5 -5
  207. package/src/repo-root.mjs +31 -0
  208. package/src/resolve/eval.mjs +2 -1
  209. package/src/resolve/fingerprint.mjs +19 -22
  210. package/src/resolve/resolve.mjs +1 -41
  211. package/src/review/critic.mjs +10 -13
  212. package/src/run.mjs +98 -0
  213. package/src/secrets.mjs +14 -22
  214. package/src/state/events.mjs +129 -0
  215. package/src/status-snapshot.mjs +233 -0
  216. package/src/status.mjs +2 -6
  217. package/src/sys/git.mjs +82 -0
  218. package/src/sys/proc.mjs +151 -0
  219. package/src/testing/parsers.mjs +265 -0
  220. package/src/testing/patches.mjs +23 -0
  221. package/src/testing/paths.mjs +49 -0
  222. package/src/testing/runners.mjs +186 -0
  223. package/src/testing/vault.mjs +68 -0
  224. package/src/text/frontmatter.mjs +49 -0
  225. package/src/text/tokens.mjs +52 -0
  226. package/src/toolwords.mjs +3 -0
  227. package/src/util/fs.mjs +20 -0
  228. package/src/util/sort.mjs +12 -0
  229. package/src/workspace.mjs +58 -11
  230. package/src/write/plan.mjs +82 -0
  231. package/templates/plan/tracker.json +1 -0
  232. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  233. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  234. package/docs/decisions/README.md +0 -600
  235. package/src/bench/tasks.mjs +0 -36
@@ -29,6 +29,7 @@ import {
29
29
  } from '../projection/shared.mjs';
30
30
  import { buildManifest, declaredShared, manifestPath, manifestText, readManifest, writeManifest } from '../projection/manifest.mjs';
31
31
  import { desiredRegion } from '../projection/rules.mjs';
32
+ import { plainNumber } from '../util/fs.mjs';
32
33
 
33
34
  /**
34
35
  * Everything `plan` and `install` both need, computed once.
@@ -76,9 +77,9 @@ async function gather(flags) {
76
77
  */
77
78
  function divergentNote(d, total) {
78
79
  if (d.reason === 'coverage') {
79
- return `${d.id} carried by ${plain(d.roots.length)} of ${plain(total)} repositories, so each region keeps its own`;
80
+ return `${d.id} carried by ${plainNumber(d.roots.length)} of ${plainNumber(total)} repositories, so each region keeps its own`;
80
81
  }
81
- return `${d.id} rendered differently in ${plain(d.roots.length)} repositories, so each region keeps its own`;
82
+ return `${d.id} rendered differently in ${plainNumber(d.roots.length)} repositories, so each region keeps its own`;
82
83
  }
83
84
 
84
85
  /** Why a selected rule cannot leave any region, in the words its reason deserves. */
@@ -98,7 +99,7 @@ function excludedNote({ id, why }) {
98
99
  function capNote(target, bytes) {
99
100
  if (target.path === null || target.cap === null || bytes <= target.cap.bytes) return null;
100
101
  return [
101
- `${target.surface} ${plain(bytes)} B exceeds the ${plain(target.cap.bytes)} B it allows for ${target.cap.appliesTo}`,
102
+ `${target.surface} ${plainNumber(bytes)} B exceeds the ${plainNumber(target.cap.bytes)} B it allows for ${target.cap.appliesTo}`,
102
103
  ` ${target.cap.why}`,
103
104
  ` the rules above that limit are not read; move rules this workspace does not share back into each region, or drop the shared copy for this surface`,
104
105
  ].join('\n');
@@ -157,10 +158,10 @@ export async function sharedPlanCommand(_args, flags) {
157
158
  }
158
159
 
159
160
  const lines = [
160
- `shared ${target.root} ${plain(roots.length)} repositories (${target.source})`,
161
- `rules ${plain(shared.length)} of ${plain(candidates)} ids carried once ${plain(bytes)} B`,
161
+ `shared ${target.root} ${plainNumber(roots.length)} repositories (${target.source})`,
162
+ `rules ${plainNumber(shared.length)} of ${plainNumber(candidates)} ids carried once ${plainNumber(bytes)} B`,
162
163
  ];
163
- for (const r of shared) lines.push(` ${r.id} ${r.priority ?? 'unclassified'} ${plain(Buffer.byteLength(r.rendered))} B ${r.source}`);
164
+ for (const r of shared) lines.push(` ${r.id} ${r.priority ?? 'unclassified'} ${plainNumber(Buffer.byteLength(r.rendered))} B ${r.source}`);
164
165
  if (divergent.length > 0) {
165
166
  // Named, not summarised: a reader who expected a python rule here and did not get it
166
167
  // is owed the reason, and "divergent: 2" is not a reason.
@@ -171,9 +172,9 @@ export async function sharedPlanCommand(_args, flags) {
171
172
  lines.push(`left in every region ${excluded.length} rule(s) no root can hand over:`);
172
173
  for (const e of excluded) lines.push(` ${excludedNote(e)}`);
173
174
  }
174
- lines.push(`saves ${plain(save)} B across ${plain(roots.length)} regions`);
175
+ lines.push(`saves ${plainNumber(save)} B across ${plainNumber(roots.length)} regions`);
175
176
  for (const r of rows) {
176
- lines.push(` ${r.name} ${plain(r.bytes)} B ${r.mode} ${r.rules.length > 0 ? r.rules.join(', ') : '(carries nothing extra)'}`);
177
+ lines.push(` ${r.name} ${plainNumber(r.bytes)} B ${r.mode} ${r.rules.length > 0 ? r.rules.join(', ') : '(carries nothing extra)'}`);
177
178
  }
178
179
  lines.push('writes');
179
180
  for (const t of targets) {
@@ -189,7 +190,7 @@ export async function sharedPlanCommand(_args, flags) {
189
190
  if (over !== null) lines.push(`warn ${over}`);
190
191
  }
191
192
  lines.push(`nothing written; re-run with: ${CLI} shared install --yes`);
192
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
193
+ emit(lines.join('\n'));
193
194
  return EXIT.OK;
194
195
  }
195
196
 
@@ -297,7 +298,7 @@ export async function sharedInstallCommand(_args, flags) {
297
298
  }
298
299
  emit(
299
300
  [
300
- `shared ${plain(shared.length)} rule(s), ${plain(Buffer.byteLength(sharedBody(shared)))} B`,
301
+ `shared ${plainNumber(shared.length)} rule(s), ${plainNumber(Buffer.byteLength(sharedBody(shared)))} B`,
301
302
  ...states.map((s) =>
302
303
  s.path === null
303
304
  ? ` paste ${s.surface} no user-level file is recorded for it`
@@ -306,7 +307,6 @@ export async function sharedInstallCommand(_args, flags) {
306
307
  `manifest ${manifests.map((m) => `${m.action}:${basename(m.root)}`).join(' ')}`,
307
308
  dryRun ? 'dry-run nothing written' : `nothing written; re-run with: ${CLI} shared install --yes`,
308
309
  ].join('\n'),
309
- { maxBytes: flags.maxBytes ?? 0 },
310
310
  );
311
311
  // After the report: the report says what, the diff shows it (R50).
312
312
  emitDiffs(diffs, flags);
@@ -327,8 +327,8 @@ export async function sharedInstallCommand(_args, flags) {
327
327
  }
328
328
 
329
329
  const lines = [
330
- `shared installed for ${plain(roots.length)} repositories`,
331
- `rules ${plain(shared.length)} carried once ${shared.map((r) => r.id).join(', ')}`,
330
+ `shared installed for ${plainNumber(roots.length)} repositories`,
331
+ `rules ${plainNumber(shared.length)} carried once ${shared.map((r) => r.id).join(', ')}`,
332
332
  ];
333
333
  for (const w of written) {
334
334
  // `inject`'s word for a file with no region in it is "added"; the word for a file
@@ -354,7 +354,7 @@ export async function sharedInstallCommand(_args, flags) {
354
354
  if (over !== null) lines.push(`warn ${over}`);
355
355
  }
356
356
  lines.push(`verify ${CLI} doctor reports the copy as current, and ${CLI} tokens --repos --root . reports what is left`);
357
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
357
+ emit(lines.join('\n'));
358
358
  // After the report: the report says what, the diff shows it (R50).
359
359
  emitDiffs(diffs, flags);
360
360
  return EXIT.OK;
@@ -418,11 +418,6 @@ function describeShared(state, targets) {
418
418
  return `${targets.length} user-level file(s) carry every rule this repository handed over`;
419
419
  }
420
420
 
421
- /** `1234567`, never `1,234,567`: two machines must print the same bytes (ADR-013). */
422
- function plain(n) {
423
- return String(n);
424
- }
425
-
426
421
  /** The directory name, for a line that has to name several repositories at once. */
427
422
  function basename(path) {
428
423
  return path.split('/').filter(Boolean).pop() ?? path;
@@ -10,6 +10,7 @@
10
10
  // black box.
11
11
 
12
12
  import { readFile, realpath } from 'node:fs/promises';
13
+ import { repoContext } from '../context.mjs';
13
14
  import { join } from 'node:path';
14
15
  import { existsSync } from 'node:fs';
15
16
 
@@ -19,11 +20,14 @@ import { bodyFile, sourcePath } from '../resolve/body.mjs';
19
20
  import { unknownSkills, scoreCorpus } from '../resolve/eval.mjs';
20
21
  import { PACKAGE_ROOT, resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
21
22
  import { fingerprint } from '../resolve/fingerprint.mjs';
22
- import { tokenize, rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
23
+ import { rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
24
+ import { tokenize } from '../text/tokens.mjs';
23
25
  import { localSkillNodes } from '../resolve/local.mjs';
24
26
  import { nearMatches } from '../resolve/near.mjs';
25
27
  import { CLI } from '../name.mjs';
26
28
  import { parsePathList } from '../argv.mjs';
29
+ import { commandRoot } from '../repo-root.mjs';
30
+ import { gitText } from '../sys/git.mjs';
27
31
 
28
32
  export { parsePathList };
29
33
 
@@ -32,11 +36,8 @@ const DEFAULT_BUDGET = 20480;
32
36
  /** git status paths, so path signals come from the real diff. */
33
37
  export async function dirtyPaths(root) {
34
38
  try {
35
- const { execFile } = await import('node:child_process');
36
- const { promisify } = await import('node:util');
37
- const run = promisify(execFile);
38
- const { stdout } = await run('git', ['-C', root, 'status', '--porcelain=v1'], { maxBuffer: 8 << 20 });
39
- return stdout
39
+ const stdout = await gitText(root, ['status', '--porcelain=v1'], { maxBuffer: 8 << 20 });
40
+ return (stdout ?? '')
40
41
  .split('\n')
41
42
  .filter(Boolean)
42
43
  .map((line) => line.slice(3).trim())
@@ -82,11 +83,11 @@ async function skillsRegistry(args) {
82
83
  * costs a fingerprint plus a directory scan, so it runs only when the registry did
83
84
  * not answer.
84
85
  */
85
- async function findSkill(registry, root, name, verb) {
86
+ async function findSkill(registry, root, name, verb, ctx) {
86
87
  if (!name) skillsFailure(`${verb} needs a skill name`, `usage: ${CLI} skills ${verb} <name>`, EXIT.USAGE);
87
88
  const skill = registry.skills.find((s) => s.name === name);
88
89
  if (skill) return skill;
89
- const repoStack = (await fingerprint(root)).signals;
90
+ const repoStack = (await ctx.fingerprint()).signals;
90
91
  const local = (await localSkillNodes(root, registry, repoStack)).find((s) => s.name === name);
91
92
  if (local) return local;
92
93
  const near = nearMatches(name, registry.skills.map((s) => s.name));
@@ -114,7 +115,10 @@ function skillFacts(skill) {
114
115
  /** Tier 1: the selection the task resolves to, with the text packet's display sets. */
115
116
  export async function skillsBriefFacts(root, args = {}) {
116
117
  const registry = await skillsRegistry(args);
117
- const repo = await fingerprint(root);
118
+ // One context per call: a second question about the same repository inside this
119
+ // command costs nothing (review 0003 Q67).
120
+ const ctx = args.ctx ?? repoContext(root);
121
+ const repo = await ctx.fingerprint();
118
122
  const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
119
123
  const repoStack = repo.signals;
120
124
  const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
@@ -246,7 +250,7 @@ export async function skillsFindFacts(root, query, args = {}) {
246
250
  ...registry.skills
247
251
  .map((skill) => ({ skill, ...scoreSkill(skill, context) }))
248
252
  .filter((r) => selectionVerdict(r.evidence, r.reasons) === null),
249
- ...(await localSkillNodes(root, registry, (await fingerprint(root)).signals))
253
+ ...(await localSkillNodes(root, registry, (await (args.ctx ?? repoContext(root)).fingerprint()).signals))
250
254
  .map((skill) => ({ skill, ...scoreSkill(skill, context) }))
251
255
  .filter((r) => r.score > 0),
252
256
  ].sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
@@ -259,7 +263,7 @@ export async function skillsFindFacts(root, query, args = {}) {
259
263
  /** Tier 2: one skill body and the metadata needed to judge it. */
260
264
  export async function skillsShowFacts(root, name, args = {}) {
261
265
  const registry = await skillsRegistry(args);
262
- const skill = await findSkill(registry, root, name, 'show');
266
+ const skill = await findSkill(registry, root, name, 'show', args.ctx ?? repoContext(root));
263
267
  const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
264
268
  // The repo is a location for a declared source too, so `show` reads a body the
265
269
  // caller already has instead of reporting the body missing.
@@ -287,8 +291,9 @@ export async function skillsShowFacts(root, name, args = {}) {
287
291
  /** The signal chain for one skill in one repository. */
288
292
  export async function skillsWhyFacts(root, name, args = {}) {
289
293
  const registry = await skillsRegistry(args);
290
- const skill = await findSkill(registry, root, name, 'why');
291
- const repo = await fingerprint(root);
294
+ const ctx = args.ctx ?? repoContext(root);
295
+ const skill = await findSkill(registry, root, name, 'why', ctx);
296
+ const repo = await ctx.fingerprint();
292
297
  const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
293
298
  const tokens = tokenize(args.task ?? '');
294
299
  const { score, reasons } = scoreSkill(skill, { repoStack: repo.signals, taskTokens: tokens, paths });
@@ -386,7 +391,6 @@ export async function indexCommand(args, flags) {
386
391
  '',
387
392
  `deferred entries return a pointer, not a body. Fetch one with: ${CLI} skills show <name>`,
388
393
  ].join('\n'),
389
- { maxBytes: flags.maxBytes ?? 0 },
390
394
  );
391
395
  return EXIT.OK;
392
396
  }
@@ -429,14 +433,14 @@ async function emitAllFacts(facts, flags) {
429
433
  if (deferred.length > 0) {
430
434
  lines.push('', `DEFERRED ${deferred.length}: ${deferred.map((s) => s.name).join(', ')}`);
431
435
  }
432
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
436
+ emit(lines.join('\n'));
433
437
  return EXIT.OK;
434
438
  }
435
439
 
436
440
  // --- find ---------------------------------------------------------------------
437
441
 
438
442
  export async function findCommand(args, flags) {
439
- const root = flags.root ?? process.cwd();
443
+ const root = commandRoot(flags);
440
444
  let facts;
441
445
  try {
442
446
  facts = await skillsFindFacts(root, args.join(' '), flags);
@@ -449,7 +453,6 @@ export async function findCommand(args, flags) {
449
453
  if (isJson(flags)) {
450
454
  emitJson(
451
455
  { ok: true, query, workspace, results },
452
- { maxBytes: flags.maxBytes ?? 0 },
453
456
  );
454
457
  return EXIT.OK;
455
458
  }
@@ -469,17 +472,17 @@ export async function findCommand(args, flags) {
469
472
  `${r.name.padEnd(26)} ${kb(r.bytes).padStart(7)} ${r.reasons.join(' ')}`,
470
473
  ),
471
474
  ].join('\n'),
472
- { maxBytes: flags.maxBytes ?? 0 },
473
475
  );
474
476
  return EXIT.OK;
475
477
  }
476
478
 
477
479
  // --- brief: tier 1 ------------------------------------------------------------
478
480
  export async function briefCommand(args, flags) {
479
- const root = flags.root ?? process.cwd();
481
+ const root = commandRoot(flags);
482
+ const ctx = repoContext(root);
480
483
  let facts;
481
484
  try {
482
- facts = await skillsBriefFacts(root, flags);
485
+ facts = await skillsBriefFacts(root, { ...flags, ctx });
483
486
  } catch (error) {
484
487
  return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
485
488
  }
@@ -498,21 +501,20 @@ export async function briefCommand(args, flags) {
498
501
  }
499
502
 
500
503
  if (facts.warning) {
501
- const repo = await fingerprint(root);
504
+ const repo = await ctx.fingerprint();
502
505
  emit(
503
506
  [
504
507
  `repo ${root.split('/').pop()} (${repoLine(repo)})`,
505
508
  '',
506
509
  facts.warning,
507
510
  ].join('\n'),
508
- { maxBytes: flags.maxBytes ?? 0 },
509
511
  );
510
512
  return EXIT.OK;
511
513
  }
512
514
 
513
515
  const { shown, shownDeferred, shownUsed, pointerOnly, excludedEntries, workspace, workspaceRoot, budget } = facts;
514
516
  const lines = [];
515
- const repo = await fingerprint(root);
517
+ const repo = await ctx.fingerprint();
516
518
 
517
519
  lines.push(
518
520
  `RESOLVED ${shown.length} skills, ${kb(shownUsed)}${flags.all || shownUsed <= budget ? '' : ' (budget capped)'} - repo ${root.split('/').pop()} (${repoLine(repo)})`,
@@ -533,7 +535,7 @@ export async function briefCommand(args, flags) {
533
535
  lines.push('', `NOT SELECTED ${hidden.length}`, '', ` notes: ${CLI} skills why <name> | ${CLI} skills show <name>`);
534
536
  }
535
537
  lines.push('', 'OVERRIDE --skill <name> | --budget <bytes> | --all | --paths <globs>');
536
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
538
+ emit(lines.join('\n'));
537
539
  return EXIT.OK;
538
540
  }
539
541
 
@@ -578,14 +580,14 @@ export async function briefCommand(args, flags) {
578
580
  }
579
581
 
580
582
  lines.push('', 'OVERRIDE --skill <name> | --budget <bytes> | --all | --paths <globs>');
581
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
583
+ emit(lines.join('\n'));
582
584
  return EXIT.OK;
583
585
  }
584
586
 
585
587
  // --- show: tier 2 -------------------------------------------------------------
586
588
 
587
589
  export async function showCommand(args, flags) {
588
- const root = flags.root ?? process.cwd();
590
+ const root = commandRoot(flags);
589
591
  let facts;
590
592
  try {
591
593
  facts = await skillsShowFacts(root, args[0], flags);
@@ -595,7 +597,7 @@ export async function showCommand(args, flags) {
595
597
  const { skill, workspace, body, bodyFrom, bodyText } = facts;
596
598
  const source = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
597
599
  if (isJson(flags)) {
598
- emitJson({ ok: true, ...skill, workspace, body, bodyFrom, bodyText }, { maxBytes: flags.maxBytes ?? 0 });
600
+ emitJson({ ok: true, ...skill, workspace, body, bodyFrom, bodyText });
599
601
  return EXIT.OK;
600
602
  }
601
603
  emit(
@@ -618,7 +620,6 @@ export async function showCommand(args, flags) {
618
620
  ]
619
621
  .filter((l) => l !== null)
620
622
  .join('\n'),
621
- { maxBytes: flags.maxBytes ?? 0 },
622
623
  );
623
624
  return EXIT.OK;
624
625
  }
@@ -626,7 +627,7 @@ export async function showCommand(args, flags) {
626
627
  // --- why ----------------------------------------------------------------------
627
628
 
628
629
  export async function whyCommand(args, flags) {
629
- const root = flags.root ?? process.cwd();
630
+ const root = commandRoot(flags);
630
631
  let facts;
631
632
  try {
632
633
  facts = await skillsWhyFacts(root, args[0], flags);
@@ -641,7 +642,7 @@ export async function whyCommand(args, flags) {
641
642
  // to scrape: a skill that can never be selected in this repo is a decision, not
642
643
  // a line of output.
643
644
  if (isJson(flags)) {
644
- emitJson({ ok: true, command: 'skills why', ...facts }, { maxBytes: flags.maxBytes ?? 0 });
645
+ emitJson({ ok: true, command: 'skills why', ...facts });
645
646
  return EXIT.OK;
646
647
  }
647
648
 
@@ -667,7 +668,7 @@ export async function whyCommand(args, flags) {
667
668
  if (facts.variantOf) lines.push('', `variant-of ${facts.variantOf} (sibling group, one body per stack)`);
668
669
  if (facts.requires?.length > 0) lines.push(`requires ${facts.requires.join(', ')}`);
669
670
 
670
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
671
+ emit(lines.join('\n'));
671
672
  return EXIT.OK;
672
673
  }
673
674
 
@@ -773,7 +774,7 @@ export async function evalCommand(positional, flags) {
773
774
  }
774
775
 
775
776
  lines.push('', okRun ? 'OK the resolver meets the gold corpus' : 'REGRESSION: the resolver no longer meets the gold corpus');
776
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
777
+ emit(lines.join('\n'));
777
778
  return okRun ? EXIT.OK : EXIT.FAIL;
778
779
  }
779
780
 
@@ -14,14 +14,15 @@ import {
14
14
  evaluateSkillTriggers,
15
15
  proposeSkillDescription,
16
16
  } from '../graph/compile.mjs';
17
- import { loadRunRecords } from '../bench/report.mjs';
18
- import { extractPatchFiles } from '../bench/grade.mjs';
19
- import { resolveVaultDir } from '../bench/mine.mjs';
17
+ import { loadRunRecords } from '../measure/records.mjs';
18
+ import { extractPatchFiles } from '../testing/patches.mjs';
19
+ import { resolveVaultDir } from '../testing/vault.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
23
  import { CLI } from '../name.mjs';
24
24
  import { LOCAL_DIR } from '../state.mjs';
25
+ import { commandRoot } from '../repo-root.mjs';
25
26
 
26
27
  async function readSkillRawText(skill, workspaceRoot, root) {
27
28
  const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
@@ -68,7 +69,7 @@ export async function compileCommand(args, flags) {
68
69
  const { registry, errors, ok } = await loadRegistry(null, flags);
69
70
  if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
70
71
 
71
- const root = resolve(flags.root ?? process.cwd());
72
+ const root = resolve(commandRoot(flags));
72
73
  const ws = await resolveWorkspace(flags);
73
74
  const targetName = args[0] ?? null;
74
75
 
@@ -117,7 +118,6 @@ export async function compileCommand(args, flags) {
117
118
  splitPlan: s.splitPlan,
118
119
  })),
119
120
  },
120
- { maxBytes: flags.maxBytes ?? 0 },
121
121
  );
122
122
  return EXIT.OK;
123
123
  }
@@ -150,7 +150,7 @@ export async function compileCommand(args, flags) {
150
150
  }
151
151
  }
152
152
 
153
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
153
+ emit(lines.join('\n'));
154
154
  return EXIT.OK;
155
155
  }
156
156
 
@@ -223,7 +223,6 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
223
223
  : null,
224
224
  proposal,
225
225
  },
226
- { maxBytes: flags.maxBytes ?? 0 },
227
226
  );
228
227
  return EXIT.OK;
229
228
  }
@@ -246,7 +245,7 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
246
245
  }
247
246
  }
248
247
 
249
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
248
+ emit(lines.join('\n'));
250
249
  if (!mode.apply) {
251
250
  note(`note dry-run nothing written; apply with: ${CLI} skills compile ${skill.name} --yes`);
252
251
  }
@@ -38,6 +38,8 @@ import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
38
38
  import { CLI } from '../name.mjs';
39
39
  import { MANIFEST_FILE } from '../name.mjs';
40
40
  import { nameProblems } from '../graph/spec.mjs';
41
+ import { commandRoot } from '../repo-root.mjs';
42
+ import { parseFrontmatter } from '../text/frontmatter.mjs';
41
43
 
42
44
  /**
43
45
  * Where skills live in this repo. The estate already has a convention - a
@@ -82,7 +84,7 @@ export async function installCommand(args, flags) {
82
84
  const ws = await resolveWorkspace(flags);
83
85
  if (ws.error) return fail(`workspace config: ${ws.error}`, EXIT.CONFIG, 'fix the JSON, or remove the file');
84
86
 
85
- const root = flags.root ?? process.cwd();
87
+ const root = commandRoot(flags);
86
88
  const repo = await fingerprint(root);
87
89
  const pick = await chooseSource(skill, repo.signals, flags, { root, workspaceRoot: ws.root });
88
90
  if (pick.error) return fail(pick.error, EXIT.USAGE, pick.remedy);
@@ -210,7 +212,6 @@ export async function installCommand(args, flags) {
210
212
  ]
211
213
  .filter((l) => l !== null)
212
214
  .join('\n'),
213
- { maxBytes: flags.maxBytes ?? 0 },
214
215
  );
215
216
  }
216
217
 
@@ -498,13 +499,13 @@ async function dirEntries(root) {
498
499
  */
499
500
  export async function wrapFlat(src, name) {
500
501
  const text = await readFile(src, 'utf8');
501
- const fm = /^---\n([\s\S]*?)\n---\n?/.exec(text);
502
- if (!fm) return `---\nname: ${name}\n---\n\n${text.trim()}\n`;
503
- const keys = fm[1]
502
+ const { front, block } = parseFrontmatter(text);
503
+ if (front === null) return `---\nname: ${name}\n---\n\n${text.trim()}\n`;
504
+ const keys = front
504
505
  .split('\n')
505
506
  .filter((l) => !/^name:/.test(l))
506
507
  .join('\n');
507
- return `---\nname: ${name}\n${keys}\n---\n\n${text.slice(fm[0].length).trim()}\n`;
508
+ return `---\nname: ${name}\n${keys}\n---\n\n${text.slice(block.length).trim()}\n`;
508
509
  }
509
510
 
510
511
  async function fileSha(path) {