simt-dev 0.2.2 → 0.3.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 +37 -0
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/package.json +2 -1
- package/src/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +36 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +359 -0
- package/src/commands/harness.mjs +156 -5
- package/src/commands/help.mjs +58 -1
- package/src/commands/index.mjs +25 -1
- package/src/commands/plan.mjs +99 -0
- package/src/commands/tokens.mjs +31 -0
- package/src/decisions/index.mjs +23 -5
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/server.mjs +37 -6
- package/src/mcp/tools.mjs +84 -0
- package/src/measure/context.mjs +177 -6
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/manifest.mjs +26 -3
- package/src/projection/rules.mjs +80 -5
- package/src/projection/surfaces.mjs +22 -8
- package/src/toolwords.mjs +69 -0
- package/templates/plan/tracker.json +36 -0
package/src/commands/harness.mjs
CHANGED
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import { stat } from 'node:fs/promises';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
|
-
import { join } from 'node:path';
|
|
19
|
+
import { isAbsolute, join } from 'node:path';
|
|
20
20
|
|
|
21
|
-
import { EXIT, emit, emitJson, isJson, note } from '../output/index.mjs';
|
|
21
|
+
import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
|
|
22
22
|
import { SURFACES, CANDIDATE_DIRS } from '../projection/surfaces.mjs';
|
|
23
|
+
import { LOADERS, LOADER_IDS, caps, loader } from '../projection/loaders.mjs';
|
|
24
|
+
import { viewAll } from '../measure/loaderView.mjs';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* `simt-dev harness detect`. The subcommand is validated by dispatch, which rejects a
|
|
@@ -88,7 +90,9 @@ async function scan(root) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
for (const c of CANDIDATE_DIRS) {
|
|
91
|
-
if (await present(root, c.dir))
|
|
93
|
+
if (await present(root, c.dir)) {
|
|
94
|
+
rows.push(row(c.dir, c.harness, { readsAgentsMd: c.readsAgentsMd ?? null, verified: c.verified ?? null }));
|
|
95
|
+
}
|
|
92
96
|
}
|
|
93
97
|
rows.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
|
|
94
98
|
return { root, rows };
|
|
@@ -120,7 +124,9 @@ function row(dir, label, surface) {
|
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
function countUnclaimed(scan) {
|
|
123
|
-
|
|
127
|
+
// A candidate that cites a document and reads AGENTS.md is covered by the standard
|
|
128
|
+
// region. One that cites nothing is the case the note exists for (ADR-067).
|
|
129
|
+
return scan.rows.filter((r) => r.surface === null && r.readsAgentsMd == null).length;
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
/**
|
|
@@ -143,8 +149,153 @@ function render(root, repo, user) {
|
|
|
143
149
|
function block(scan, pad) {
|
|
144
150
|
if (scan.rows.length === 0) return [`${pad}(none present)`];
|
|
145
151
|
return scan.rows.map((r) => {
|
|
146
|
-
const through =
|
|
152
|
+
const through =
|
|
153
|
+
r.surface === null
|
|
154
|
+
? r.readsAgentsMd === 'all'
|
|
155
|
+
? 'AGENTS.md'
|
|
156
|
+
: 'no rule channel in this release'
|
|
157
|
+
: r.channel
|
|
158
|
+
? 'projected rule file'
|
|
159
|
+
: 'AGENTS.md';
|
|
147
160
|
const agents = r.readsAgentsMd === 'all' ? ' (reads AGENTS.md)' : r.readsAgentsMd === 'some' ? ' (some readers read AGENTS.md)' : '';
|
|
148
161
|
return `${pad}${r.dir.padEnd(12)} ${r.label.padEnd(38)} ${through}${agents}`;
|
|
149
162
|
});
|
|
150
163
|
}
|
|
164
|
+
|
|
165
|
+
// `simt harness loaders` - what each reader loads at session start, and the vendor
|
|
166
|
+
// document every claim in that answer came from.
|
|
167
|
+
//
|
|
168
|
+
// It exists because `simt tokens` answers a question about files and this one answers a
|
|
169
|
+
// question about a reader, and the two disagree in ways that matter: Antigravity scans
|
|
170
|
+
// `.agents/rules/` flat and ignores nested files, and Codex stops merging at 32 KiB
|
|
171
|
+
// across the chain rather than per file. A byte count that ignores those is not
|
|
172
|
+
// pessimistic, it is wrong, and it was wrong in the meter this slice unblocked.
|
|
173
|
+
//
|
|
174
|
+
// It exits 0 whatever it finds, for the same reason `harness detect` does: a reader that
|
|
175
|
+
// loads nothing here is a fact about this repository, not a defect in it.
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `simt harness loaders [--root <path>] [--reader <id>] [--table]`.
|
|
179
|
+
*
|
|
180
|
+
* With no flags it prints one block per reader: what it loads, what the cap leaves out,
|
|
181
|
+
* and the sentence it was read from. `--reader` narrows it. `--table` prints the
|
|
182
|
+
* vendor facts without measuring anything, which is the answer to "what does this reader
|
|
183
|
+
* do" in a repository where the answer is not about bytes.
|
|
184
|
+
*/
|
|
185
|
+
export async function harnessLoadersCommand(positional, flags) {
|
|
186
|
+
const root = flags.root ?? process.cwd();
|
|
187
|
+
const wanted = typeof flags.reader === 'string' ? flags.reader : null;
|
|
188
|
+
const rows = wanted === null ? LOADERS : [loader(wanted)].filter(Boolean);
|
|
189
|
+
|
|
190
|
+
if (rows.length === 0) {
|
|
191
|
+
return fail(
|
|
192
|
+
`unknown reader "${wanted}"`,
|
|
193
|
+
EXIT.USAGE,
|
|
194
|
+
`known readers: ${LOADER_IDS.join(', ')}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (flags.table === true) {
|
|
199
|
+
const payload = rows.map((row) => ({
|
|
200
|
+
id: row.id,
|
|
201
|
+
label: row.label,
|
|
202
|
+
readers: row.readers,
|
|
203
|
+
order: row.order,
|
|
204
|
+
nested: row.nested,
|
|
205
|
+
start: row.start,
|
|
206
|
+
alsoReads: row.alsoReads ?? [],
|
|
207
|
+
imports: row.imports,
|
|
208
|
+
caps: caps(row),
|
|
209
|
+
skills: row.skills ?? [],
|
|
210
|
+
hooks: row.hooks,
|
|
211
|
+
hooksNote: row.hooksNote ?? null,
|
|
212
|
+
mcp: row.mcp,
|
|
213
|
+
verified: row.verified,
|
|
214
|
+
note: row.note ?? null,
|
|
215
|
+
}));
|
|
216
|
+
if (isJson(flags)) {
|
|
217
|
+
emitJson({ ok: true, command: 'harness loaders', rows: payload });
|
|
218
|
+
return EXIT.OK;
|
|
219
|
+
}
|
|
220
|
+
emit(rows.map((row) => loaderTable(row)).join('\n'));
|
|
221
|
+
return EXIT.OK;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// `--at` asks what a reader loading from a subdirectory would get, which is the only
|
|
225
|
+
// way the chain rows mean anything: Codex merges the project root down to the working
|
|
226
|
+
// directory, so the same repository costs a different reader a different total
|
|
227
|
+
// depending on where that reader is standing.
|
|
228
|
+
const views = await viewAll(root, rows, flags.at ? { cwd: isAbsolute(flags.at) ? flags.at : join(root, flags.at) } : {});
|
|
229
|
+
if (isJson(flags)) {
|
|
230
|
+
emitJson({ ok: true, command: 'harness loaders', root, rows: views });
|
|
231
|
+
return EXIT.OK;
|
|
232
|
+
}
|
|
233
|
+
emit(`${views.map((view) => renderView(view)).join('\n')}`);
|
|
234
|
+
return EXIT.OK;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* One reader's measured view of the repository.
|
|
239
|
+
*
|
|
240
|
+
* The dropped rows come first under a heading of their own. A total that quietly
|
|
241
|
+
* excludes a file past the cap reads as a total, and the reader who is about to lose the
|
|
242
|
+
* tail of their deepest instruction file to a merge is exactly the person who needs to
|
|
243
|
+
* see it named.
|
|
244
|
+
*/
|
|
245
|
+
function renderView(view) {
|
|
246
|
+
const lines = [`${view.reader.padEnd(11)} ${view.label} ${view.files.length} file(s) at start, ${view.totalBytes} B`];
|
|
247
|
+
lines.push(` order ${view.order}`);
|
|
248
|
+
for (const file of view.files) {
|
|
249
|
+
const state = file.beyondCap
|
|
250
|
+
? ' BEYOND CAP - not read'
|
|
251
|
+
: file.truncated
|
|
252
|
+
? ` TRUNCATED to ${file.keptBytes} B`
|
|
253
|
+
: file.overCap
|
|
254
|
+
? ` OVER CAP (${file.countedBytes} B)`
|
|
255
|
+
: '';
|
|
256
|
+
lines.push(` ${file.file.padEnd(44)} ${String(file.countedBytes).padStart(7)} B${state}`);
|
|
257
|
+
}
|
|
258
|
+
if (view.cap) {
|
|
259
|
+
lines.push(` cap ${view.cap.bytes} B over ${view.cap.appliesTo} (${view.cap.config ?? 'fixed'})`);
|
|
260
|
+
} else if (!view.perFileCap && !view.budget) {
|
|
261
|
+
lines.push(' cap none published');
|
|
262
|
+
}
|
|
263
|
+
if (view.perFileCap) {
|
|
264
|
+
lines.push(` file cap ${view.perFileCap.bytes} B per rule file (${view.perFileCap.config ?? 'fixed'})`);
|
|
265
|
+
}
|
|
266
|
+
if (view.budget) {
|
|
267
|
+
lines.push(` budget ${view.budget.tokens} tokens (${view.budget.bytes} B approx) over ${view.budget.appliesTo}`);
|
|
268
|
+
}
|
|
269
|
+
if (view.imports?.syntax) {
|
|
270
|
+
lines.push(` imports ${view.imports.syntax}, ${view.imports.depth} hop(s)`);
|
|
271
|
+
}
|
|
272
|
+
if (view.userScope.length > 0) {
|
|
273
|
+
lines.push(` user ${view.userScope.join(', ')} (not measured: your bytes, not the repo's)`);
|
|
274
|
+
}
|
|
275
|
+
return `${lines.join('\n')}\n`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The vendor facts, without a repository — the part of the answer that is not a measurement. */
|
|
279
|
+
function loaderTable(row) {
|
|
280
|
+
const lines = [`${row.id} ${row.label} (${row.readers})`];
|
|
281
|
+
lines.push(` order ${row.order}`);
|
|
282
|
+
lines.push(` nested ${row.nested}`);
|
|
283
|
+
for (const entry of row.start) {
|
|
284
|
+
const where = entry.scope === 'user' ? 'user' : 'project';
|
|
285
|
+
lines.push(` start ${where} ${entry.path}${entry.atStart ? '' : ' (on demand)'}`);
|
|
286
|
+
}
|
|
287
|
+
for (const extra of row.alsoReads ?? []) {
|
|
288
|
+
lines.push(` also ${extra.what}${extra.atStart ? '' : ' (on demand)'}`);
|
|
289
|
+
}
|
|
290
|
+
for (const c of caps(row)) {
|
|
291
|
+
lines.push(` cap ${c.bytes} ${c.scope === 'tokens' ? 'tokens' : 'B'} over ${c.appliesTo ?? c.scope}`);
|
|
292
|
+
}
|
|
293
|
+
if (row.cap === null) lines.push(` cap none published — ${row.capNote ?? 'checked'}`);
|
|
294
|
+
lines.push(` skills ${(row.skills ?? []).join(', ') || 'none documented'}`);
|
|
295
|
+
lines.push(` hooks ${row.hooks?.path ?? 'none documented'}${row.hooksNote ? ` — ${row.hooksNote}` : ''}`);
|
|
296
|
+
lines.push(
|
|
297
|
+
` mcp ${row.mcp?.path ?? (row.mcp?.key ? `profile ${row.mcp.key}` : 'none documented')}`,
|
|
298
|
+
);
|
|
299
|
+
lines.push(` source ${row.verified}`);
|
|
300
|
+
return lines.join('\n');
|
|
301
|
+
}
|
package/src/commands/help.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { emit } from '../output/index.mjs';
|
|
2
2
|
import { parse } from '../argv.mjs';
|
|
3
|
-
import { CLI } from '../name.mjs';
|
|
3
|
+
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
4
|
+
import { DEFAULT_DIRECTORIES, DEFAULT_TEMPLATE } from '../plan/template.mjs';
|
|
4
5
|
|
|
5
6
|
const COMMANDS = `${CLI} init [path] adopt or scaffold a repo into the ecosystem
|
|
6
7
|
(dry-run unless --yes; conflicts block without
|
|
@@ -38,6 +39,20 @@ ${CLI} hooks install [--yes] install the shared dispatcher; report what it wou
|
|
|
38
39
|
${CLI} hooks status what each git event routes to, and whether it is wired
|
|
39
40
|
${CLI} hooks run <event> run one event's gates now, as git would
|
|
40
41
|
(pre-commit, pre-push, post-merge, ...)
|
|
42
|
+
${CLI} agent-hook <event> the entry point a harness calls before a tool call
|
|
43
|
+
(reads one JSON payload on stdin and answers in that
|
|
44
|
+
harness's dialect; exit 2 blocks; needs --harness)
|
|
45
|
+
${CLI} agent-hooks install write this repo's policies into the harnesses that
|
|
46
|
+
read a hooks file
|
|
47
|
+
(dry-run unless --yes; merges into the file, keeps every
|
|
48
|
+
key it does not own; Codex needs the hook trusted)
|
|
49
|
+
${CLI} agent-hooks status what each harness will run, and what is not trusted yet
|
|
50
|
+
${CLI} plan status count tracker rows
|
|
51
|
+
(${MANIFEST_FILE} key plans when set, otherwise
|
|
52
|
+
the ${DEFAULT_TEMPLATE} template:
|
|
53
|
+
${DEFAULT_DIRECTORIES.join(' and ')})
|
|
54
|
+
${CLI} plan next [<id>] one ready slice's section, or the one named; ~1 KB
|
|
55
|
+
${CLI} plan done <id> [<sha>] mark one row; dry-run unless --yes
|
|
41
56
|
${CLI} graph lint check the skill registry against the skills on disk
|
|
42
57
|
(byte counts, dangling edges, unresolvable variants)
|
|
43
58
|
${CLI} ci simt-dev's own .github files: create or refresh them
|
|
@@ -54,6 +69,25 @@ ${CLI} harness detect which harnesses this repo (and, with --user, you)
|
|
|
54
69
|
are set up for, and which have a rule channel here
|
|
55
70
|
(presence only: a directory found is not a claim about
|
|
56
71
|
what its owner reads; exits 0 whatever it finds)
|
|
72
|
+
${CLI} harness loaders what each reader loads at session start, in bytes,
|
|
73
|
+
and what its size cap drops
|
|
74
|
+
(--table prints the vendor facts without measuring;
|
|
75
|
+
--reader <id> narrows it; every claim cites its source)
|
|
76
|
+
${CLI} bench mine [--yes] mine verified benchmark tasks from git history
|
|
77
|
+
(dry-run unless --yes; validates RED and GOLD twice in
|
|
78
|
+
a temp worktree and stores test patches in the vault)
|
|
79
|
+
${CLI} bench run [--yes] run benchmark tasks across arms in isolated one-commit exports
|
|
80
|
+
(dry-run unless --yes; grades against vault test patches
|
|
81
|
+
and declared gates; --regrade <run> verifies stored runs)
|
|
82
|
+
${CLI} bench report statistical report across benchmark arms (R, Q, C, CPI, cache)
|
|
83
|
+
(read-only; paired bootstrap 95% CIs; --compare <a> <b>
|
|
84
|
+
prints deltas with McNemar and Wilcoxon p-values)
|
|
85
|
+
${CLI} bench ablate [--yes] leave-one-out ablation across core, profile and process sections
|
|
86
|
+
(dry-run unless --yes; 10-task canary with escalation,
|
|
87
|
+
value-per-KB ranking and deletion candidates)
|
|
88
|
+
${CLI} bench probes [--yes] 12 synthetic rule-compliance probes with deterministic graders
|
|
89
|
+
(dry-run unless --yes; grades git state, transcript and ledger,
|
|
90
|
+
and reports compliance per arm)
|
|
57
91
|
${CLI} mcp the read-only surface as MCP tools over stdio
|
|
58
92
|
(--list prints it without reading stdin; init, sync,
|
|
59
93
|
upgrade and the installers stay CLI-only by design)`;
|
|
@@ -76,6 +110,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
76
110
|
--dry-run report what would change, change nothing
|
|
77
111
|
--yes assume yes; required in automation, never prompts
|
|
78
112
|
--check report drift and write nothing (sync, ci)
|
|
113
|
+
--explain sync: name each line dropped as already inferable, with
|
|
114
|
+
the fact behind it
|
|
79
115
|
--all ignore a cap: every ruling of a record, or the skills budget
|
|
80
116
|
--variant <id> which body of a multi-variant skill to use
|
|
81
117
|
--from <repo/path> install from a declared source by name
|
|
@@ -88,6 +124,27 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
88
124
|
--user harness detect: also report the directories in your home
|
|
89
125
|
--repos tokens: measure every repository under the root, not
|
|
90
126
|
just the one, and report what repeats an earlier one
|
|
127
|
+
--session <path> tokens: split a local Claude Code or Codex transcript
|
|
128
|
+
into rules, file reads and tool output
|
|
129
|
+
--reader <id> harness loaders: report one reader (opencode, claude, codex,
|
|
130
|
+
cursor, antigravity, copilot, deepseek)
|
|
131
|
+
--table harness loaders: print order, caps, imports, skill dirs and
|
|
132
|
+
the hook and MCP config files, without measuring a repo
|
|
133
|
+
--at <path> harness loaders: measure as if the agent were working in
|
|
134
|
+
this directory (default: the root; readers that merge the
|
|
135
|
+
project root down to the working directory cost more here)
|
|
136
|
+
--arm <spec> bench run: arm to evaluate (bare, current, kernel, brief,
|
|
137
|
+
or name=path/to/arm.json; repeatable)
|
|
138
|
+
--harness <id> bench run: adapter to invoke (claude, codex, gemini, opencode,
|
|
139
|
+
cursor, aider, gold, noop; default: gold);
|
|
140
|
+
agent-hook: which dialect to answer in (claude, cursor,
|
|
141
|
+
codex, antigravity)
|
|
142
|
+
--model <id> bench run: model identifier recorded and passed to the adapter
|
|
143
|
+
--reps <n> bench run: repetitions per task and arm (default: 3)
|
|
144
|
+
--regrade <run> bench run: re-grade a stored run from its archived agent patch
|
|
145
|
+
--compare <a> <b> bench report: compare two arms with deltas and p-values
|
|
146
|
+
--test <cmd> plan done: the test command that closes the row
|
|
147
|
+
--state <name> plan done: target state (default: Complete)
|
|
91
148
|
--help this text
|
|
92
149
|
--version the version, and the SHA it was built from`;
|
|
93
150
|
|
package/src/commands/index.mjs
CHANGED
|
@@ -29,13 +29,22 @@ import { syncCommand, checkCommand } from './sync.mjs';
|
|
|
29
29
|
import { initCommand } from './init.mjs';
|
|
30
30
|
import { ciCommand } from './ci.mjs';
|
|
31
31
|
import { hooksCommand } from './hooks.mjs';
|
|
32
|
+
import { agentHookCommand, agentHooksCommand } from './agenthook.mjs';
|
|
32
33
|
import { compareCommand } from './compare.mjs';
|
|
33
34
|
import { upgradeCommand } from './upgrade.mjs';
|
|
34
35
|
import { mcpCommand } from './mcp.mjs';
|
|
35
36
|
import { installCommand } from './skillsinstall.mjs';
|
|
36
37
|
import { listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
|
|
37
38
|
import { tokensCommand } from './tokens.mjs';
|
|
38
|
-
import { harnessCommand } from './harness.mjs';
|
|
39
|
+
import { harnessCommand, harnessLoadersCommand } from './harness.mjs';
|
|
40
|
+
import {
|
|
41
|
+
benchMineCommand,
|
|
42
|
+
benchRunCommand,
|
|
43
|
+
benchReportCommand,
|
|
44
|
+
benchAblateCommand,
|
|
45
|
+
benchProbesCommand,
|
|
46
|
+
} from './bench.mjs';
|
|
47
|
+
import { planCommand } from './plan.mjs';
|
|
39
48
|
import { parse } from '../argv.mjs';
|
|
40
49
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
41
50
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
@@ -53,7 +62,10 @@ const GROUPS = new Map([
|
|
|
53
62
|
['graph', 'lint'],
|
|
54
63
|
['rules', 'list'],
|
|
55
64
|
['hooks', 'status'],
|
|
65
|
+
['plan', 'status'],
|
|
66
|
+
['agent-hooks', 'status'],
|
|
56
67
|
['harness', 'detect'],
|
|
68
|
+
['bench', 'mine'],
|
|
57
69
|
]);
|
|
58
70
|
|
|
59
71
|
/**
|
|
@@ -84,14 +96,26 @@ export const IMPLEMENTED = {
|
|
|
84
96
|
'rules why': rulesWhy,
|
|
85
97
|
sync: syncCommand,
|
|
86
98
|
check: checkCommand,
|
|
99
|
+
'plan status': (args, flags) => planCommand('status', args, flags),
|
|
100
|
+
'plan next': (args, flags) => planCommand('next', args, flags),
|
|
101
|
+
'plan done': (args, flags) => planCommand('done', args, flags),
|
|
87
102
|
init: initCommand,
|
|
88
103
|
ci: ciCommand,
|
|
89
104
|
'hooks install': (args, flags) => hooksCommand('install', args, flags),
|
|
90
105
|
'hooks status': (args, flags) => hooksCommand('status', args, flags),
|
|
91
106
|
'hooks run': (args, flags) => hooksCommand('run', args, flags),
|
|
107
|
+
'agent-hook': agentHookCommand,
|
|
108
|
+
'agent-hooks install': (args, flags) => agentHooksCommand('install', args, flags),
|
|
109
|
+
'agent-hooks status': (args, flags) => agentHooksCommand('status', args, flags),
|
|
92
110
|
compare: compareCommand,
|
|
93
111
|
tokens: tokensCommand,
|
|
94
112
|
'harness detect': harnessCommand,
|
|
113
|
+
'harness loaders': harnessLoadersCommand,
|
|
114
|
+
'bench mine': benchMineCommand,
|
|
115
|
+
'bench run': benchRunCommand,
|
|
116
|
+
'bench report': benchReportCommand,
|
|
117
|
+
'bench ablate': benchAblateCommand,
|
|
118
|
+
'bench probes': benchProbesCommand,
|
|
95
119
|
upgrade: upgradeCommand,
|
|
96
120
|
mcp: mcpCommand,
|
|
97
121
|
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.mjs';
|
|
2
|
+
import { formatCounts, planDone, planNext, planStatus } from '../plan/index.mjs';
|
|
3
|
+
import { CLI } from '../name.mjs';
|
|
4
|
+
|
|
5
|
+
// Preview unless --yes, and --dry-run wins even then. A tracker row marked done
|
|
6
|
+
// for a commit that was never written is a claim the next reader will believe.
|
|
7
|
+
|
|
8
|
+
export async function planCommand(sub, args, flags) {
|
|
9
|
+
const root = flags.root ?? process.cwd();
|
|
10
|
+
if (sub === 'status') return runStatus(root, flags);
|
|
11
|
+
if (sub === 'next') return runNext(root, args[0] ?? null, flags);
|
|
12
|
+
if (sub === 'done') {
|
|
13
|
+
if (!args[0]) return fail('plan done needs a slice id', EXIT.USAGE, `run: ${CLI} plan next`);
|
|
14
|
+
return runDone(root, args[0], args[1] ?? null, flags);
|
|
15
|
+
}
|
|
16
|
+
if (!sub) {
|
|
17
|
+
return fail('plan needs a subcommand', EXIT.USAGE, `run: ${CLI} plan status | ${CLI} plan next | ${CLI} plan done <id>`);
|
|
18
|
+
}
|
|
19
|
+
return fail(`unknown plan subcommand ${sub}`, EXIT.USAGE, `run: ${CLI} plan status`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function runStatus(root, flags) {
|
|
23
|
+
const result = await planStatus(root);
|
|
24
|
+
if (result.error) return fail(result.error, EXIT.USAGE, result.remedy);
|
|
25
|
+
if (isJson(flags)) {
|
|
26
|
+
emitJson({ ok: true, command: 'plan status', ...result });
|
|
27
|
+
return EXIT.OK;
|
|
28
|
+
}
|
|
29
|
+
const lines = [`repo ${root}`, `template ${result.template}`, `dirs ${result.directories.join(', ')}`];
|
|
30
|
+
if (result.documents.length === 0) lines.push(`(no tracker in ${result.directories.join(' or ')})`);
|
|
31
|
+
for (const doc of result.documents) {
|
|
32
|
+
lines.push(`${doc.name} slices=${doc.slices} ${formatCounts(doc.counts, result.stateOrder)}`);
|
|
33
|
+
}
|
|
34
|
+
lines.push('', `total ${formatCounts(result.totals, result.stateOrder) || '-'}`);
|
|
35
|
+
lines.push(`ready ${result.ready}`);
|
|
36
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
37
|
+
return EXIT.OK;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runNext(root, id, flags) {
|
|
41
|
+
const result = await planNext(root, id);
|
|
42
|
+
if (result.error) return fail(result.error, EXIT.USAGE, result.remedy);
|
|
43
|
+
if (isJson(flags)) {
|
|
44
|
+
emitJson({ ok: true, command: 'plan next', ...result });
|
|
45
|
+
return EXIT.OK;
|
|
46
|
+
}
|
|
47
|
+
if (!result.next) {
|
|
48
|
+
const lines = [`repo ${root}`, result.reason];
|
|
49
|
+
if (result.blocked?.length) {
|
|
50
|
+
lines.push('', 'blocked:');
|
|
51
|
+
for (const row of result.blocked) {
|
|
52
|
+
const unknown = row.unknownDependencies.length ? ` unknown: ${row.unknownDependencies.join(', ')}` : '';
|
|
53
|
+
lines.push(` ${row.id} ${row.doc} depends on: ${row.dependsOn || '(none)'}${unknown}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
57
|
+
return EXIT.OK;
|
|
58
|
+
}
|
|
59
|
+
const next = result.next;
|
|
60
|
+
const head = [
|
|
61
|
+
`repo ${root}`,
|
|
62
|
+
`slice ${next.id} ${next.doc}`,
|
|
63
|
+
`state ${next.state}`,
|
|
64
|
+
`depends ${next.dependsOn || '(none)'}`,
|
|
65
|
+
`bytes ${next.sectionBytes}`,
|
|
66
|
+
'',
|
|
67
|
+
next.section || '(no section for this id)',
|
|
68
|
+
];
|
|
69
|
+
emit(head.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
70
|
+
return EXIT.OK;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function runDone(root, id, sha, flags) {
|
|
74
|
+
const mode = writeMode(flags);
|
|
75
|
+
const result = await planDone(root, id, {
|
|
76
|
+
sha,
|
|
77
|
+
test: flags.test ?? null,
|
|
78
|
+
state: flags.state ?? null,
|
|
79
|
+
dryRun: !mode.apply,
|
|
80
|
+
});
|
|
81
|
+
if (result.error) return fail(result.error, EXIT.USAGE, result.remedy);
|
|
82
|
+
if (isJson(flags)) {
|
|
83
|
+
emitJson({ ok: true, command: 'plan done', ...result });
|
|
84
|
+
return EXIT.OK;
|
|
85
|
+
}
|
|
86
|
+
const lines = [`repo ${root}`];
|
|
87
|
+
if (result.dryRun) {
|
|
88
|
+
lines.push(`dry-run ${result.id} ${result.doc} line ${result.line}`);
|
|
89
|
+
lines.push(result.wouldChange ? `would ${result.from} -> ${result.to}` : `state ${result.from}`);
|
|
90
|
+
if (result.remedy) lines.push(`apply ${result.remedy}`);
|
|
91
|
+
} else {
|
|
92
|
+
lines.push(`changed ${result.changed ? 'yes' : 'no'} ${result.id} ${result.doc} line ${result.line}`);
|
|
93
|
+
lines.push(result.from === result.to ? `state ${result.to}` : `state ${result.from} -> ${result.to}`);
|
|
94
|
+
}
|
|
95
|
+
if (result.evidenceRecorded) lines.push(`evidence ${result.sha}${result.test ? ` test: ${result.test}` : ''}`);
|
|
96
|
+
else if (result.evidenceNote) lines.push(`note ${result.evidenceNote}`);
|
|
97
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
98
|
+
return EXIT.OK;
|
|
99
|
+
}
|
package/src/commands/tokens.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
mcpTools,
|
|
34
34
|
totals,
|
|
35
35
|
} from '../measure/context.mjs';
|
|
36
|
+
import { measureSession } from '../measure/session.mjs';
|
|
36
37
|
|
|
37
38
|
/** `1234567`, never `1,234,567`. `toLocaleString` output depends on the locale, and
|
|
38
39
|
* this command exists so two machines produce two numbers a pull request can diff. */
|
|
@@ -45,6 +46,36 @@ function files(n) {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export async function tokensCommand(positional, flags) {
|
|
49
|
+
if (flags.session !== undefined) {
|
|
50
|
+
const session = await measureSession(flags.session);
|
|
51
|
+
if (!session.ok) {
|
|
52
|
+
return fail(session.error, EXIT.CONFIG, session.remedy);
|
|
53
|
+
}
|
|
54
|
+
if (isJson(flags)) {
|
|
55
|
+
emitJson({
|
|
56
|
+
ok: true,
|
|
57
|
+
command: 'tokens',
|
|
58
|
+
mode: 'session',
|
|
59
|
+
session: session.path,
|
|
60
|
+
bytesPerToken: BYTES_PER_TOKEN,
|
|
61
|
+
rules: session.rules,
|
|
62
|
+
fileReads: session.fileReads,
|
|
63
|
+
toolOutput: session.toolOutput,
|
|
64
|
+
total: session.total,
|
|
65
|
+
});
|
|
66
|
+
return EXIT.OK;
|
|
67
|
+
}
|
|
68
|
+
const lines = [
|
|
69
|
+
`tokens ${session.path} (session)`,
|
|
70
|
+
`rules ${plain(session.rules.bytes)} B ~${plain(session.rules.tokens)} tokens (${plain(session.rules.share)}%)`,
|
|
71
|
+
`file reads ${plain(session.fileReads.bytes)} B ~${plain(session.fileReads.tokens)} tokens (${plain(session.fileReads.share)}%)`,
|
|
72
|
+
`tool output ${plain(session.toolOutput.bytes)} B ~${plain(session.toolOutput.tokens)} tokens (${plain(session.toolOutput.share)}%)`,
|
|
73
|
+
`total ${plain(session.total.bytes)} B ~${plain(session.total.tokens)} tokens`,
|
|
74
|
+
];
|
|
75
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
76
|
+
return EXIT.OK;
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
// `targetRoot` owns the precedence, so `tokens`, `compare` and `upgrade` cannot
|
|
49
80
|
// disagree about which directory was named (ADR-014).
|
|
50
81
|
const target = await targetRoot(flags, positional);
|
package/src/decisions/index.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
22
22
|
import { CLI } from '../name.mjs';
|
|
23
|
+
import { PREFIX_ARGUING, TOOL_WORDS } from '../toolwords.mjs';
|
|
23
24
|
|
|
24
25
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
export const DECISIONS_DIR = resolve(HERE, '..', '..', 'docs', 'decisions');
|
|
@@ -240,7 +241,7 @@ export async function loadDecisions({ all = false } = {}) {
|
|
|
240
241
|
(m) => `ADR-${m[1]}`,
|
|
241
242
|
),
|
|
242
243
|
partial,
|
|
243
|
-
summary: summarise(text, partial, { all }),
|
|
244
|
+
summary: summarise(text, partial, { all, file }),
|
|
244
245
|
bytes: Buffer.byteLength(text),
|
|
245
246
|
});
|
|
246
247
|
}
|
|
@@ -288,7 +289,7 @@ export function byId(decisions) {
|
|
|
288
289
|
* answer "why is this here"; the reader who needs the rest asks.
|
|
289
290
|
* @returns {{ruled: string[], rulingNumbers: (number|null)[], rulingCount: number, rejected: string[], writtenAgainst: string[]}}
|
|
290
291
|
*/
|
|
291
|
-
function summarise(text, partial = [], { all = false } = {}) {
|
|
292
|
+
function summarise(text, partial = [], { all = false, file = null } = {}) {
|
|
292
293
|
// A section ends at the next `## ` — but not at one inside a fenced code block.
|
|
293
294
|
// ADR-007's entire ruling is a document skeleton in a fence whose headings are `##`,
|
|
294
295
|
// so the old lookahead ended the section at the first line of the ruling itself. It
|
|
@@ -446,7 +447,15 @@ const section = (name) => {
|
|
|
446
447
|
// Named rather than stripped. A record that cites this org's repositories is still
|
|
447
448
|
// the record; silently deleting the citation would hide which decisions were made in
|
|
448
449
|
// response to what, and would mangle sentences mid-clause.
|
|
449
|
-
|
|
450
|
+
//
|
|
451
|
+
// What is named is a repository, and the shape was wrong about that: every
|
|
452
|
+
// `simt-<word>` that was not `simt-dev` was reported as one, so the tool's own
|
|
453
|
+
// `simt-priority` was listed as a surveyed repository and the record that settled the
|
|
454
|
+
// kernel read as if it had been decided by looking at some other repository (ADR-080).
|
|
455
|
+
// A record whose subject is the prefix is arguing rather than citing, so it names none.
|
|
456
|
+
writtenAgainst: PREFIX_ARGUING.has(file)
|
|
457
|
+
? []
|
|
458
|
+
: [...new Set((text.match(ESTATE) ?? []).filter((name) => !TOOL_WORDS.has(name)))].sort(),
|
|
450
459
|
};
|
|
451
460
|
}
|
|
452
461
|
|
|
@@ -458,6 +467,10 @@ const section = (name) => {
|
|
|
458
467
|
* The skill registry's `source.repo` values are asserted to match this (S14); a name
|
|
459
468
|
* the registry adds that is not `simt-*` has to be added here or that assertion fails.
|
|
460
469
|
*
|
|
470
|
+
* The shape is not the whole answer, and treating it as one is what ADR-080 settles: a
|
|
471
|
+
* `simt-` name this tool owns is not a repository, and neither reader can tell that from
|
|
472
|
+
* the shape. `src/toolwords.mjs` is the list both of them share.
|
|
473
|
+
*
|
|
461
474
|
* Not global: callers reuse it with `exec`, and a `/g` flag would remember `lastIndex`.
|
|
462
475
|
*/
|
|
463
476
|
/**
|
|
@@ -486,7 +499,12 @@ export const ESTATE_NAME =
|
|
|
486
499
|
|
|
487
500
|
/** Replace estate repository names with a phrase a reader who does not have them can use. */
|
|
488
501
|
export function redactEstate(text) {
|
|
489
|
-
return text.replace(new RegExp(ESTATE_NAME.source, 'g'),
|
|
502
|
+
return String(text ?? '').replace(new RegExp(ESTATE_NAME.source, 'g'), (name) =>
|
|
503
|
+
// The tool's own names are left where they are. Redacting them turned a ruling about
|
|
504
|
+
// `simt-priority` into a sentence about `a repository`, which is how a reader learned
|
|
505
|
+
// that the record had surveyed something and not what it said (ADR-080).
|
|
506
|
+
TOOL_WORDS.has(name) ? name : 'a repository',
|
|
507
|
+
);
|
|
490
508
|
}
|
|
491
509
|
|
|
492
510
|
/**
|
|
@@ -506,7 +524,7 @@ export function redactEstate(text) {
|
|
|
506
524
|
* Only a real verb follows, which is what keeps `simt-dev.json`, `simt-dev-check.yml` and
|
|
507
525
|
* `simt-dev's` untouched - those are the project's artefacts and the project keeps its name.
|
|
508
526
|
*/
|
|
509
|
-
const VERB = '(?:init|doctor|check|sync|upgrade|ci|compare|hooks|mcp|rules|skills|graph|--[a-z-]+)';
|
|
527
|
+
const VERB = '(?:init|doctor|check|sync|upgrade|ci|compare|hooks|mcp|rules|skills|graph|tokens|harness|bench|--[a-z-]+)';
|
|
510
528
|
|
|
511
529
|
function redactCommand(text) {
|
|
512
530
|
return String(text ?? '').replace(new RegExp(`\\bsimt-dev (?=${VERB})\\b`, 'g'), `${CLI} `);
|