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.
- package/CHANGELOG.md +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.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-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- 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/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- 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
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- 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 +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- 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 +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- 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 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- 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 +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- 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 +31 -14
- package/src/projection/manifest.mjs +89 -53
- 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 +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- 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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
}
|
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/adapters.json
CHANGED
|
@@ -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
|
}
|