simt-dev 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +162 -841
  2. package/README.md +21 -135
  3. package/agents/simt-agent.md +84 -0
  4. package/bin/simt.mjs +19 -55
  5. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  6. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  7. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
  8. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  9. package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
  10. package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
  11. package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
  12. 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
  13. 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
  14. package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
  15. package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
  16. package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
  17. 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
  18. 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
  19. package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
  20. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  21. 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
  22. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  23. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  24. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  25. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  26. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  27. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  28. package/package.json +13 -3
  29. package/rules/core.md +9 -2
  30. package/rules/mdc/slice-commit.mdc +57 -4
  31. package/rules/mdc/test-gate.mdc +2 -0
  32. package/rules/stacks/android.md +3 -3
  33. package/rules/stacks/connectiq.md +2 -2
  34. package/rules/stacks/dart.md +1 -1
  35. package/rules/stacks/deno.md +1 -1
  36. package/rules/stacks/dotnet.md +1 -1
  37. package/rules/stacks/elixir.md +1 -1
  38. package/rules/stacks/go.md +1 -1
  39. package/rules/stacks/jvm.md +3 -2
  40. package/rules/stacks/node.md +2 -2
  41. package/rules/stacks/python.md +7 -6
  42. package/rules/stacks/ruby.md +1 -1
  43. package/rules/stacks/rust.md +1 -1
  44. package/rules/stacks/swift.md +1 -1
  45. package/rules/stacks/typescript.md +2 -2
  46. package/rules/stacks/uv.md +3 -2
  47. package/skills/simt-commands/SKILL.md +70 -0
  48. package/src/advice/advice.mjs +7 -3
  49. package/src/agenthooks/decide.mjs +141 -9
  50. package/src/agenthooks/opencode.mjs +72 -0
  51. package/src/agenthooks/prompt.mjs +10 -3
  52. package/src/agenthooks/render.mjs +186 -19
  53. package/src/agenthooks/shim.mjs +55 -0
  54. package/src/agents/render.mjs +277 -0
  55. package/src/api.mjs +29 -0
  56. package/src/argv.mjs +37 -3
  57. package/src/bench/ablate.mjs +31 -14
  58. package/src/bench/adapters.json +16 -0
  59. package/src/bench/corpus.mjs +150 -0
  60. package/src/bench/grade.mjs +23 -49
  61. package/src/bench/mine.mjs +247 -36
  62. package/src/bench/probes.mjs +104 -68
  63. package/src/bench/process.mjs +128 -0
  64. package/src/bench/report.mjs +260 -84
  65. package/src/bench/run.mjs +251 -150
  66. package/src/bench/variants.mjs +112 -40
  67. package/src/brief/assemble.mjs +44 -3
  68. package/src/ci/workflow.mjs +46 -14
  69. package/src/cli.mjs +45 -0
  70. package/src/commands/agent.mjs +205 -0
  71. package/src/commands/agenthook.mjs +113 -20
  72. package/src/commands/bench.mjs +192 -18
  73. package/src/commands/brief.mjs +5 -4
  74. package/src/commands/ci.mjs +2 -1
  75. package/src/commands/compare.mjs +50 -49
  76. package/src/commands/completions.mjs +2 -6
  77. package/src/commands/decide.mjs +10 -9
  78. package/src/commands/distill.mjs +4 -3
  79. package/src/commands/doctor.mjs +11 -7
  80. package/src/commands/evidence.mjs +19 -12
  81. package/src/commands/explain.mjs +0 -1
  82. package/src/commands/gate.mjs +98 -28
  83. package/src/commands/graph.mjs +20 -8
  84. package/src/commands/harness.mjs +3 -2
  85. package/src/commands/help.mjs +160 -31
  86. package/src/commands/hooks.mjs +163 -50
  87. package/src/commands/ignore.mjs +2 -1
  88. package/src/commands/impact.mjs +8 -9
  89. package/src/commands/index.mjs +278 -180
  90. package/src/commands/init.mjs +171 -38
  91. package/src/commands/lease.mjs +206 -39
  92. package/src/commands/lessons.mjs +72 -15
  93. package/src/commands/local.mjs +3 -3
  94. package/src/commands/map.mjs +47 -27
  95. package/src/commands/mcp.mjs +0 -1
  96. package/src/commands/pack.mjs +9 -6
  97. package/src/commands/plan.mjs +6 -5
  98. package/src/commands/plugin.mjs +3 -3
  99. package/src/commands/prbody.mjs +3 -3
  100. package/src/commands/repoindex.mjs +5 -3
  101. package/src/commands/review.mjs +2 -2
  102. package/src/commands/rules.mjs +124 -33
  103. package/src/commands/shared.mjs +2 -3
  104. package/src/commands/skills.mjs +82 -45
  105. package/src/commands/skillscompile.mjs +12 -12
  106. package/src/commands/skillsinstall.mjs +14 -2
  107. package/src/commands/slice.mjs +396 -26
  108. package/src/commands/spec.mjs +151 -0
  109. package/src/commands/status.mjs +4 -7
  110. package/src/commands/sync.mjs +50 -33
  111. package/src/commands/tests.mjs +11 -11
  112. package/src/commands/tokens.mjs +2 -2
  113. package/src/commands/undo.mjs +29 -31
  114. package/src/commands/upgrade.mjs +40 -4
  115. package/src/commands/why.mjs +19 -12
  116. package/src/compare/hooksstate.mjs +23 -1
  117. package/src/compare/learn.mjs +82 -57
  118. package/src/compare/parallel.mjs +50 -0
  119. package/src/compare/row.mjs +26 -3
  120. package/src/completions.mjs +5 -0
  121. package/src/context.mjs +64 -0
  122. package/src/decisions/dir.mjs +84 -0
  123. package/src/decisions/index-doc.mjs +46 -0
  124. package/src/decisions/index.mjs +109 -11
  125. package/src/diff.mjs +1 -1
  126. package/src/distill/distill.mjs +17 -2
  127. package/src/distill/engine.mjs +5 -1
  128. package/src/ecosystem/node-runner.mjs +144 -0
  129. package/src/explain.mjs +18 -6
  130. package/src/graph/load.mjs +42 -20
  131. package/src/hooks/capability.mjs +17 -4
  132. package/src/hooks/dispatcher.mjs +513 -36
  133. package/src/hooks/gates.mjs +0 -43
  134. package/src/hooks/resolve.mjs +11 -10
  135. package/src/hooks/shim.mjs +17 -9
  136. package/src/identity.mjs +96 -15
  137. package/src/index/extract.mjs +192 -11
  138. package/src/index/persist.mjs +307 -47
  139. package/src/index/store.mjs +263 -60
  140. package/src/index/tests.mjs +6 -35
  141. package/src/journal.mjs +372 -84
  142. package/src/kernel/card.mjs +37 -12
  143. package/src/kernel/checks.mjs +44 -9
  144. package/src/kernel/variants.mjs +27 -16
  145. package/src/lease.mjs +97 -15
  146. package/src/ledger/index.mjs +120 -25
  147. package/src/local/client.mjs +4 -6
  148. package/src/locate.mjs +33 -9
  149. package/src/manifest/schema.mjs +213 -0
  150. package/src/mcp/resources.mjs +12 -13
  151. package/src/mcp/server.mjs +151 -38
  152. package/src/mcp/tools.mjs +90 -16
  153. package/src/measure/session.mjs +3 -2
  154. package/src/memory/decide.mjs +98 -43
  155. package/src/memory/lessons.mjs +168 -33
  156. package/src/name.mjs +9 -0
  157. package/src/node-version.mjs +37 -0
  158. package/src/output/index.mjs +111 -3
  159. package/src/packs/manage.mjs +17 -3
  160. package/src/packs.mjs +20 -2
  161. package/src/plan/index.mjs +31 -8
  162. package/src/plan/template.mjs +27 -5
  163. package/src/plugins.mjs +6 -2
  164. package/src/prbody.mjs +7 -5
  165. package/src/projection/baselines.mjs +1 -1
  166. package/src/projection/disclosure.mjs +2 -2
  167. package/src/projection/loaders.mjs +41 -1
  168. package/src/projection/lock.mjs +31 -14
  169. package/src/projection/manifest.mjs +89 -53
  170. package/src/projection/markers.mjs +43 -3
  171. package/src/projection/mdc.mjs +34 -7
  172. package/src/projection/project.mjs +162 -18
  173. package/src/projection/rules.mjs +58 -6
  174. package/src/projection/surfaces.mjs +9 -1
  175. package/src/repo-root.mjs +88 -0
  176. package/src/resolve/body.mjs +4 -0
  177. package/src/resolve/fingerprint.mjs +24 -15
  178. package/src/resolve/local.mjs +58 -0
  179. package/src/review/critic.mjs +6 -3
  180. package/src/secrets.mjs +165 -22
  181. package/src/state.mjs +76 -0
  182. package/src/status.mjs +9 -4
  183. package/src/toolwords.mjs +8 -0
  184. package/src/tsconfig.mjs +39 -99
  185. package/src/workspace.mjs +55 -10
  186. package/templates/plan/tracker.json +1 -1
  187. package/docs/decisions/README.md +0 -591
@@ -9,7 +9,8 @@
9
9
  // token instead of guessing. Suppressing it is how a discovery tool becomes a
10
10
  // black box.
11
11
 
12
- import { readFile } from 'node:fs/promises';
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
 
@@ -20,9 +21,11 @@ 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
23
  import { tokenize, rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
24
+ import { localSkillNodes } from '../resolve/local.mjs';
23
25
  import { nearMatches } from '../resolve/near.mjs';
24
26
  import { CLI } from '../name.mjs';
25
27
  import { parsePathList } from '../argv.mjs';
28
+ import { commandRoot } from '../repo-root.mjs';
26
29
 
27
30
  export { parsePathList };
28
31
 
@@ -75,18 +78,25 @@ async function skillsRegistry(args) {
75
78
  return registry;
76
79
  }
77
80
 
78
- function findSkill(registry, name, verb) {
81
+ /**
82
+ * The named skill: the registry first, then the repo's own skills, because a skill
83
+ * the repo installed is addressable by the same name (plan 0005 S3). The local lookup
84
+ * costs a fingerprint plus a directory scan, so it runs only when the registry did
85
+ * not answer.
86
+ */
87
+ async function findSkill(registry, root, name, verb, ctx) {
79
88
  if (!name) skillsFailure(`${verb} needs a skill name`, `usage: ${CLI} skills ${verb} <name>`, EXIT.USAGE);
80
89
  const skill = registry.skills.find((s) => s.name === name);
81
- if (!skill) {
82
- const near = nearMatches(name, registry.skills.map((s) => s.name));
83
- skillsFailure(
84
- `no skill named "${name}"${near.length ? ` (did you mean ${near.join(', ')}?)` : ''}`,
85
- `run: ${CLI} skills find <query>`,
86
- EXIT.USAGE,
87
- );
88
- }
89
- return skill;
90
+ if (skill) return skill;
91
+ const repoStack = (await ctx.fingerprint()).signals;
92
+ const local = (await localSkillNodes(root, registry, repoStack)).find((s) => s.name === name);
93
+ if (local) return local;
94
+ const near = nearMatches(name, registry.skills.map((s) => s.name));
95
+ skillsFailure(
96
+ `no skill named "${name}"${near.length ? ` (did you mean ${near.join(', ')}?)` : ''}`,
97
+ `run: ${CLI} skills find <query>`,
98
+ EXIT.USAGE,
99
+ );
90
100
  }
91
101
 
92
102
  /** The JSON-safe skill facts both surfaces spread: absent lists read as empty, not missing. */
@@ -106,7 +116,10 @@ function skillFacts(skill) {
106
116
  /** Tier 1: the selection the task resolves to, with the text packet's display sets. */
107
117
  export async function skillsBriefFacts(root, args = {}) {
108
118
  const registry = await skillsRegistry(args);
109
- const repo = await fingerprint(root);
119
+ // One context per call: a second question about the same repository inside this
120
+ // command costs nothing (review 0003 Q67).
121
+ const ctx = args.ctx ?? repoContext(root);
122
+ const repo = await ctx.fingerprint();
110
123
  const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
111
124
  const repoStack = repo.signals;
112
125
  const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
@@ -116,6 +129,10 @@ export async function skillsBriefFacts(root, args = {}) {
116
129
  const task = args.task ?? null;
117
130
  const repoFacts = { root, stack: repoStack, facts: repo.facts };
118
131
 
132
+ // The repo's own skills rank alongside the registry, so every command agrees on
133
+ // the names a reader in this repo can address (plan 0005 S3).
134
+ const pool = [...registry.skills, ...(await localSkillNodes(root, registry, repoStack))];
135
+
119
136
  // A brief with no task and no paths resolves on stack alone, which produces a
120
137
  // plausible-looking list the agent may act on. It warns rather than silently
121
138
  // answering; under `--json` the warning is a field, not prose on stdout.
@@ -125,7 +142,7 @@ export async function skillsBriefFacts(root, args = {}) {
125
142
  'pass --task "<what you are doing>" or stage files so the diff drives selection, ' +
126
143
  'or pass --all to take everything for this stack.';
127
144
  if (args.all) {
128
- const usable = registry.skills.filter((s) => stackCompatible(s, repoStack));
145
+ const usable = pool.filter((s) => stackCompatible(s, repoStack));
129
146
  return {
130
147
  kind: 'all',
131
148
  repoStack,
@@ -153,12 +170,12 @@ export async function skillsBriefFacts(root, args = {}) {
153
170
  }
154
171
 
155
172
  const context = { repoStack, taskTokens, paths };
156
- const { selected, excluded } = rankSkills(registry.skills, context, { forced });
173
+ const { selected, excluded } = rankSkills(pool, context, { forced });
157
174
 
158
175
  // Hard dependencies are pulled in even when they did not score, and even past
159
176
  // budget: a skill that is useless without its dependency is worse than a large
160
177
  // context, because it silently misleads.
161
- const expanded = closure(selected, registry.skills);
178
+ const expanded = closure(selected, pool);
162
179
  const { picked, deferred, used: inlineBytes } = selectWithinBudget(expanded, {
163
180
  budget,
164
181
  all: args.all === true,
@@ -216,31 +233,53 @@ export async function skillsBriefFacts(root, args = {}) {
216
233
  };
217
234
  }
218
235
 
219
- /** Raw search across capability, keyword and summary. */
220
- export async function skillsFindFacts(query, args = {}) {
236
+ /**
237
+ * Raw search across capability, keyword and summary.
238
+ *
239
+ * Local nodes are admitted on `score > 0`, not the `selectionVerdict` rule the
240
+ * registry nodes keep: that rule exists to compensate for find having no repo, and
241
+ * a local skill has one - its presence in the repo is the scoping signal the rule
242
+ * stands in for.
243
+ */
244
+ export async function skillsFindFacts(root, query, args = {}) {
221
245
  const q = String(query ?? '').trim();
222
246
  if (!q) skillsFailure('find needs a query', `usage: ${CLI} skills find <query>`, EXIT.USAGE);
223
247
  const registry = await skillsRegistry(args);
224
248
  const tokens = tokenize(q);
225
- const results = registry.skills
226
- .map((skill) => ({ skill, ...scoreSkill(skill, { repoStack: [], taskTokens: tokens, paths: [] }) }))
227
- .filter((r) => selectionVerdict(r.evidence, r.reasons) === null)
228
- .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
249
+ const context = { repoStack: [], taskTokens: tokens, paths: [] };
250
+ const results = [
251
+ ...registry.skills
252
+ .map((skill) => ({ skill, ...scoreSkill(skill, context) }))
253
+ .filter((r) => selectionVerdict(r.evidence, r.reasons) === null),
254
+ ...(await localSkillNodes(root, registry, (await (args.ctx ?? repoContext(root)).fingerprint()).signals))
255
+ .map((skill) => ({ skill, ...scoreSkill(skill, context) }))
256
+ .filter((r) => r.score > 0),
257
+ ].sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
229
258
  return {
230
259
  query: q,
231
- results: results.map((r) => ({ name: r.skill.name, score: r.score, summary: r.skill.summary, reasons: r.reasons })),
260
+ results: results.map((r) => ({ name: r.skill.name, score: r.score, bytes: r.skill.bytes, summary: r.skill.summary, reasons: r.reasons })),
232
261
  };
233
262
  }
234
263
 
235
264
  /** Tier 2: one skill body and the metadata needed to judge it. */
236
265
  export async function skillsShowFacts(root, name, args = {}) {
237
266
  const registry = await skillsRegistry(args);
238
- const skill = findSkill(registry, name, 'show');
267
+ const skill = await findSkill(registry, root, name, 'show', args.ctx ?? repoContext(root));
239
268
  const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
240
269
  // The repo is a location for a declared source too, so `show` reads a body the
241
270
  // caller already has instead of reporting the body missing.
242
271
  const body = await sourcePath(skill, workspaceRoot, root);
243
- const bodyFrom = body === null ? 'none' : body.startsWith(`${root}/`) ? 'repo' : 'workspace';
272
+ // A local file is canonicalized by realpath, while a declared repo source keeps the
273
+ // caller's spelling: on a symlinked prefix (macOS `/var`) the label must accept the
274
+ // canonical root too, or a repo body is mislabelled `workspace`.
275
+ let realRoot = root;
276
+ try {
277
+ realRoot = await realpath(root);
278
+ } catch {
279
+ /* root does not exist; the label falls back to the un-canonical comparison */
280
+ }
281
+ const bodyFrom =
282
+ body === null ? 'none' : body.startsWith(`${root}/`) || body.startsWith(`${realRoot}/`) ? 'repo' : 'workspace';
244
283
  return {
245
284
  skill: skillFacts(skill),
246
285
  workspace,
@@ -253,8 +292,9 @@ export async function skillsShowFacts(root, name, args = {}) {
253
292
  /** The signal chain for one skill in one repository. */
254
293
  export async function skillsWhyFacts(root, name, args = {}) {
255
294
  const registry = await skillsRegistry(args);
256
- const skill = findSkill(registry, name, 'why');
257
- const repo = await fingerprint(root);
295
+ const ctx = args.ctx ?? repoContext(root);
296
+ const skill = await findSkill(registry, root, name, 'why', ctx);
297
+ const repo = await ctx.fingerprint();
258
298
  const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
259
299
  const tokens = tokenize(args.task ?? '');
260
300
  const { score, reasons } = scoreSkill(skill, { repoStack: repo.signals, taskTokens: tokens, paths });
@@ -352,7 +392,6 @@ export async function indexCommand(args, flags) {
352
392
  '',
353
393
  `deferred entries return a pointer, not a body. Fetch one with: ${CLI} skills show <name>`,
354
394
  ].join('\n'),
355
- { maxBytes: flags.maxBytes ?? 0 },
356
395
  );
357
396
  return EXIT.OK;
358
397
  }
@@ -395,16 +434,17 @@ async function emitAllFacts(facts, flags) {
395
434
  if (deferred.length > 0) {
396
435
  lines.push('', `DEFERRED ${deferred.length}: ${deferred.map((s) => s.name).join(', ')}`);
397
436
  }
398
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
437
+ emit(lines.join('\n'));
399
438
  return EXIT.OK;
400
439
  }
401
440
 
402
441
  // --- find ---------------------------------------------------------------------
403
442
 
404
443
  export async function findCommand(args, flags) {
444
+ const root = commandRoot(flags);
405
445
  let facts;
406
446
  try {
407
- facts = await skillsFindFacts(args.join(' '), flags);
447
+ facts = await skillsFindFacts(root, args.join(' '), flags);
408
448
  } catch (error) {
409
449
  return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
410
450
  }
@@ -414,7 +454,6 @@ export async function findCommand(args, flags) {
414
454
  if (isJson(flags)) {
415
455
  emitJson(
416
456
  { ok: true, query, workspace, results },
417
- { maxBytes: flags.maxBytes ?? 0 },
418
457
  );
419
458
  return EXIT.OK;
420
459
  }
@@ -434,17 +473,17 @@ export async function findCommand(args, flags) {
434
473
  `${r.name.padEnd(26)} ${kb(r.bytes).padStart(7)} ${r.reasons.join(' ')}`,
435
474
  ),
436
475
  ].join('\n'),
437
- { maxBytes: flags.maxBytes ?? 0 },
438
476
  );
439
477
  return EXIT.OK;
440
478
  }
441
479
 
442
480
  // --- brief: tier 1 ------------------------------------------------------------
443
481
  export async function briefCommand(args, flags) {
444
- const root = flags.root ?? process.cwd();
482
+ const root = commandRoot(flags);
483
+ const ctx = repoContext(root);
445
484
  let facts;
446
485
  try {
447
- facts = await skillsBriefFacts(root, flags);
486
+ facts = await skillsBriefFacts(root, { ...flags, ctx });
448
487
  } catch (error) {
449
488
  return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
450
489
  }
@@ -463,21 +502,20 @@ export async function briefCommand(args, flags) {
463
502
  }
464
503
 
465
504
  if (facts.warning) {
466
- const repo = await fingerprint(root);
505
+ const repo = await ctx.fingerprint();
467
506
  emit(
468
507
  [
469
508
  `repo ${root.split('/').pop()} (${repoLine(repo)})`,
470
509
  '',
471
510
  facts.warning,
472
511
  ].join('\n'),
473
- { maxBytes: flags.maxBytes ?? 0 },
474
512
  );
475
513
  return EXIT.OK;
476
514
  }
477
515
 
478
516
  const { shown, shownDeferred, shownUsed, pointerOnly, excludedEntries, workspace, workspaceRoot, budget } = facts;
479
517
  const lines = [];
480
- const repo = await fingerprint(root);
518
+ const repo = await ctx.fingerprint();
481
519
 
482
520
  lines.push(
483
521
  `RESOLVED ${shown.length} skills, ${kb(shownUsed)}${flags.all || shownUsed <= budget ? '' : ' (budget capped)'} - repo ${root.split('/').pop()} (${repoLine(repo)})`,
@@ -498,7 +536,7 @@ export async function briefCommand(args, flags) {
498
536
  lines.push('', `NOT SELECTED ${hidden.length}`, '', ` notes: ${CLI} skills why <name> | ${CLI} skills show <name>`);
499
537
  }
500
538
  lines.push('', 'OVERRIDE --skill <name> | --budget <bytes> | --all | --paths <globs>');
501
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
539
+ emit(lines.join('\n'));
502
540
  return EXIT.OK;
503
541
  }
504
542
 
@@ -543,14 +581,14 @@ export async function briefCommand(args, flags) {
543
581
  }
544
582
 
545
583
  lines.push('', 'OVERRIDE --skill <name> | --budget <bytes> | --all | --paths <globs>');
546
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
584
+ emit(lines.join('\n'));
547
585
  return EXIT.OK;
548
586
  }
549
587
 
550
588
  // --- show: tier 2 -------------------------------------------------------------
551
589
 
552
590
  export async function showCommand(args, flags) {
553
- const root = flags.root ?? process.cwd();
591
+ const root = commandRoot(flags);
554
592
  let facts;
555
593
  try {
556
594
  facts = await skillsShowFacts(root, args[0], flags);
@@ -560,7 +598,7 @@ export async function showCommand(args, flags) {
560
598
  const { skill, workspace, body, bodyFrom, bodyText } = facts;
561
599
  const source = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
562
600
  if (isJson(flags)) {
563
- emitJson({ ok: true, ...skill, workspace, body, bodyFrom, bodyText }, { maxBytes: flags.maxBytes ?? 0 });
601
+ emitJson({ ok: true, ...skill, workspace, body, bodyFrom, bodyText });
564
602
  return EXIT.OK;
565
603
  }
566
604
  emit(
@@ -583,7 +621,6 @@ export async function showCommand(args, flags) {
583
621
  ]
584
622
  .filter((l) => l !== null)
585
623
  .join('\n'),
586
- { maxBytes: flags.maxBytes ?? 0 },
587
624
  );
588
625
  return EXIT.OK;
589
626
  }
@@ -591,7 +628,7 @@ export async function showCommand(args, flags) {
591
628
  // --- why ----------------------------------------------------------------------
592
629
 
593
630
  export async function whyCommand(args, flags) {
594
- const root = flags.root ?? process.cwd();
631
+ const root = commandRoot(flags);
595
632
  let facts;
596
633
  try {
597
634
  facts = await skillsWhyFacts(root, args[0], flags);
@@ -606,7 +643,7 @@ export async function whyCommand(args, flags) {
606
643
  // to scrape: a skill that can never be selected in this repo is a decision, not
607
644
  // a line of output.
608
645
  if (isJson(flags)) {
609
- emitJson({ ok: true, command: 'skills why', ...facts }, { maxBytes: flags.maxBytes ?? 0 });
646
+ emitJson({ ok: true, command: 'skills why', ...facts });
610
647
  return EXIT.OK;
611
648
  }
612
649
 
@@ -632,7 +669,7 @@ export async function whyCommand(args, flags) {
632
669
  if (facts.variantOf) lines.push('', `variant-of ${facts.variantOf} (sibling group, one body per stack)`);
633
670
  if (facts.requires?.length > 0) lines.push(`requires ${facts.requires.join(', ')}`);
634
671
 
635
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
672
+ emit(lines.join('\n'));
636
673
  return EXIT.OK;
637
674
  }
638
675
 
@@ -738,7 +775,7 @@ export async function evalCommand(positional, flags) {
738
775
  }
739
776
 
740
777
  lines.push('', okRun ? 'OK the resolver meets the gold corpus' : 'REGRESSION: the resolver no longer meets the gold corpus');
741
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
778
+ emit(lines.join('\n'));
742
779
  return okRun ? EXIT.OK : EXIT.FAIL;
743
780
  }
744
781
 
@@ -20,7 +20,9 @@ import { resolveVaultDir } from '../bench/mine.mjs';
20
20
  import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
21
21
  import { nearMatches } from '../resolve/near.mjs';
22
22
  import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
23
- import { CLI, STATE_DIR } from '../name.mjs';
23
+ import { CLI } from '../name.mjs';
24
+ import { LOCAL_DIR } from '../state.mjs';
25
+ import { commandRoot } from '../repo-root.mjs';
24
26
 
25
27
  async function readSkillRawText(skill, workspaceRoot, root) {
26
28
  const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
@@ -44,7 +46,7 @@ async function readSkillRawText(skill, workspaceRoot, root) {
44
46
 
45
47
  async function loadGoldPathsByTask(root) {
46
48
  const map = new Map();
47
- const tasksDir = join(root, STATE_DIR, 'bench', 'tasks');
49
+ const tasksDir = join(root, '.simt', 'bench', 'tasks');
48
50
  const vaultRoot = resolveVaultDir(root);
49
51
  const entries = (await readdir(tasksDir).catch(() => [])).filter((f) => f.endsWith('.json')).sort();
50
52
  for (const entry of entries) {
@@ -67,7 +69,7 @@ export async function compileCommand(args, flags) {
67
69
  const { registry, errors, ok } = await loadRegistry(null, flags);
68
70
  if (!ok) return fail(errors[0], EXIT.REGISTRY, WORKSPACE_REMEDY);
69
71
 
70
- const root = resolve(flags.root ?? process.cwd());
72
+ const root = resolve(commandRoot(flags));
71
73
  const ws = await resolveWorkspace(flags);
72
74
  const targetName = args[0] ?? null;
73
75
 
@@ -116,7 +118,6 @@ export async function compileCommand(args, flags) {
116
118
  splitPlan: s.splitPlan,
117
119
  })),
118
120
  },
119
- { maxBytes: flags.maxBytes ?? 0 },
120
121
  );
121
122
  return EXIT.OK;
122
123
  }
@@ -149,7 +150,7 @@ export async function compileCommand(args, flags) {
149
150
  }
150
151
  }
151
152
 
152
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
153
+ emit(lines.join('\n'));
153
154
  return EXIT.OK;
154
155
  }
155
156
 
@@ -172,26 +173,26 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
172
173
  triggerStats,
173
174
  });
174
175
 
175
- const outDir = join(root, STATE_DIR, 'compiled', 'skills', skill.name);
176
- const proposalPath = join(root, STATE_DIR, 'proposals', 'skills', `${skill.name}.json`);
176
+ const outDir = join(root, LOCAL_DIR, 'compiled', 'skills', skill.name);
177
+ const proposalPath = join(root, LOCAL_DIR, 'proposals', 'skills', `${skill.name}.json`);
177
178
  const writtenFiles = [];
178
179
 
179
180
  if (mode.apply) {
180
181
  await mkdir(outDir, { recursive: true });
181
182
  await writeFile(join(outDir, 'SKILL.md'), compiled.mainText, 'utf8');
182
- writtenFiles.push(`${STATE_DIR}/compiled/skills/${skill.name}/SKILL.md`);
183
+ writtenFiles.push(`${LOCAL_DIR}/compiled/skills/${skill.name}/SKILL.md`);
183
184
 
184
185
  for (const ref of compiled.references) {
185
186
  if (!ref.content) continue;
186
187
  const refFull = join(outDir, ref.path);
187
188
  await mkdir(dirname(refFull), { recursive: true });
188
189
  await writeFile(refFull, ref.content, 'utf8');
189
- writtenFiles.push(`${STATE_DIR}/compiled/skills/${skill.name}/${ref.path}`);
190
+ writtenFiles.push(`${LOCAL_DIR}/compiled/skills/${skill.name}/${ref.path}`);
190
191
  }
191
192
 
192
193
  await mkdir(dirname(proposalPath), { recursive: true });
193
194
  await writeFile(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`, 'utf8');
194
- writtenFiles.push(`${STATE_DIR}/proposals/skills/${skill.name}.json`);
195
+ writtenFiles.push(`${LOCAL_DIR}/proposals/skills/${skill.name}.json`);
195
196
  }
196
197
 
197
198
  if (isJson(flags)) {
@@ -222,7 +223,6 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
222
223
  : null,
223
224
  proposal,
224
225
  },
225
- { maxBytes: flags.maxBytes ?? 0 },
226
226
  );
227
227
  return EXIT.OK;
228
228
  }
@@ -245,7 +245,7 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
245
245
  }
246
246
  }
247
247
 
248
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
248
+ emit(lines.join('\n'));
249
249
  if (!mode.apply) {
250
250
  note(`note dry-run nothing written; apply with: ${CLI} skills compile ${skill.name} --yes`);
251
251
  }
@@ -38,6 +38,7 @@ 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';
41
42
 
42
43
  /**
43
44
  * Where skills live in this repo. The estate already has a convention - a
@@ -82,13 +83,25 @@ export async function installCommand(args, flags) {
82
83
  const ws = await resolveWorkspace(flags);
83
84
  if (ws.error) return fail(`workspace config: ${ws.error}`, EXIT.CONFIG, 'fix the JSON, or remove the file');
84
85
 
85
- const root = flags.root ?? process.cwd();
86
+ const root = commandRoot(flags);
86
87
  const repo = await fingerprint(root);
87
88
  const pick = await chooseSource(skill, repo.signals, flags, { root, workspaceRoot: ws.root });
88
89
  if (pick.error) return fail(pick.error, EXIT.USAGE, pick.remedy);
89
90
 
90
91
  const found = await locateSource(pick.source, { workspaceRoot: ws.root, root });
91
92
  if (!found.path) {
93
+ // A repo-side candidate with the wrong size is a third reason, and it needs
94
+ // its own wording: the file is on the reader's disk, so "this repo does not
95
+ // hold it" would send them looking for a file that is already there. The
96
+ // variant path calls the same failure an impostor (chooseSource).
97
+ const impostor = found.candidates.find((c) => c.how === 'repo');
98
+ if (impostor) {
99
+ return fail(
100
+ `${skill.name}: ${root}/${pick.source.path} exists but is not the declared body (${pick.source.bytes} bytes declared)`,
101
+ EXIT.USAGE,
102
+ `run: ${CLI} graph lint --workspace <path> to see what the registry expects, or delete the local copy`,
103
+ );
104
+ }
92
105
  // Two reasons land here and they need different wording: no workspace at all,
93
106
  // or a workspace that should hold this body and does not. An install that
94
107
  // cannot read a body has failed, so this is never a degraded mode - the
@@ -198,7 +211,6 @@ export async function installCommand(args, flags) {
198
211
  ]
199
212
  .filter((l) => l !== null)
200
213
  .join('\n'),
201
- { maxBytes: flags.maxBytes ?? 0 },
202
214
  );
203
215
  }
204
216