simt-dev 0.6.2 → 0.7.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.
- package/CHANGELOG.md +83 -1
- package/README.md +46 -17
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- 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
- 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
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- 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
- 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
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/README.md +53 -0
- package/package.json +4 -2
- package/rules/core.md +2 -2
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +10 -11
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +4 -3
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +238 -15
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/prompt.mjs +6 -2
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shell.mjs +523 -0
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/argv.mjs +62 -9
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/mine.mjs +48 -1
- package/src/bench/probes.mjs +5 -6
- package/src/bench/report.mjs +243 -80
- package/src/bench/run.mjs +12 -11
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +6 -0
- package/src/ci/workflow.mjs +16 -7
- package/src/commands/agent.mjs +206 -0
- package/src/commands/agenthook.mjs +133 -17
- package/src/commands/bench.mjs +2 -1
- package/src/commands/ci.mjs +28 -3
- package/src/commands/compare.mjs +8 -7
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +20 -6
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/evidence.mjs +13 -3
- package/src/commands/explain.mjs +52 -0
- package/src/commands/gate.mjs +85 -19
- package/src/commands/graph.mjs +18 -5
- package/src/commands/help.mjs +96 -26
- package/src/commands/hooks.mjs +258 -67
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/impact.mjs +4 -2
- package/src/commands/index.mjs +69 -102
- package/src/commands/init.mjs +239 -37
- package/src/commands/lease.mjs +304 -0
- package/src/commands/lessons.mjs +77 -9
- package/src/commands/map.mjs +33 -18
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +17 -2
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +4 -0
- package/src/commands/rules.mjs +219 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +329 -206
- package/src/commands/skillscompile.mjs +8 -7
- package/src/commands/skillsinstall.mjs +112 -30
- package/src/commands/slice.mjs +712 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +146 -36
- package/src/commands/tests.mjs +5 -2
- package/src/commands/undo.mjs +114 -0
- package/src/commands/upgrade.mjs +103 -9
- package/src/commands/why.mjs +10 -4
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -1
- package/src/completions.mjs +196 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +254 -0
- package/src/distill/distill.mjs +3 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +270 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +52 -24
- package/src/hooks/capability.mjs +14 -1
- package/src/hooks/dispatcher.mjs +441 -9
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +23 -3
- package/src/hooks/shim.mjs +12 -7
- package/src/identity.mjs +67 -14
- package/src/ignore.mjs +133 -0
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +372 -55
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +545 -0
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +195 -0
- package/src/ledger/index.mjs +91 -25
- package/src/mcp/resources.mjs +5 -3
- package/src/mcp/server.mjs +86 -13
- package/src/mcp/tools.mjs +340 -264
- package/src/memory/decide.mjs +98 -38
- package/src/memory/lessons.mjs +180 -33
- package/src/name.mjs +9 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +37 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +33 -8
- package/src/prbody.mjs +253 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +21 -14
- package/src/projection/manifest.mjs +82 -21
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +54 -3
- package/src/projection/shared.mjs +4 -1
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +69 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +5 -0
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +4 -2
- package/src/secrets.mjs +374 -0
- package/src/state.mjs +76 -0
- package/src/status.mjs +157 -0
- package/src/toolwords.mjs +5 -0
- package/src/tsconfig.mjs +39 -99
- package/templates/plan/tracker.json +1 -1
|
@@ -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/argv.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Anything unrecognised is an error, not a silent ignore: a typo'd gate name
|
|
11
11
|
// that is ignored is worse than a typo'd gate name that fails.
|
|
12
12
|
|
|
13
|
-
import { EXIT
|
|
13
|
+
import { EXIT } from './output/index.mjs';
|
|
14
14
|
|
|
15
15
|
// `--quiet`, `--semantic` and `--mermaid` were here and were read by nothing (review
|
|
16
16
|
// 0001 filed it; it survived every change to this file since). Each was a plan's
|
|
@@ -93,6 +93,18 @@ export const BOOL_FLAGS = new Set([
|
|
|
93
93
|
// the deterministic outline. Off by default: no model in the default path (P1),
|
|
94
94
|
// and a model that is not asked for should not be opened against (ADR-090).
|
|
95
95
|
'local',
|
|
96
|
+
// doctor: apply the remedies flagged safe (local, idempotent, journaled) and
|
|
97
|
+
// list the rest. Off by default because a repair, however safe, is still a
|
|
98
|
+
// write, and a command whose name is a diagnosis should not write by default.
|
|
99
|
+
'fix',
|
|
100
|
+
// status: print the one-line gate summary from the last check. Off by
|
|
101
|
+
// default because the only mode this command has is the line; bare `status`
|
|
102
|
+
// is a usage error naming it, the way `plan` without a subcommand is.
|
|
103
|
+
'line',
|
|
104
|
+
// every writer: print the plan as unified diffs. Off by default because a
|
|
105
|
+
// plan report is the compact answer and the diff is the verbose one; --diff
|
|
106
|
+
// never changes what the run writes or its exit code, only what it shows.
|
|
107
|
+
'diff',
|
|
96
108
|
]);
|
|
97
109
|
|
|
98
110
|
export const VALUE_FLAGS = new Set([
|
|
@@ -107,6 +119,14 @@ export const VALUE_FLAGS = new Set([
|
|
|
107
119
|
'task',
|
|
108
120
|
'workspace',
|
|
109
121
|
'root',
|
|
122
|
+
// skills/graph: which registry to read. A value rather than a boolean because
|
|
123
|
+
// "a registry" without a path would have to guess, and the guess is the
|
|
124
|
+
// defect Q37 exists to remove (review 0003).
|
|
125
|
+
'registry',
|
|
126
|
+
// compare: how many repository rows to build at once (review 0003 Q38).
|
|
127
|
+
// A count, not a switch, because "parallel" without a cap is the burst this
|
|
128
|
+
// flag exists to prevent.
|
|
129
|
+
'concurrency',
|
|
110
130
|
// skills install: which body of a multi-variant skill, and which source when the
|
|
111
131
|
// bytes alone do not identify one.
|
|
112
132
|
'variant',
|
|
@@ -138,6 +158,15 @@ export const VALUE_FLAGS = new Set([
|
|
|
138
158
|
// plan done: the test command that proves the slice. Plan 0003 R31.
|
|
139
159
|
'test',
|
|
140
160
|
'state',
|
|
161
|
+
// slice finish: the Conventional Commits type for the commit it makes. A value,
|
|
162
|
+
// not a boolean, because a row title cannot say whether the slice was a feat, a
|
|
163
|
+
// fix or a chore, and deriving it from the words would be a guess no reader of
|
|
164
|
+
// the message could argue with (plan 0003 R53).
|
|
165
|
+
'type',
|
|
166
|
+
// lease: how long the claim lasts, in seconds. A value, because "a lease is short"
|
|
167
|
+
// is not a number any reader can act on, and an expiry nobody chose is an expiry
|
|
168
|
+
// nobody can plan around (plan 0003 R54, ADR-101).
|
|
169
|
+
'ttl',
|
|
141
170
|
// map: outline (paths), names (exported symbols) or signatures (the line).
|
|
142
171
|
// A boolean would not say which of the three the caller wanted.
|
|
143
172
|
'level',
|
|
@@ -151,6 +180,10 @@ export const VALUE_FLAGS = new Set([
|
|
|
151
180
|
// "generate a package" with no destination would have to guess, and a guess about where
|
|
152
181
|
// a directory tree goes is the one failure that leaves files behind.
|
|
153
182
|
'out',
|
|
183
|
+
// pr-body: the ref the body diffs against. A value, because the fork point is a
|
|
184
|
+
// name only the author's branch knows, and the default (origin/main, then main)
|
|
185
|
+
// is a guess the report states out loud rather than a question it asks.
|
|
186
|
+
'base',
|
|
154
187
|
]);
|
|
155
188
|
|
|
156
189
|
/**
|
|
@@ -168,9 +201,29 @@ export const VALUE_FLAGS = new Set([
|
|
|
168
201
|
*/
|
|
169
202
|
export const REPEATABLE_FLAGS = new Set(['skill', 'arm', 'compare', 'supersedes']);
|
|
170
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Whether a raw argv asks for JSON, read before parsing succeeds.
|
|
206
|
+
*
|
|
207
|
+
* A parse failure is still a result the caller may need as JSON (Q11), but the
|
|
208
|
+
* parsed flags do not exist yet — so the dispatcher reads the spelling straight
|
|
209
|
+
* off argv. Only the exact `--json` spelling counts, and nothing past `--` does:
|
|
210
|
+
* a positional that happens to read `--json` is a path, not a flag.
|
|
211
|
+
*/
|
|
212
|
+
export function rawJsonFlag(argv) {
|
|
213
|
+
for (const arg of argv) {
|
|
214
|
+
if (arg === '--') break;
|
|
215
|
+
if (arg === '--json') return true;
|
|
216
|
+
if (arg === '--no-json') return false;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
171
221
|
/**
|
|
172
222
|
* @param {string[]} argv raw arguments, excluding node and the script path
|
|
173
|
-
* @returns {{ok: true, value: object} | {ok: false, code: number}}
|
|
223
|
+
* @returns {{ok: true, value: object} | {ok: false, error: {message: string, code: number}}}
|
|
224
|
+
*
|
|
225
|
+
* A failure is data, never output: reporting it is the caller's job, because only
|
|
226
|
+
* the caller knows whether `--json` was asked for (Q11). Nothing here prints.
|
|
174
227
|
*/
|
|
175
228
|
export function parse(argv) {
|
|
176
229
|
const flags = {};
|
|
@@ -196,14 +249,14 @@ export function parse(argv) {
|
|
|
196
249
|
if (raw.startsWith('no-')) {
|
|
197
250
|
const positive = raw.slice(3);
|
|
198
251
|
if (!BOOL_FLAGS.has(positive)) {
|
|
199
|
-
return { ok: false,
|
|
252
|
+
return { ok: false, error: usage(`unknown flag --${raw}`) };
|
|
200
253
|
}
|
|
201
254
|
flags[camel(positive)] = false;
|
|
202
255
|
continue;
|
|
203
256
|
}
|
|
204
257
|
if (BOOL_FLAGS.has(raw)) {
|
|
205
258
|
if (inlineValue !== undefined) {
|
|
206
|
-
return { ok: false,
|
|
259
|
+
return { ok: false, error: usage(`--${raw} takes no value`) };
|
|
207
260
|
}
|
|
208
261
|
flags[camel(raw)] = true;
|
|
209
262
|
continue;
|
|
@@ -211,19 +264,19 @@ export function parse(argv) {
|
|
|
211
264
|
if (VALUE_FLAGS.has(raw)) {
|
|
212
265
|
const value = inlineValue ?? argv[i + 1];
|
|
213
266
|
if (value === undefined || (inlineValue === undefined && value.startsWith('--'))) {
|
|
214
|
-
return { ok: false,
|
|
267
|
+
return { ok: false, error: usage(`--${raw} requires a value`) };
|
|
215
268
|
}
|
|
216
269
|
if (inlineValue === undefined) i += 1;
|
|
217
270
|
if (raw === 'budget' || raw === 'max-bytes') {
|
|
218
271
|
const n = Number(value);
|
|
219
272
|
if (!Number.isInteger(n) || n < 0) {
|
|
220
|
-
return { ok: false,
|
|
273
|
+
return { ok: false, error: usage(`--${raw} must be a non-negative integer, received ${value}`) };
|
|
221
274
|
}
|
|
222
275
|
flags[camel(raw)] = n;
|
|
223
276
|
} else if (raw === 'reps') {
|
|
224
277
|
const n = Number(value);
|
|
225
278
|
if (!Number.isInteger(n) || n < 1) {
|
|
226
|
-
return { ok: false,
|
|
279
|
+
return { ok: false, error: usage(`--reps must be a positive integer, received ${value}`) };
|
|
227
280
|
}
|
|
228
281
|
flags.reps = n;
|
|
229
282
|
} else {
|
|
@@ -234,7 +287,7 @@ export function parse(argv) {
|
|
|
234
287
|
}
|
|
235
288
|
continue;
|
|
236
289
|
}
|
|
237
|
-
return { ok: false,
|
|
290
|
+
return { ok: false, error: usage(`unknown flag --${raw}`) };
|
|
238
291
|
}
|
|
239
292
|
|
|
240
293
|
return { ok: true, value: { flags, positional } };
|
|
@@ -253,5 +306,5 @@ function camel(name) {
|
|
|
253
306
|
}
|
|
254
307
|
|
|
255
308
|
function usage(message) {
|
|
256
|
-
return
|
|
309
|
+
return { message, code: EXIT.USAGE };
|
|
257
310
|
}
|
package/src/bench/ablate.mjs
CHANGED
|
@@ -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
|
-
|
|
347
|
-
if (
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
statistic: cmp.
|
|
571
|
-
pValue: cmp.
|
|
572
|
-
|
|
573
|
-
|
|
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,
|
|
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
|
}
|
package/src/bench/mine.mjs
CHANGED
|
@@ -29,11 +29,12 @@ export const DEFAULT_MINE_TARGET = 20;
|
|
|
29
29
|
/**
|
|
30
30
|
* Classify whether a repository-relative path is a test file.
|
|
31
31
|
*
|
|
32
|
-
* Covers the
|
|
32
|
+
* Covers the ecosystems named in plan 0003 §5 R01:
|
|
33
33
|
* - `test/`, `tests/`, `spec/`, `__tests__/`, `src/test/`
|
|
34
34
|
* - `*.test.*`, `*.spec.*`, `*_test.*`, `*_spec.*`
|
|
35
35
|
* - `*_test.go` (Go)
|
|
36
36
|
* - `test_*.py` (pytest)
|
|
37
|
+
* - `tests/*.rs` (Cargo integration tests)
|
|
37
38
|
* - `*Test.java`, `*Tests.java`, `*Test.kt`, `*Tests.kt` (Gradle / Maven)
|
|
38
39
|
*
|
|
39
40
|
* @param {string} relPath forward-slash repository-relative path
|
|
@@ -259,6 +260,31 @@ export function parseGoTestJson(text) {
|
|
|
259
260
|
};
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Parse libtest's text output, which `cargo test` prints for every test binary.
|
|
265
|
+
*
|
|
266
|
+
* A line is a result only when it has the `test <name> ... <status>` shape.
|
|
267
|
+
* Harness summaries (`test result: ok.`), compile output and doctest prose are
|
|
268
|
+
* not results. `ignored` is neither a pass nor a failure: counting it as one
|
|
269
|
+
* would make a skipped test look like evidence the change works.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} text
|
|
272
|
+
* @returns {{passed: string[], failed: string[]}}
|
|
273
|
+
*/
|
|
274
|
+
export function parseCargoTestOutput(text) {
|
|
275
|
+
const passed = [];
|
|
276
|
+
const failed = [];
|
|
277
|
+
for (const line of String(text ?? '').split('\n')) {
|
|
278
|
+
const match = /^test\s+(.+?)\s+\.\.\.\s+(ok|FAILED|ignored)\s*$/.exec(line.trimEnd());
|
|
279
|
+
if (!match || match[2] === 'ignored') continue;
|
|
280
|
+
(match[2] === 'ok' ? passed : failed).push(match[1]);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
passed: dedupeSorted(passed),
|
|
284
|
+
failed: dedupeSorted(failed),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
262
288
|
/**
|
|
263
289
|
* Detect the test runner for a repository and build the command for a set of
|
|
264
290
|
* changed test files.
|
|
@@ -302,6 +328,27 @@ export async function detectTestRunner(root, testFiles) {
|
|
|
302
328
|
};
|
|
303
329
|
}
|
|
304
330
|
|
|
331
|
+
if (existsSync(join(root, 'Cargo.toml'))) {
|
|
332
|
+
// Cargo has no "run this file" argument. An integration test is a target
|
|
333
|
+
// named after its file in `tests/`, so it can be selected with `--test`.
|
|
334
|
+
// A source file carries inline unit tests and can only be reached by the
|
|
335
|
+
// crate's test binary, so one such file means the whole crate runs.
|
|
336
|
+
const targets = dedupeSorted(
|
|
337
|
+
files.filter((f) => /^tests\/[^/]+\.rs$/.test(f)).map((f) => basename(f, '.rs')),
|
|
338
|
+
);
|
|
339
|
+
const integrationOnly = files.length > 0 && files.every((f) => /^tests\/[^/]+\.rs$/.test(f));
|
|
340
|
+
const args = ['test'];
|
|
341
|
+
if (integrationOnly) {
|
|
342
|
+
for (const target of targets) args.push('--test', target);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
runner: 'cargo-test',
|
|
346
|
+
displayCmd: ['cargo', ...args].join(' '),
|
|
347
|
+
buildExec: () => ({ cmd: 'cargo', args }),
|
|
348
|
+
parse: (stdout) => parseCargoTestOutput(stdout),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
305
352
|
if (existsSync(join(root, 'go.mod'))) {
|
|
306
353
|
const pkgs = dedupeSorted(
|
|
307
354
|
files.map((f) => {
|
package/src/bench/probes.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs
|
|
|
25
25
|
import { tmpdir } from 'node:os';
|
|
26
26
|
import { dirname, join, resolve } from 'node:path';
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
29
29
|
import { parse as parseRegion } from '../projection/markers.mjs';
|
|
30
30
|
import { materialiseArm, parseArmSpec } from './run.mjs';
|
|
31
31
|
|
|
@@ -92,7 +92,7 @@ function runNode(args, { cwd, env = {} } = {}) {
|
|
|
92
92
|
* @returns {Promise<Array<object>>}
|
|
93
93
|
*/
|
|
94
94
|
export async function readLedgerEntries(workDir) {
|
|
95
|
-
const ledgerPath = join(workDir,
|
|
95
|
+
const ledgerPath = join(workDir, LOCAL_DIR, 'ledger.jsonl');
|
|
96
96
|
try {
|
|
97
97
|
const text = await readFile(ledgerPath, 'utf8');
|
|
98
98
|
const entries = [];
|
|
@@ -115,7 +115,7 @@ export async function readLedgerEntries(workDir) {
|
|
|
115
115
|
* Append a ledger entry to `.simt-dev/ledger.jsonl` in `workDir`.
|
|
116
116
|
*/
|
|
117
117
|
export async function appendLedgerEntry(workDir, entry) {
|
|
118
|
-
const ledgerPath = join(workDir,
|
|
118
|
+
const ledgerPath = join(workDir, LOCAL_DIR, 'ledger.jsonl');
|
|
119
119
|
await mkdir(dirname(ledgerPath), { recursive: true });
|
|
120
120
|
const treeRes = await runGit(['write-tree'], { cwd: workDir });
|
|
121
121
|
const record = {
|
|
@@ -1148,8 +1148,7 @@ export async function gradeProbe(probeOrId, ctx) {
|
|
|
1148
1148
|
* @param {string} root
|
|
1149
1149
|
* @returns {Promise<Array<object>>}
|
|
1150
1150
|
*/
|
|
1151
|
-
export async function loadProbeRecords(root) {
|
|
1152
|
-
const probesRoot = join(resolve(root), STATE_DIR, 'bench', 'probes');
|
|
1151
|
+
export async function loadProbeRecords(root) { const probesRoot = join(resolve(root), LOCAL_DIR, 'bench', 'probes');
|
|
1153
1152
|
if (!existsSync(probesRoot)) return [];
|
|
1154
1153
|
|
|
1155
1154
|
const entries = await readdir(probesRoot, { withFileTypes: true }).catch(() => []);
|
|
@@ -1242,7 +1241,7 @@ export async function runBenchProbes(
|
|
|
1242
1241
|
for (let rep = 1; rep <= reps; rep += 1) {
|
|
1243
1242
|
const runDir = join(
|
|
1244
1243
|
resolvedRoot,
|
|
1245
|
-
|
|
1244
|
+
LOCAL_DIR,
|
|
1246
1245
|
'bench',
|
|
1247
1246
|
'probes',
|
|
1248
1247
|
`${harness}-${armSpec.name}-r${rep}`,
|