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
@@ -117,7 +117,9 @@ async function defaultRegistry(flags) {
117
117
  * failure to resolve is a WARNING, not 38 `ENOENT` lines (ADR-014).
118
118
  *
119
119
  * Memoised per the inputs that decide the answer: the `--workspace` flag, the
120
- * `SIMT_DEV_WORKSPACE` environment and the user-config location. A single
120
+ * `SIMT_DEV_WORKSPACE` environment, the user-config location, and the current
121
+ * directory, whose manifest declares the workspace the variable may point into
122
+ * (ADR-119). A single
121
123
  * first-call-wins promise served one caller's workspace to every later caller with
122
124
  * different flags (Q14) — `skills_index({workspace: ws})` followed by
123
125
  * `skills_index({})` answered `ws` twice. Callers with the same inputs still share
@@ -129,6 +131,7 @@ export function workspace(flags = {}) {
129
131
  flags?.workspace ?? null,
130
132
  process.env.SIMT_DEV_WORKSPACE ?? null,
131
133
  process.env.XDG_CONFIG_HOME ?? null,
134
+ process.cwd(),
132
135
  ]);
133
136
  if (!workspacePromises.has(key)) workspacePromises.set(key, resolveWorkspace(flags));
134
137
  return workspacePromises.get(key);
@@ -154,10 +157,15 @@ export async function loadRegistry(path = null, flags = {}) {
154
157
  // errors are facts about ABSENCE - no registry here, or more than one and the reader must
155
158
  // pick - and are reported as such. A named path that is missing is a different fact: the
156
159
  // reader pointed at a file that is not there, and it names that path.
157
- let where = path;
160
+ //
161
+ // Resolution order for the registry itself (review 0003 Q37): positional path,
162
+ // then `--registry`, then `SIMT_REGISTRY`, then the default tiers above.
163
+ const explicit =
164
+ path ?? flags?.registry ?? process.env.SIMT_REGISTRY ?? process.env.SIMT_DEV_REGISTRY ?? null;
165
+ let where = explicit;
158
166
  let resolveErrors = null;
159
167
  let resolvedWorkspace = null;
160
- if (path === null) {
168
+ if (explicit === null) {
161
169
  const found = await defaultRegistry(flags);
162
170
  where = found.where;
163
171
  resolveErrors = found.errors;
@@ -181,7 +189,7 @@ export async function loadRegistry(path = null, flags = {}) {
181
189
  registry = JSON.parse(await readFile(where, 'utf8'));
182
190
  } catch (error) {
183
191
  const message =
184
- path === null && error.code === 'ENOENT'
192
+ explicit === null && error.code === 'ENOENT'
185
193
  ? `no skill registry at ${where}; create one there, or name it explicitly`
186
194
  : `${where}: unreadable or invalid JSON (${error.message})`;
187
195
  return {
@@ -260,6 +268,11 @@ export async function loadRegistry(path = null, flags = {}) {
260
268
  * broken registry rather than a wrong pointer, which is exactly backwards.
261
269
  */
262
270
  function holdsAnyRepo(registry, root) {
271
+ // Every repo the registry names, vendored or not: the question is whether this
272
+ // directory is a workspace at all. Filtering out vendored sources made the check
273
+ // unsatisfiable the moment the last non-vendored skill left the registry - an
274
+ // all-vendored registry names repos just the same, and a home folder holds none
275
+ // of them.
263
276
  const named = new Set(
264
277
  (registry.skills ?? []).flatMap((s) => (s.sources ?? []).map((x) => x.repo)).filter(Boolean),
265
278
  );
@@ -282,14 +295,20 @@ export function lintRegistry(registry) {
282
295
  const threshold = registry.deferredThreshold ?? 20000;
283
296
 
284
297
  // --- shape -----------------------------------------------------------------
298
+ // Every check below assumes arrays of objects. A registry that fails the shape
299
+ // is already fully described by the shape error; continuing past it only
300
+ // trades one true error for a crash (`for..of` over `undefined`) or a pile of
301
+ // secondary noise, so the lint stops here (review 0003 Q35).
285
302
  if (!Array.isArray(registry.capabilities) || capabilities.size === 0) {
286
303
  err('capabilities: missing or empty');
287
304
  }
288
305
  if (!Array.isArray(registry.skills) || skills.length === 0) {
289
306
  err('skills: missing or empty');
307
+ }
308
+ if (errors.length > 0) {
290
309
  return { sync: errors, warnings, index };
291
310
  }
292
- for (const c of registry.capabilities) {
311
+ for (const c of registry.capabilities ?? []) {
293
312
  if (!c.id || !c.summary) err(`capability ${c.id ?? '<no id>'}: needs id and summary`);
294
313
  }
295
314
 
@@ -449,22 +468,22 @@ function checkRequires(at, skill, index, err) {
449
468
  if (!index.has(dep)) err(`${at}: requires unknown skill "${dep}"`);
450
469
  if (dep === skill.name) err(`${at}: requires itself`);
451
470
  }
452
- // Cycle detection over `requires`. Depth is bounded by node count, so an
453
- // accidental cycle terminates rather than hanging the lint.
454
- const seen = new Set([skill.name]);
455
- let frontier = skill.requires ?? [];
456
- while (frontier.length > 0) {
457
- const next = [];
458
- for (const dep of frontier) {
459
- if (seen.has(dep)) {
460
- err(`requires cycle: ${[...seen].join(' -> ')} -> ${dep}`);
471
+ // Cycle detection over `requires`, depth-first with a recursion stack. The old
472
+ // breadth-first walk shared one `seen` set across branches, so a diamond
473
+ // (a requires b and c; both require d) reported d's second visit as a cycle.
474
+ // Only a dependency already on the current path is a cycle; a node reached
475
+ // through two branches is sharing, not looping (review 0003 Q35).
476
+ const visit = (name, path) => {
477
+ const stack = [...path, name];
478
+ for (const dep of index.get(name)?.requires ?? []) {
479
+ if (stack.includes(dep)) {
480
+ err(`requires cycle: ${[...stack, dep].join(' -> ')}`);
461
481
  return;
462
482
  }
463
- seen.add(dep);
464
- next.push(...(index.get(dep)?.requires ?? []));
483
+ visit(dep, stack);
465
484
  }
466
- frontier = next;
467
- }
485
+ };
486
+ visit(skill.name, []);
468
487
  }
469
488
 
470
489
  function checkAlias(at, skill, index, err) {
@@ -570,6 +589,10 @@ async function lintSourcesOnDisk(registry, workspaceRoot) {
570
589
  const warnings = [];
571
590
 
572
591
  for (const skill of registry.skills ?? []) {
592
+ // A vendored skill ships with the package; its source is the package root,
593
+ // not a workspace member. Verifying it against the workspace filesystem is
594
+ // the wrong check (the path will not exist there by design).
595
+ if (skill.vendored) continue;
573
596
  for (const source of skill.sources ?? []) {
574
597
  const at = `${skill.name}: ${source.repo}/${source.path}`;
575
598
  if (!source.repo || !source.path) continue;
@@ -644,8 +667,7 @@ export async function measureSkill(full) {
644
667
  hasEntry = false;
645
668
  }
646
669
  if (!hasEntry) {
647
- const md = files === 1 ? '' : '';
648
- throw Object.assign(new Error(`${full} has no SKILL.md entry point${md}`), { code: 'NO_SKILL_MD' });
670
+ throw Object.assign(new Error(`${full} has no SKILL.md entry point`), { code: 'NO_SKILL_MD' });
649
671
  }
650
672
 
651
673
  return { bytes, files, flat: false };
@@ -21,7 +21,9 @@
21
21
  import { constants as fsConstants } from 'node:fs';
22
22
  import { access, readdir, readFile, stat } from 'node:fs/promises';
23
23
  import { join } from 'node:path';
24
- import { ECOSYSTEMS, manifestPath } from '../projection/manifest.mjs';
24
+ import { ECOSYSTEMS } from '../projection/manifest.mjs';
25
+ import { loadManifest } from '../manifest/schema.mjs';
26
+ import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
25
27
 
26
28
  /** Binaries the shared dispatcher shells out to directly, rather than via a script. */
27
29
  const BIN_DIR = join('node_modules', '.bin');
@@ -104,10 +106,22 @@ async function probeTool(root, eco) {
104
106
  export async function readCapability(root) {
105
107
  const capability = { scripts: {}, binaries: [], commands: {}, tools: [], runner: null, error: null };
106
108
 
109
+ // Q31: a package.json names a Node repo, but not which manager it installs
110
+ // with. The lockfile on disk wins over the `packageManager` field (the field
111
+ // can disagree; `checkToolchain` reports that), and the field wins over the
112
+ // npm default — so a `packageManager: pnpm@9` repo resolves pnpm before its
113
+ // first install, and every consumer below reads the same answer.
114
+ let packageManager = null;
107
115
  try {
108
116
  const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
109
117
  capability.scripts = manifest.scripts ?? {};
110
- capability.runner = 'npm';
118
+ packageManager = typeof manifest.packageManager === 'string' ? manifest.packageManager : null;
119
+ const lockfiles = [];
120
+ for (const file of ['pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb', 'package-lock.json']) {
121
+ if (await fileExists(join(root, file))) lockfiles.push(file);
122
+ }
123
+ capability.runner = resolveNodeRunner({ lockfiles, packageManager }).id;
124
+ capability.packageManager = packageManager;
111
125
  } catch (error) {
112
126
  // A repo with no manifest is a normal shape — a Python or Gradle repo has no
113
127
  // package.json at all — so this is reported, never thrown. The gate filter treats
@@ -157,8 +171,7 @@ export async function readCapability(root) {
157
171
  // so the only evidence that can prove it is the declaration itself. A repo with no
158
172
  // manifest simply has no declared commands.
159
173
  try {
160
- const manifest = JSON.parse(await readFile(await manifestPath(root), 'utf8'));
161
- capability.commands = declaredCommands(manifest);
174
+ capability.commands = declaredCommands((await loadManifest(root)).manifest);
162
175
  } catch {
163
176
  capability.commands = {};
164
177
  }