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
@@ -15,10 +15,14 @@ import { createHash } from 'node:crypto';
15
15
  import { checkSkill, nameProblems, readFrontmatter } from './spec.mjs';
16
16
  import { matchesGlob } from '../resolve/resolve.mjs';
17
17
 
18
- export const BODY_TOKEN_BUDGET = 5000;
19
- export const BYTES_PER_TOKEN = 4;
20
- export const BODY_BYTE_BUDGET = BODY_TOKEN_BUDGET * BYTES_PER_TOKEN;
21
- export const BODY_LINE_BUDGET = 500;
18
+ import {
19
+ BODY_BYTE_BUDGET,
20
+ BODY_LINE_BUDGET,
21
+ BODY_TOKEN_BUDGET,
22
+ BYTES_PER_TOKEN,
23
+ } from './spec.mjs';
24
+
25
+ export { BODY_BYTE_BUDGET, BODY_LINE_BUDGET, BODY_TOKEN_BUDGET, BYTES_PER_TOKEN };
22
26
 
23
27
  const TARGET_MAIN_BYTES = 12_000;
24
28
  const TARGET_MAIN_LINES = 350;
@@ -18,7 +18,8 @@ import { existsSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
 
20
20
  import { KNOWN_TAGS } from '../resolve/fingerprint.mjs';
21
- import { matchesGlob, syntheticPathFor, tokenize } from '../resolve/resolve.mjs';
21
+ import { matchesGlob, syntheticPathFor } from '../resolve/resolve.mjs';
22
+ import { tokenize } from '../text/tokens.mjs';
22
23
  import { PACKAGE_ROOT, resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
23
24
 
24
25
  /**
@@ -117,7 +118,9 @@ async function defaultRegistry(flags) {
117
118
  * failure to resolve is a WARNING, not 38 `ENOENT` lines (ADR-014).
118
119
  *
119
120
  * Memoised per the inputs that decide the answer: the `--workspace` flag, the
120
- * `SIMT_DEV_WORKSPACE` environment and the user-config location. A single
121
+ * `SIMT_DEV_WORKSPACE` environment, the user-config location, and the current
122
+ * directory, whose manifest declares the workspace the variable may point into
123
+ * (ADR-119). A single
121
124
  * first-call-wins promise served one caller's workspace to every later caller with
122
125
  * different flags (Q14) — `skills_index({workspace: ws})` followed by
123
126
  * `skills_index({})` answered `ws` twice. Callers with the same inputs still share
@@ -129,6 +132,7 @@ export function workspace(flags = {}) {
129
132
  flags?.workspace ?? null,
130
133
  process.env.SIMT_DEV_WORKSPACE ?? null,
131
134
  process.env.XDG_CONFIG_HOME ?? null,
135
+ process.cwd(),
132
136
  ]);
133
137
  if (!workspacePromises.has(key)) workspacePromises.set(key, resolveWorkspace(flags));
134
138
  return workspacePromises.get(key);
@@ -9,6 +9,8 @@
9
9
  // error the reader cannot act on is a papercut that trains people to skip the gate, so
10
10
  // every message names the rule it broke and the fix.
11
11
 
12
+ import { parseFrontmatter } from '../text/frontmatter.mjs';
13
+
12
14
  /** The one specification this knows, and the name `--spec` accepts. */
13
15
  export const SPEC = 'agentskills';
14
16
 
@@ -19,9 +21,10 @@ const SPEC_FIELDS = ['name', 'description', 'license', 'compatibility', 'metadat
19
21
 
20
22
  /** Tokens, not bytes, because the specification counts tokens and every number here is
21
23
  * the same four-bytes-per-token estimate the context meter uses. */
22
- const BODY_TOKEN_BUDGET = 5000;
23
- const BODY_LINE_BUDGET = 500;
24
- const BYTES_PER_TOKEN = 4;
24
+ export const BODY_TOKEN_BUDGET = 5000;
25
+ export const BODY_LINE_BUDGET = 500;
26
+ export const BYTES_PER_TOKEN = 4;
27
+ export const BODY_BYTE_BUDGET = BODY_TOKEN_BUDGET * BYTES_PER_TOKEN;
25
28
 
26
29
  /** The `name` rules, as one predicate with the rule it enforces. */
27
30
  const NAME_RULES = {
@@ -167,10 +170,9 @@ export function checkSkill({ directoryName, fileName = 'SKILL.md', text, at }) {
167
170
  export function readFrontmatter(text) {
168
171
  const meta = {};
169
172
  const present = [];
170
- const body = text.replace(/^/, '');
171
- const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(body);
172
- if (!match) return { meta, present, body };
173
- const lines = match[1].split(/\r?\n/);
173
+ const { front, body } = parseFrontmatter(text.replace(/^/, ''));
174
+ if (front === null) return { meta, present, body };
175
+ const lines = front.split(/\r?\n/);
174
176
 
175
177
  for (let i = 0; i < lines.length; i += 1) {
176
178
  const line = lines[i];
@@ -215,7 +217,7 @@ export function readFrontmatter(text) {
215
217
  }
216
218
  meta[key] = scalar(raw);
217
219
  }
218
- return { meta, present, body: body.slice(match[0].length) };
220
+ return { meta, present, body };
219
221
  }
220
222
 
221
223
  /** Strip the quoting and the trailing comment a scalar may carry. */
@@ -0,0 +1,143 @@
1
+ // Harness data records.
2
+ //
3
+ // ADR-013: content is data, the engine is code. A harness is a record under
4
+ // `harnesses/`, not a row in a table the engine edits. A new harness arrives as a new
5
+ // file; the engine — this loader and the views over it — never changes shape to make a
6
+ // product work. The surface row, the reader row, the hook dialect and the plugin layout
7
+ // of one harness land together in one file and cannot drift apart, which is the failure
8
+ // the five-module spread they used to occupy made routine (review 0003 Q71).
9
+ //
10
+ // Records are sparse: a facet is present only when the harness has one. DeepSeek has a
11
+ // reader row and nothing else; Copilot has a surface row, a reader row and a scoping key;
12
+ // Claude has all seven. Absence is a fact — a facet that is not there is one the vendor
13
+ // documents none of — so the views filter, they do not fill.
14
+ //
15
+ // The shipped records live in `harnesses/` at the package root. A repository may override
16
+ // them under `.simt/harnesses/` the way it overrides `ecosystems/`: a same-id record is a
17
+ // shallow merge over the shipped one (arrays replaced whole), a new id is appended, and a
18
+ // record that does not parse is skipped, never a crash.
19
+ //
20
+ // The package root is computed here rather than imported from workspace.mjs for the same
21
+ // reason `ecosystem/records.mjs` computes it: workspace.mjs is part of the import graph
22
+ // that builds these tables at evaluation time, and an import cycle would leave the binding
23
+ // in the temporal dead zone. This file is two levels below the root (src/harness/), so
24
+ // two levels up is the root — a fixed path, not a heuristic.
25
+
26
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
27
+ import { dirname, join, resolve } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
31
+ const SHIPPED_DIR = join(PACKAGE_ROOT, 'harnesses');
32
+
33
+ /**
34
+ * The records in force for a repository: the shipped set, with a repo-local
35
+ * `.simt/harnesses/` merged over it. `null` is the shipped set alone, which is what the
36
+ * const-iterating tests and every call without a root read.
37
+ */
38
+ export function loadHarnesses(root = null) {
39
+ const base = shipped();
40
+ if (!root) return base;
41
+ const localDir = join(root, '.simt', 'harnesses');
42
+ if (!existsSync(localDir)) return base;
43
+ const byId = new Map(base.map((r) => [r.id, r]));
44
+ for (const file of readdirSync(localDir)) {
45
+ if (!file.endsWith('.json')) continue;
46
+ const id = file.slice(0, -5);
47
+ const rec = parse(join(localDir, file));
48
+ if (!rec) continue;
49
+ // Same id: a shallow merge over the shipped record, so a local override that omits
50
+ // a field keeps the shipped one. New id: the record as-is.
51
+ byId.set(id, byId.has(id) ? { ...byId.get(id), ...rec } : rec);
52
+ }
53
+ return [...byId.values()];
54
+ }
55
+
56
+ /** The shipped records, read once per process and cached. */
57
+ function shipped() {
58
+ if (shippedCache) return shippedCache;
59
+ const map = new Map();
60
+ if (existsSync(SHIPPED_DIR)) readRecords(SHIPPED_DIR, map);
61
+ shippedCache = [...map.values()];
62
+ return shippedCache;
63
+ }
64
+
65
+ let shippedCache = null;
66
+
67
+ /** Read one directory of records into `into`, keyed by the file stem. */
68
+ function readRecords(dir, into) {
69
+ for (const file of readdirSync(dir)) {
70
+ if (!file.endsWith('.json')) continue;
71
+ const id = file.slice(0, -5);
72
+ const rec = parse(join(dir, file));
73
+ // The id must match the file stem: a mismatch is a record pointing at the wrong
74
+ // harness, and shipping it would make two names mean one record.
75
+ if (rec && rec.id === id) into.set(id, rec);
76
+ }
77
+ }
78
+
79
+ /** One record, or null when the file is absent, unreadable or does not parse. */
80
+ function parse(path) {
81
+ try {
82
+ const rec = JSON.parse(readFileSync(path, 'utf8'));
83
+ return rec && typeof rec === 'object' ? rec : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // The views. Each returns the rows the old module constant held, in the order
91
+ // the const-iterating tests pin: the records' `*Order` fields, not readdir's.
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /** The surface rows, in the order `project` iterates them (ADR-020's union order). */
95
+ export function harnessSurfaces(root = null) {
96
+ return loadHarnesses(root)
97
+ .filter((r) => r.surface)
98
+ .sort((a, b) => a.surfaceOrder - b.surfaceOrder)
99
+ .map((r) => r.surface);
100
+ }
101
+
102
+ /** The pointer rows, in their table order. */
103
+ export function harnessPointers(root = null) {
104
+ return loadHarnesses(root)
105
+ .filter((r) => r.pointer)
106
+ .sort((a, b) => a.pointerOrder - b.pointerOrder)
107
+ .map((r) => r.pointer);
108
+ }
109
+
110
+ /** The reader rows, in the order the reader table lists its harnesses. */
111
+ export function harnessLoaders(root = null) {
112
+ return loadHarnesses(root)
113
+ .filter((r) => r.loader)
114
+ .sort((a, b) => a.loaderOrder - b.loaderOrder)
115
+ .map((r) => r.loader);
116
+ }
117
+
118
+ /** Surface ids whose scoping key was read in the vendor's own documentation. */
119
+ export function harnessScopeKeys(root = null) {
120
+ return new Set(loadHarnesses(root).filter((r) => r.scopeKey === true).map((r) => r.id));
121
+ }
122
+
123
+ /** The hook dialect per harness, keyed by id: the data a config document and a decision are shaped by. */
124
+ export function harnessHooks(root = null) {
125
+ const out = {};
126
+ for (const r of loadHarnesses(root)) if (r.hooks) out[r.id] = r.hooks;
127
+ return out;
128
+ }
129
+
130
+ /** The payload extraction contract per harness, keyed by id: how a hook payload names its tool, its input and its writes. */
131
+ export function harnessPayloads(root = null) {
132
+ const out = {};
133
+ for (const r of loadHarnesses(root)) if (r.payload) out[r.id] = r.payload;
134
+ return out;
135
+ }
136
+
137
+ /** The plugin layouts, in the order the `simt plugin` help line names its kinds. */
138
+ export function harnessPlugins(root = null) {
139
+ return loadHarnesses(root)
140
+ .filter((r) => r.plugin)
141
+ .sort((a, b) => a.pluginOrder - b.pluginOrder)
142
+ .map((r) => ({ id: r.id, ...r.plugin }));
143
+ }
@@ -21,7 +21,8 @@
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 { ecosystemGateRows } from '../ecosystem/records.mjs';
25
+ import { loadManifest } from '../manifest/schema.mjs';
25
26
  import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
26
27
 
27
28
  /** Binaries the shared dispatcher shells out to directly, rather than via a script. */
@@ -81,7 +82,7 @@ async function executableFile(path) {
81
82
  /**
82
83
  * Can this machine run the ecosystem's program?
83
84
  *
84
- * Two kinds of evidence, matching the two kinds of tool in `ECOSYSTEMS`: a binary on
85
+ * Two kinds of evidence, matching the two kinds of tool in the ecosystem records: a binary on
85
86
  * PATH, probed directory by directory the way the shell would find it, and a file the
86
87
  * repository commits (the Maven wrapper), probed in place the way `gradlew` is probed.
87
88
  * Either answer that is not a positive YES is NO: a seed that guesses a toolchain is
@@ -157,7 +158,9 @@ export async function readCapability(root) {
157
158
  // file in the repo cannot: a Cargo.toml without cargo installed is an ecosystem that
158
159
  // is here but whose gates would all fail closed. The probe is per-row, because a row
159
160
  // that marks its tool `file` lives in the repository, not on PATH.
160
- for (const eco of ECOSYSTEMS) {
161
+ // Q70: the rows come from the records, and `root` lets a repository's own
162
+ // `.simt/ecosystems/` add a tool the shipped set does not probe.
163
+ for (const eco of ecosystemGateRows(root)) {
161
164
  if (await probeTool(root, eco)) capability.tools.push(eco.tool);
162
165
  }
163
166
 
@@ -170,8 +173,7 @@ export async function readCapability(root) {
170
173
  // so the only evidence that can prove it is the declaration itself. A repo with no
171
174
  // manifest simply has no declared commands.
172
175
  try {
173
- const manifest = JSON.parse(await readFile(await manifestPath(root), 'utf8'));
174
- capability.commands = declaredCommands(manifest);
176
+ capability.commands = declaredCommands((await loadManifest(root)).manifest);
175
177
  } catch {
176
178
  capability.commands = {};
177
179
  }