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
@@ -0,0 +1,277 @@
1
+ // One renderer per harness for the agent file, and the plan the install writes.
2
+ //
3
+ // Six harnesses read a subagent and no two agree on the file. Five write a
4
+ // markdown file with YAML frontmatter and a body that becomes the system prompt;
5
+ // Codex writes a TOML file whose `developer_instructions` holds the same body.
6
+ // The path, the extension, the frontmatter keys and the document shape are all
7
+ // different, and none of them is the same fact. So this module owns the
8
+ // differences and the command owns none of them:
9
+ //
10
+ // opencode .opencode/agents/simt-agent.md markdown description
11
+ // claude .claude/agents/simt-agent.md markdown name, description
12
+ // cursor .cursor/agents/simt-agent.md markdown name, description
13
+ // gemini .gemini/agents/simt-agent.md markdown name, description
14
+ // codex .codex/agents/simt-agent.toml TOML name, description, developer_instructions
15
+ // antigravity .agents/agents/simt-agent.md markdown name, description
16
+ //
17
+ // The source of truth is ONE file in this repository, `agents/simt-agent.md`:
18
+ // neutral frontmatter plus a harness-agnostic body. The renderer strips that
19
+ // frontmatter and re-emits the keys each harness documents, so the body — the
20
+ // part that says how to act — is written once and cannot drift between
21
+ // harnesses. An agent is a file the tool owns whole, not a config entry to
22
+ // merge, so `rendered` compares bytes and reports created, unchanged or updated.
23
+
24
+ import { mkdir, readFile } from 'node:fs/promises';
25
+ import { dirname, join } from 'node:path';
26
+
27
+ import { PACKAGE_ROOT } from '../workspace.mjs';
28
+ import { splitFrontmatter } from '../memory/decide.mjs';
29
+ import { LOADERS } from '../projection/loaders.mjs';
30
+ import { journaledWriteFile } from '../journal.mjs';
31
+
32
+ /** The one agent this tool ships, and the filename stem every harness shares. */
33
+ const AGENT_FILE = 'simt-agent.md';
34
+ export const AGENT_STEM = 'simt-agent';
35
+
36
+ /** The companion skill: the command reference the agent reads on demand. */
37
+ const SKILL_SOURCE = join('skills', 'simt-commands', 'SKILL.md');
38
+ export const SKILL_STEM = 'simt-commands';
39
+
40
+ /**
41
+ * The per-harness differences. `frontmatter` returns the ordered [key, value]
42
+ * pairs a markdown harness documents; Codex has none because its shape is TOML,
43
+ * not frontmatter. The body is identical everywhere and is not a difference.
44
+ */
45
+ const ADAPTERS = {
46
+ opencode: {
47
+ id: 'opencode',
48
+ format: 'md',
49
+ frontmatter: (agent) => [
50
+ ['description', agent.description],
51
+ ],
52
+ },
53
+ claude: {
54
+ id: 'claude',
55
+ format: 'md',
56
+ frontmatter: (agent) => [
57
+ ['name', agent.name],
58
+ ['description', agent.description],
59
+ ],
60
+ },
61
+ cursor: {
62
+ id: 'cursor',
63
+ format: 'md',
64
+ frontmatter: (agent) => [
65
+ ['name', agent.name],
66
+ ['description', agent.description],
67
+ ],
68
+ },
69
+ gemini: {
70
+ id: 'gemini',
71
+ format: 'md',
72
+ frontmatter: (agent) => [
73
+ ['name', agent.name],
74
+ ['description', agent.description],
75
+ ],
76
+ },
77
+ codex: {
78
+ id: 'codex',
79
+ format: 'toml',
80
+ },
81
+ antigravity: {
82
+ id: 'antigravity',
83
+ format: 'md',
84
+ frontmatter: (agent) => [
85
+ ['name', agent.name],
86
+ ['description', agent.description],
87
+ ],
88
+ },
89
+ };
90
+
91
+ /** The reader-table row for a harness's agents directory, or null when none is documented. */
92
+ function agentInfo(id) {
93
+ const row = LOADERS.find((l) => l.id === id);
94
+ return row?.agents ?? null;
95
+ }
96
+
97
+ /**
98
+ * The path a harness reads the agent from, sourced from the reader table rather
99
+ * than spelled here. Null for a harness that documents no agents directory.
100
+ */
101
+ export function agentPath(id) {
102
+ const info = agentInfo(id);
103
+ return info ? join(info.dir, `${AGENT_STEM}${info.ext}`) : null;
104
+ }
105
+
106
+ /**
107
+ * Every reader, with the agents path it documents and the note for the ones that
108
+ * do not. The command reports these so a harness this tool does not write is
109
+ * named rather than silent.
110
+ */
111
+ export function agentRows() {
112
+ return LOADERS.map((l) => ({
113
+ id: l.id,
114
+ label: l.label,
115
+ path: l.agents ? join(l.agents.dir, `${AGENT_STEM}${l.agents.ext}`) : null,
116
+ note: l.agents ? l.agents.verified : (l.agentsNote ?? 'no agents file is documented'),
117
+ }));
118
+ }
119
+
120
+ /** The adapters that both have a reader-table path and a renderer, in reader order. */
121
+ export function adapters() {
122
+ return agentRows()
123
+ .filter((r) => r.path !== null)
124
+ .map((r) => ADAPTERS[r.id])
125
+ .filter(Boolean);
126
+ }
127
+
128
+ /** One adapter by id, or null when the harness has no path or no renderer. */
129
+ export function adapter(id) {
130
+ return ADAPTERS[id] && agentInfo(id) !== null ? ADAPTERS[id] : null;
131
+ }
132
+
133
+ /** Parse the neutral frontmatter of the source file: `key: value` lines. */
134
+ function parseSimpleFront(front) {
135
+ const out = {};
136
+ for (const line of front.split('\n')) {
137
+ const idx = line.indexOf(':');
138
+ if (idx < 0) continue;
139
+ const key = line.slice(0, idx).trim();
140
+ const val = line.slice(idx + 1).trim();
141
+ if (key) out[key] = val;
142
+ }
143
+ return out;
144
+ }
145
+
146
+ /**
147
+ * The one agent, read from this repository's `agents/` directory. The body is
148
+ * the system prompt; `name` and `description` are re-emitted per harness.
149
+ */
150
+ export async function loadAgent() {
151
+ const raw = await readFile(join(PACKAGE_ROOT, 'agents', AGENT_FILE), 'utf8');
152
+ const { front, body } = splitFrontmatter(raw);
153
+ const meta = parseSimpleFront(front ?? '');
154
+ if (!meta.name || !meta.description) {
155
+ throw new Error(`${AGENT_FILE} must declare a name and a description in its frontmatter`);
156
+ }
157
+ return { name: meta.name, description: meta.description, body: body.trim() };
158
+ }
159
+
160
+ /** The companion skill file, read verbatim from this repository. */
161
+ async function loadSkill() {
162
+ return readFile(join(PACKAGE_ROOT, SKILL_SOURCE), 'utf8');
163
+ }
164
+
165
+ /**
166
+ * Where the skill lands in the receiving repo. The same heuristic `skills
167
+ * install` uses: respect an existing skills directory, default to `.agents/skills`.
168
+ */
169
+ async function skillDestPath(root) {
170
+ let entries = [];
171
+ try {
172
+ const { readdir } = await import('node:fs/promises');
173
+ entries = await readdir(root);
174
+ } catch { /* fresh repo, no entries */ }
175
+ let dir;
176
+ if (entries.includes('.agents')) dir = '.agents/skills';
177
+ else if (entries.includes('.cursor')) dir = '.cursor/skills';
178
+ else if (entries.includes('.claude')) dir = '.claude/skills';
179
+ else dir = '.agents/skills';
180
+ return join(dir, SKILL_STEM, 'SKILL.md');
181
+ }
182
+
183
+ /** A basic TOML string: double-quoted, with backslashes and quotes escaped. */
184
+ function tomlString(value) {
185
+ return '"' + String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
186
+ }
187
+
188
+ /** The markdown form: frontmatter the harness documents, a blank line, the body. */
189
+ function renderMarkdown(adapterRow, agent) {
190
+ const fm = adapterRow.frontmatter(agent).map(([k, v]) => `${k}: ${v}`).join('\n');
191
+ return `---\n${fm}\n---\n\n${agent.body}\n`;
192
+ }
193
+
194
+ /**
195
+ * The Codex form: a TOML file. The body goes in `developer_instructions` as a
196
+ * multi-line basic string; a literal `"""` in the body would end the string, so
197
+ * one of its quotes is escaped.
198
+ */
199
+ function renderToml(agent) {
200
+ const body = agent.body.replace(/"""/g, '\\"""');
201
+ return `name = ${tomlString(agent.name)}\ndescription = ${tomlString(agent.description)}\ndeveloper_instructions = """\n${body}\n"""\n`;
202
+ }
203
+
204
+ /**
205
+ * Render the agent for a harness and compare it to what is on disk. The tool
206
+ * owns the file whole, so the comparison is by bytes and the action is one of
207
+ * created, unchanged, updated.
208
+ */
209
+ export function rendered(adapterRow, agent, existing) {
210
+ const text = adapterRow.format === 'toml' ? renderToml(agent) : renderMarkdown(adapterRow, agent);
211
+ if (existing === null) return { text, action: 'created' };
212
+ if (existing === text) return { text, action: 'unchanged' };
213
+ return { text, action: 'updated' };
214
+ }
215
+
216
+ /** Read a file as UTF-8, or null when it is not there. */
217
+ async function readIfExists(path) {
218
+ try {
219
+ return await readFile(path, 'utf8');
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * The agent files a repository should carry, one per harness that documents an
227
+ * agents directory, plus the companion skill file. Reads each existing file and
228
+ * renders what install would write, so a dry run and a run describe the same set
229
+ * of files. `harness` narrows the agent files to one; the skill is always included.
230
+ */
231
+ export async function planAgents(root, { harness } = {}) {
232
+ const agent = await loadAgent();
233
+ const rows = harness === undefined ? adapters() : adapters().filter((a) => a.id === harness);
234
+ const plan = [];
235
+ for (const row of rows) {
236
+ const path = agentPath(row.id);
237
+ const existing = await readIfExists(join(root, path));
238
+ const result = rendered(row, agent, existing);
239
+ plan.push({ id: row.id, path, action: result.action, text: result.text });
240
+ }
241
+ // The companion skill: one file, not per-harness.
242
+ const skillText = await loadSkill();
243
+ const skillPath = await skillDestPath(root);
244
+ const skillExisting = await readIfExists(join(root, skillPath));
245
+ const skillAction = skillExisting === null ? 'created' : skillExisting === skillText ? 'unchanged' : 'updated';
246
+ plan.push({ id: 'skill', path: skillPath, action: skillAction, text: skillText });
247
+ return plan;
248
+ }
249
+
250
+ /**
251
+ * Write the agent files a plan names, journaled so `undo` restores the run.
252
+ * Skips files that are already unchanged. Returns the paths written.
253
+ */
254
+ export async function writeAgentPlan(root, journal, plan) {
255
+ const written = [];
256
+ for (const p of plan.filter((x) => x.action !== 'unchanged')) {
257
+ await mkdir(join(root, dirname(p.path)), { recursive: true });
258
+ await journaledWriteFile(root, journal, join(root, p.path), p.text);
259
+ written.push(p.path);
260
+ }
261
+ return written;
262
+ }
263
+
264
+ /**
265
+ * Write just the skill file so `installedSkills` can find it before `plan()`
266
+ * computes the region. Called by `init` before the region is rendered.
267
+ */
268
+ export async function writeSkillFile(root, journal) {
269
+ const text = await loadSkill();
270
+ const path = await skillDestPath(root);
271
+ const full = join(root, path);
272
+ const existing = await readIfExists(full);
273
+ if (existing === text) return null;
274
+ await mkdir(dirname(full), { recursive: true });
275
+ await journaledWriteFile(root, journal, full, text);
276
+ return path;
277
+ }
package/src/api.mjs ADDED
@@ -0,0 +1,29 @@
1
+ // The library surface: the read commands as data (review 0003 Q63).
2
+ //
3
+ // Every function here is the one the CLI and the MCP tools already call. Nothing in
4
+ // this file renders: a caller that wants text, JSON or an MCP result renders these
5
+ // results itself, which is the whole point - a UI, a harness and a terminal asking the
6
+ // same question must not get three answers, and they cannot disagree if they are
7
+ // reading one function's return value.
8
+ //
9
+ // This is the read half only. The commands that write (`init`, `sync`, `hooks
10
+ // install`, `skills install`, `upgrade`) still print their plans from the command
11
+ // module, because their output is a diff a person reviews before it is applied
12
+ // (ADR-001). They migrate in the next slice, when `run(ctx, input)` exists to carry
13
+ // a plan back to a caller that can show it.
14
+ //
15
+ // The exports map in package.json points `import 'simt-dev'` here, and keeps
16
+ // `./src/*` because the plugin runtime and the harness shims import deep paths.
17
+ // Removing that subpath would break them to make the surface look tidier.
18
+
19
+ export { checkRepo, checkTargets, checkTarget, frozenFindings } from './commands/sync.mjs';
20
+ export { diagnose, serialisedFindings } from './commands/doctor.mjs';
21
+ export { rulesCatalogue, rulesWhyFacts } from './commands/rules.mjs';
22
+ export { skillsBriefFacts, skillsFindFacts, skillsShowFacts, skillsWhyFacts } from './commands/skills.mjs';
23
+ export { assembleBrief } from './brief/assemble.mjs';
24
+ export { loadRegistry } from './graph/load.mjs';
25
+ export { discover } from './compare/discover.mjs';
26
+ export { buildRows, resolveConcurrency, summarise } from './compare/row.mjs';
27
+ export { planNext } from './plan/index.mjs';
28
+ export { buildPrBody } from './prbody.mjs';
29
+ export { federationAccess, readFederation } from './federation/config.mjs';
package/src/argv.mjs CHANGED
@@ -26,6 +26,11 @@ import { EXIT } from './output/index.mjs';
26
26
  // mean something, which it could not while three accepted flags had no description.
27
27
 
28
28
  export const BOOL_FLAGS = new Set([
29
+ // bench ablate: compare across harness, model and experiment groups. The command
30
+ // read `flags.crossGroup` while the parser accepted no such flag, so the override
31
+ // existed only in the error message that told the reader to pass it (review 0003
32
+ // Q62, found by the spec-vs-parser drift test).
33
+ 'cross-group',
29
34
  'json',
30
35
  'all',
31
36
  'dry-run',
@@ -89,6 +94,10 @@ export const BOOL_FLAGS = new Set([
89
94
  // bench.json marked unmeasured, and a lint that failed it on arrival would make
90
95
  // every new pack red before its author wrote the rule (plan 0003 R45).
91
96
  'require-measured',
97
+ // bench mine: restore the old "reflect exactly this history" semantics by
98
+ // deleting task files the current history does not reproduce. Off by default
99
+ // because mining is append-only (review 0003 Q48).
100
+ 'replace',
92
101
  // distill: propose the summary from the configured local chat model instead of
93
102
  // the deterministic outline. Off by default: no model in the default path (P1),
94
103
  // and a model that is not asked for should not be opened against (ADR-090).
@@ -119,6 +128,14 @@ export const VALUE_FLAGS = new Set([
119
128
  'task',
120
129
  'workspace',
121
130
  'root',
131
+ // skills/graph: which registry to read. A value rather than a boolean because
132
+ // "a registry" without a path would have to guess, and the guess is the
133
+ // defect Q37 exists to remove (review 0003).
134
+ 'registry',
135
+ // compare: how many repository rows to build at once (review 0003 Q38).
136
+ // A count, not a switch, because "parallel" without a cap is the burst this
137
+ // flag exists to prevent.
138
+ 'concurrency',
122
139
  // skills install: which body of a multi-variant skill, and which source when the
123
140
  // bytes alone do not identify one.
124
141
  'variant',
@@ -158,6 +175,9 @@ export const VALUE_FLAGS = new Set([
158
175
  // lease: how long the claim lasts, in seconds. A value, because "a lease is short"
159
176
  // is not a number any reader can act on, and an expiry nobody chose is an expiry
160
177
  // nobody can plan around (plan 0003 R54, ADR-101).
178
+ // lessons capture: how long a captured candidate stays eligible. A value, because a
179
+ // default would be a deadline nobody chose (review 0003 Q62, found by the drift test).
180
+ 'expires',
161
181
  'ttl',
162
182
  // map: outline (paths), names (exported symbols) or signatures (the line).
163
183
  // A boolean would not say which of the three the caller wanted.
@@ -273,9 +293,23 @@ export function parse(argv) {
273
293
  flags.reps = n;
274
294
  } else {
275
295
  const key = camel(raw);
276
- flags[key] = REPEATABLE_FLAGS.has(raw)
277
- ? [...(Array.isArray(flags[key]) ? flags[key] : []), value]
278
- : value;
296
+ if (REPEATABLE_FLAGS.has(raw)) {
297
+ flags[key] = [...(Array.isArray(flags[key]) ? flags[key] : []), value];
298
+ } else if (flags[key] !== undefined) {
299
+ // `--root a --root b` kept `b`, so half of what the caller asked for vanished
300
+ // with no error and no way to tell which half. Only a flag declared repeatable
301
+ // accumulates (review 0003 Q62); a second value for any other is a usage error.
302
+ return {
303
+ ok: false,
304
+ error: usage(
305
+ `--${raw} was given twice; it takes one value (repeatable: ${[...REPEATABLE_FLAGS]
306
+ .map((name) => `--${name}`)
307
+ .join(', ')})`,
308
+ ),
309
+ };
310
+ } else {
311
+ flags[key] = value;
312
+ }
279
313
  }
280
314
  continue;
281
315
  }
@@ -11,6 +11,7 @@ import { mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises';
11
11
  import { dirname, join, resolve } from 'node:path';
12
12
 
13
13
  import { MANIFEST_FILE, STATE_DIR } from '../name.mjs';
14
+ import { LOCAL_DIR } from '../state.mjs';
14
15
  import { readManifest } from '../projection/manifest.mjs';
15
16
  import { inject, parse as parseRegion } from '../projection/markers.mjs';
16
17
  import { loadMdcRules, renderMdc, ruleName } from '../projection/mdc.mjs';
@@ -23,6 +24,7 @@ import {
23
24
  DEFAULT_BOOTSTRAP_SEED,
24
25
  compareArms,
25
26
  evaluateRunRecord,
27
+ holmAdjust,
26
28
  loadPrices,
27
29
  loadRunRecords,
28
30
  } from './report.mjs';
@@ -340,11 +342,14 @@ export async function ablateSectionInWorkspace(workDir, ablate) {
340
342
 
341
343
  /**
342
344
  * Decide whether a canary comparison warrants escalating the section arm to the full task suite.
345
+ *
346
+ * `pValue` overrides the comparison's raw p-values when the caller supplies a
347
+ * multiplicity-adjusted value (Holm over the family of sections tested in one run).
343
348
  */
344
- export function shouldEscalateCanary(comparison, { alpha = CANARY_ALPHA } = {}) {
349
+ export function shouldEscalateCanary(comparison, { alpha = CANARY_ALPHA, pValue } = {}) {
345
350
  if (!comparison || !comparison.pairedTasks) return false;
346
- if (comparison.mcnemar.pValue < alpha) return true;
347
- if (comparison.wilcoxon.pValue < alpha) return true;
351
+ const p = pValue ?? Math.min(comparison.rTest.pValue, comparison.wilcoxon.pValue);
352
+ if (p < alpha) return true;
348
353
  if (comparison.ci95.R[0] > 0 || comparison.ci95.R[1] < 0) return true;
349
354
  if (Math.abs(comparison.deltas.R) >= 0.2) return true;
350
355
  return false;
@@ -469,15 +474,27 @@ export async function ablateBench(
469
474
  const canaryEvaluated = canaryRecords.map((rec) => evaluateRunRecord(rec, prices));
470
475
  const currentCanary = canaryEvaluated.filter((r) => r.arm === 'current');
471
476
 
477
+ const canaryComparisons = [];
472
478
  for (const sec of sections) {
473
479
  const armCanary = canaryEvaluated.filter((r) => r.arm === sec.arm);
474
480
  if (currentCanary.length > 0 && armCanary.length > 0) {
475
- const cmp = compareArms(sec.arm, armCanary, 'current', currentCanary, { samples, seed });
476
- if (shouldEscalateCanary(cmp)) {
477
- escalatedArms.add(sec.arm);
478
- }
481
+ canaryComparisons.push({
482
+ sec,
483
+ cmp: compareArms(sec.arm, armCanary, 'current', currentCanary, { samples, seed }),
484
+ });
479
485
  }
480
486
  }
487
+ // Family = every section tested in this one run: testing ~N sections at α 0.05
488
+ // each inflates the false-positive rate, so the p-values are Holm-adjusted
489
+ // before the escalation decision (review 0003 Q46).
490
+ const adjusted = holmAdjust(
491
+ canaryComparisons.map(({ cmp }) => Math.min(cmp.rTest.pValue, cmp.wilcoxon.pValue)),
492
+ );
493
+ canaryComparisons.forEach(({ sec, cmp }, i) => {
494
+ if (shouldEscalateCanary(cmp, { pValue: adjusted[i] })) {
495
+ escalatedArms.add(sec.arm);
496
+ }
497
+ });
481
498
 
482
499
  if (escalatedArms.size > 0) {
483
500
  await execRunBench(resolvedRoot, {
@@ -529,7 +546,7 @@ export async function ablateBench(
529
546
  dC: [0, 0],
530
547
  valuePerKb: [0, 0],
531
548
  },
532
- mcnemar: { statistic: 0, pValue: 1, b: 0, c: 0 },
549
+ rTest: { statistic: 0, pValue: 1, plus: 0, minus: 0 },
533
550
  wilcoxon: { statistic: 0, pValue: 1, wPlus: 0, wMinus: 0 },
534
551
  deletionCandidate: false,
535
552
  };
@@ -566,11 +583,11 @@ export async function ablateBench(
566
583
  dC: cmp.ci95.C,
567
584
  valuePerKb: vCi,
568
585
  },
569
- mcnemar: {
570
- statistic: cmp.mcnemar.statistic,
571
- pValue: cmp.mcnemar.pValue,
572
- b: cmp.mcnemar.b,
573
- c: cmp.mcnemar.c,
586
+ rTest: {
587
+ statistic: cmp.rTest.statistic,
588
+ pValue: cmp.rTest.pValue,
589
+ plus: cmp.rTest.plus,
590
+ minus: cmp.rTest.minus,
574
591
  },
575
592
  wilcoxon: {
576
593
  statistic: cmp.wilcoxon.statistic,
@@ -616,7 +633,7 @@ export async function ablateBench(
616
633
  const markdown = formatAblationMarkdown(result);
617
634
 
618
635
  if (apply) {
619
- const outPath = join(resolvedRoot, STATE_DIR, 'bench', 'ablation.json');
636
+ const outPath = join(resolvedRoot, LOCAL_DIR, 'bench', 'ablation.json');
620
637
  await mkdir(dirname(outPath), { recursive: true });
621
638
  await writeFile(outPath, `${JSON.stringify({ schemaVersion: 1, ...result }, null, 2)}\n`, 'utf8');
622
639
  }
@@ -10,6 +10,8 @@
10
10
  "args": ["-p", "{prompt}", "--output-format", "json", "--max-turns", "{turns}"],
11
11
  "modelArgs": ["--model", "{model}"],
12
12
  "format": "json",
13
+ "isolation": "env",
14
+ "env": ["ANTHROPIC_API_KEY"],
13
15
  "usageSource": "usage.*, total_cost_usd, num_turns",
14
16
  "fields": {
15
17
  "input": ["usage.input_tokens", "usage.input"],
@@ -30,6 +32,8 @@
30
32
  "args": ["exec", "--json", "{prompt}"],
31
33
  "modelArgs": ["--model", "{model}"],
32
34
  "format": "jsonl",
35
+ "isolation": "env",
36
+ "env": ["OPENAI_API_KEY"],
33
37
  "usageSource": "token events in JSONL",
34
38
  "fields": {
35
39
  "input": ["usage.input_tokens", "input_tokens", "usage.input"],
@@ -50,6 +54,8 @@
50
54
  "args": ["-p", "{prompt}", "--output-format", "json"],
51
55
  "modelArgs": ["--model", "{model}"],
52
56
  "format": "json",
57
+ "isolation": "env",
58
+ "env": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
53
59
  "usageSource": "stats block",
54
60
  "fields": {
55
61
  "input": ["stats.inputTokens", "stats.tokens.prompt", "stats.prompt_tokens", "usage.input_tokens"],
@@ -70,6 +76,8 @@
70
76
  "args": ["run", "--format", "json", "{prompt}"],
71
77
  "modelArgs": ["--model", "{model}"],
72
78
  "format": "jsonl",
79
+ "isolation": "env",
80
+ "env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
73
81
  "usageSource": "session export",
74
82
  "fields": {
75
83
  "input": ["tokens.input", "usage.input_tokens", "part.tokens.input"],
@@ -90,6 +98,8 @@
90
98
  "args": ["-p", "{prompt}", "--output-format", "json"],
91
99
  "modelArgs": ["--model", "{model}"],
92
100
  "format": "json",
101
+ "isolation": "env",
102
+ "env": ["CURSOR_API_KEY"],
93
103
  "usageSource": "result JSON",
94
104
  "fields": {
95
105
  "input": ["usage.input_tokens", "usage.input", "stats.inputTokens"],
@@ -110,6 +120,8 @@
110
120
  "args": ["--message", "{prompt}", "--yes-always", "--analytics-log", "{analyticsLog}"],
111
121
  "modelArgs": ["--model", "{model}"],
112
122
  "format": "analytics-log",
123
+ "isolation": "env",
124
+ "env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
113
125
  "usageSource": "--analytics-log file",
114
126
  "fields": {
115
127
  "input": ["prompt_tokens", "properties.prompt_tokens", "usage.input_tokens"],
@@ -130,6 +142,8 @@
130
142
  "args": [],
131
143
  "modelArgs": [],
132
144
  "format": "builtin",
145
+ "isolation": "none",
146
+ "env": [],
133
147
  "usageSource": "vault gold.patch",
134
148
  "fields": {}
135
149
  },
@@ -142,6 +156,8 @@
142
156
  "args": [],
143
157
  "modelArgs": [],
144
158
  "format": "builtin",
159
+ "isolation": "none",
160
+ "env": [],
145
161
  "usageSource": "none",
146
162
  "fields": {}
147
163
  }