simt-dev 0.3.1 → 0.4.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 +109 -0
- package/README.md +14 -4
- package/package.json +2 -2
- package/rules/core.md +1 -1
- package/src/agenthooks/policy.mjs +10 -2
- package/src/agenthooks/prompt.mjs +81 -0
- package/src/agenthooks/render.mjs +138 -17
- package/src/argv.mjs +16 -1
- package/src/bench/grade.mjs +3 -0
- package/src/bench/run.mjs +85 -39
- package/src/bench/variants.mjs +456 -0
- package/src/brief/assemble.mjs +346 -0
- package/src/brief/kind.mjs +134 -0
- package/src/brief/packet.mjs +197 -0
- package/src/commands/agenthook.mjs +84 -5
- package/src/commands/bench.mjs +40 -15
- package/src/commands/brief.mjs +78 -0
- package/src/commands/compare.mjs +10 -2
- package/src/commands/decide.mjs +116 -0
- package/src/commands/help.mjs +34 -3
- package/src/commands/index.mjs +17 -1
- package/src/commands/lessons.mjs +127 -0
- package/src/commands/local.mjs +97 -0
- package/src/commands/map.mjs +42 -7
- package/src/commands/repoindex.mjs +4 -1
- package/src/commands/rules.mjs +17 -2
- package/src/commands/skills.mjs +3 -17
- package/src/commands/skillscompile.mjs +255 -0
- package/src/commands/sync.mjs +15 -0
- package/src/commands/tests.mjs +2 -49
- package/src/commands/why.mjs +18 -23
- package/src/compare/learn.mjs +680 -0
- package/src/decisions/index.mjs +116 -65
- package/src/federation/client.mjs +188 -0
- package/src/federation/config.mjs +134 -0
- package/src/federation/query.mjs +48 -0
- package/src/graph/compile.mjs +475 -0
- package/src/index/gate.mjs +1 -13
- package/src/index/govern.mjs +23 -11
- package/src/index/map.mjs +251 -17
- package/src/index/persist.mjs +74 -7
- package/src/index/store.mjs +222 -8
- package/src/kernel/variants.mjs +353 -0
- package/src/ledger/index.mjs +36 -1
- package/src/local/client.mjs +482 -0
- package/src/local/config.mjs +234 -0
- package/src/mcp/tools.mjs +138 -1
- package/src/memory/decide.mjs +462 -0
- package/src/memory/lessons.mjs +858 -0
- package/src/projection/lock.mjs +39 -7
- package/src/projection/rules.mjs +32 -8
- package/src/toolwords.mjs +6 -0
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
// walks the repository. A hook that measured the tree before answering would be exactly
|
|
12
12
|
// the latency the plan's acceptance criterion exists to prevent.
|
|
13
13
|
//
|
|
14
|
+
// A second event, `prompt-submit`, delivers rather than decides: it needs the manifest,
|
|
15
|
+
// the index and the packet assembly, so it is reached through a dynamic `import()` that
|
|
16
|
+
// only that event takes. A tool call therefore still pays for nothing that walks the
|
|
17
|
+
// repository, and the one path that does pays for it once per prompt instead of once per
|
|
18
|
+
// command.
|
|
19
|
+
//
|
|
14
20
|
// A denial exits 2, which is what every harness in the table reads as "block". A mistake
|
|
15
21
|
// in the hook's own invocation must NOT exit 2: a typo in a config would then block every
|
|
16
22
|
// tool call. Failures of this command exit 1, which a harness reports as a failed hook,
|
|
@@ -60,6 +66,32 @@ export async function agentHookCommand(args, flags) {
|
|
|
60
66
|
return EXIT.OK;
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
// Delivery, not decision: the packet goes into the prompt and the exit code stays 0
|
|
70
|
+
// whatever happens. The module behind it is the one thing here that reads the
|
|
71
|
+
// repository, and the tool-call path above must not pay for it, so the door is a
|
|
72
|
+
// dynamic import that only this event opens.
|
|
73
|
+
if (event === 'prompt-submit') {
|
|
74
|
+
const { deliverPrompt } = await import('../agenthooks/prompt.mjs');
|
|
75
|
+
const delivered = await deliverPrompt(row, payload);
|
|
76
|
+
if (delivered.skipped) {
|
|
77
|
+
note(`note ${id}: no packet delivered (${delivered.skipped})`);
|
|
78
|
+
return EXIT.OK;
|
|
79
|
+
}
|
|
80
|
+
emit(JSON.stringify(delivered.json));
|
|
81
|
+
return EXIT.OK;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (event === 'post-tool-use') {
|
|
85
|
+
const { deliverLessonRecall } = await import('../memory/lessons.mjs');
|
|
86
|
+
const recalled = await deliverLessonRecall(row, payload, { root: flags.root });
|
|
87
|
+
if (recalled.skipped) {
|
|
88
|
+
note(`note ${id}: no lesson recalled (${recalled.skipped})`);
|
|
89
|
+
return EXIT.OK;
|
|
90
|
+
}
|
|
91
|
+
emit(JSON.stringify(recalled.json));
|
|
92
|
+
return EXIT.OK;
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
const { root, command, writes } = factsFrom(row, payload);
|
|
64
96
|
const facts = await factsFor(root, { command, writes });
|
|
65
97
|
const decision = decide({ event, facts, env: process.env });
|
|
@@ -77,6 +109,22 @@ export async function agentHookCommand(args, flags) {
|
|
|
77
109
|
const json = decisionJson(row, decision);
|
|
78
110
|
if (json === null) return EXIT.OK;
|
|
79
111
|
|
|
112
|
+
if (root) {
|
|
113
|
+
try {
|
|
114
|
+
const { recordHookDenial } = await import('../memory/lessons.mjs');
|
|
115
|
+
await recordHookDenial(root, {
|
|
116
|
+
harness: row.id,
|
|
117
|
+
event,
|
|
118
|
+
policies: decision.blocked,
|
|
119
|
+
messages: decision.messages,
|
|
120
|
+
command,
|
|
121
|
+
writes,
|
|
122
|
+
});
|
|
123
|
+
} catch {
|
|
124
|
+
// Recording a lesson candidate must never block or alter the hook decision.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
80
128
|
emit(JSON.stringify(json));
|
|
81
129
|
for (const message of decision.messages) note(`blocked ${message}`);
|
|
82
130
|
return row.denyExit;
|
|
@@ -91,14 +139,37 @@ export async function agentHooksCommand(sub, args, flags) {
|
|
|
91
139
|
|
|
92
140
|
async function installHooks(root, flags) {
|
|
93
141
|
const mode = writeMode(flags);
|
|
142
|
+
// The delivery is a second entry in the same config, and a repository asks for it by
|
|
143
|
+
// declaring a `brief` key rather than by having the entry. Read through a dynamic
|
|
144
|
+
// import for the same reason the hook delivers through one.
|
|
145
|
+
const { promptOptIn } = await import('../agenthooks/prompt.mjs');
|
|
146
|
+
const { listPromotedLessons } = await import('../memory/lessons.mjs');
|
|
147
|
+
const [optIn, promotedLessons] = await Promise.all([
|
|
148
|
+
promptOptIn(root),
|
|
149
|
+
listPromotedLessons(root).catch(() => []),
|
|
150
|
+
]);
|
|
151
|
+
const hasLessons = promotedLessons.length > 0;
|
|
94
152
|
const plan = [];
|
|
95
153
|
for (const row of adapters()) {
|
|
96
154
|
const path = hookPath(row.id);
|
|
97
155
|
const existing = await readIfExists(join(root, path));
|
|
98
|
-
const result = rendered(row, existing);
|
|
99
|
-
plan.push({
|
|
156
|
+
const result = rendered(row, existing, { prompt: optIn.optedIn, post: hasLessons });
|
|
157
|
+
plan.push({
|
|
158
|
+
id: row.id,
|
|
159
|
+
path,
|
|
160
|
+
action: result.error ? 'refused' : result.action,
|
|
161
|
+
error: result.error,
|
|
162
|
+
text: result.text,
|
|
163
|
+
prompt: row.prompt === null || row.prompt === undefined ? null : optIn.optedIn ? 'declared' : 'not declared',
|
|
164
|
+
});
|
|
100
165
|
}
|
|
101
166
|
|
|
167
|
+
// One line for the repository, not one per adapter: the opt-in is a property of the
|
|
168
|
+
// repository, and someone who has not declared it is told how once.
|
|
169
|
+
const promptLine = plan.some((p) => p.prompt !== null) && !optIn.optedIn
|
|
170
|
+
? [`PROMPT not opted in (${optIn.why}); add "brief": {} to the manifest to deliver the packet on every prompt`]
|
|
171
|
+
: [];
|
|
172
|
+
|
|
102
173
|
// The readers this tool does not write an entry for, reported rather than skipped in
|
|
103
174
|
// silence: a command that writes four files and says nothing about the fifth harness
|
|
104
175
|
// in the repository reads as "that one is covered too".
|
|
@@ -128,6 +199,7 @@ async function installHooks(root, flags) {
|
|
|
128
199
|
`repo ${root}`,
|
|
129
200
|
...plan.map((p) => `${p.action.toUpperCase().padEnd(9)} ${p.id.padEnd(12)} ${p.path}${p.error ? ` (${p.error})` : ''}`),
|
|
130
201
|
...others.map((o) => `${o.state === 'none' ? 'NO FILE ' : 'NOT OURS'} ${o.id.padEnd(12)} ${o.path ?? '(no hooks file is documented)'}`),
|
|
202
|
+
...promptLine,
|
|
131
203
|
'',
|
|
132
204
|
refused.length > 0
|
|
133
205
|
? `REFUSED ${refused.length} config file(s) could not be read; nothing written`
|
|
@@ -177,6 +249,7 @@ async function installHooks(root, flags) {
|
|
|
177
249
|
`repo ${root}`,
|
|
178
250
|
...plan.map((p) => `${p.action.toUpperCase().padEnd(9)} ${p.id.padEnd(12)} ${p.path}`),
|
|
179
251
|
...others.map((o) => `${o.state === 'none' ? 'NO FILE ' : 'NOT OURS'} ${o.id.padEnd(12)} ${o.path ?? '(no hooks file is documented)'}`),
|
|
252
|
+
...promptLine,
|
|
180
253
|
'',
|
|
181
254
|
`WROTE ${written.length} hooks file(s); each one runs ${CLI} agent-hook before the tool call`,
|
|
182
255
|
].join('\n'),
|
|
@@ -196,14 +269,14 @@ async function hooksStatus(root, flags) {
|
|
|
196
269
|
const path = hookPath(row.id);
|
|
197
270
|
const existing = await readIfExists(join(root, path));
|
|
198
271
|
if (existing === null) {
|
|
199
|
-
rows.push({ id: row.id, path, state: 'absent', entries: 0, note: null });
|
|
272
|
+
rows.push({ id: row.id, path, state: 'absent', entries: 0, prompt: row.prompt ? 'absent' : null, note: null });
|
|
200
273
|
continue;
|
|
201
274
|
}
|
|
202
275
|
let config;
|
|
203
276
|
try {
|
|
204
277
|
config = JSON.parse(existing);
|
|
205
278
|
} catch (e) {
|
|
206
|
-
rows.push({ id: row.id, path, state: 'unreadable', entries: 0, note: e.message });
|
|
279
|
+
rows.push({ id: row.id, path, state: 'unreadable', entries: 0, prompt: row.prompt ? 'unknown' : null, note: e.message });
|
|
207
280
|
problems.push(`${path} is not valid JSON: ${e.message}`);
|
|
208
281
|
continue;
|
|
209
282
|
}
|
|
@@ -211,16 +284,18 @@ async function hooksStatus(root, flags) {
|
|
|
211
284
|
// refuses to write into one. Status has to agree with it, or one question gets two
|
|
212
285
|
// answers and the quiet one is the one a reader acts on.
|
|
213
286
|
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
214
|
-
rows.push({ id: row.id, path, state: 'unreadable', entries: 0, note: 'not a JSON object' });
|
|
287
|
+
rows.push({ id: row.id, path, state: 'unreadable', entries: 0, prompt: row.prompt ? 'unknown' : null, note: 'not a JSON object' });
|
|
215
288
|
problems.push(`${path} is not a JSON object, so this tool will not write into it`);
|
|
216
289
|
continue;
|
|
217
290
|
}
|
|
218
291
|
const found = installedIn(row, config).length;
|
|
292
|
+
const prompts = row.prompt ? installedIn(row, config, EVENTS[1]).length : 0;
|
|
219
293
|
rows.push({
|
|
220
294
|
id: row.id,
|
|
221
295
|
path,
|
|
222
296
|
state: found > 0 ? 'present' : 'absent',
|
|
223
297
|
entries: found,
|
|
298
|
+
prompt: row.prompt ? (prompts > 0 ? 'present' : 'absent') : null,
|
|
224
299
|
note: found > 0 ? (row.trust ?? null) : null,
|
|
225
300
|
});
|
|
226
301
|
}
|
|
@@ -243,6 +318,10 @@ async function hooksStatus(root, flags) {
|
|
|
243
318
|
const state = { present: 'PRESENT ', absent: 'ABSENT ', unreadable: 'INVALID ', none: 'NO FILE ', other: 'NOT OURS' }[r.state];
|
|
244
319
|
lines.push(`${state} ${r.id.padEnd(12)} ${r.path ?? '(no hooks file is documented)'}`);
|
|
245
320
|
if (r.state === 'absent') lines.push(` ${' '.repeat(12)} install: ${CLI} agent-hooks install --yes`);
|
|
321
|
+
// The second event is a separate answer to a separate question: a harness can carry
|
|
322
|
+
// the tool hook and not the delivery, and a reader who is told "PRESENT" would
|
|
323
|
+
// otherwise conclude the packet arrives on every prompt.
|
|
324
|
+
if (r.prompt) lines.push(` ${' '.repeat(12)} prompt-submit: ${r.prompt}`);
|
|
246
325
|
if (r.note) lines.push(` ${' '.repeat(12)} ${r.note}`);
|
|
247
326
|
}
|
|
248
327
|
if (problems.length > 0) {
|
package/src/commands/bench.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import { buildBenchReport } from '../bench/report.mjs';
|
|
|
15
15
|
import { formatRecall, measureRecall } from '../index/map.mjs';
|
|
16
16
|
import { ablateBench } from '../bench/ablate.mjs';
|
|
17
17
|
import { runBenchProbes } from '../bench/probes.mjs';
|
|
18
|
+
import { selectRuleVariants } from '../bench/variants.mjs';
|
|
18
19
|
import { listFlag } from './skills.mjs';
|
|
19
20
|
|
|
20
21
|
export async function benchMineCommand(positional, flags) {
|
|
@@ -280,16 +281,8 @@ export async function benchAblateCommand(positional, flags) {
|
|
|
280
281
|
emitJson({
|
|
281
282
|
ok: true,
|
|
282
283
|
command: 'bench ablate',
|
|
283
|
-
root: result.root,
|
|
284
284
|
dryRun,
|
|
285
|
-
|
|
286
|
-
pricesLoaded: result.pricesLoaded,
|
|
287
|
-
canarySize: result.canarySize,
|
|
288
|
-
canaryTasks: result.canaryTasks,
|
|
289
|
-
totalTasks: result.totalTasks,
|
|
290
|
-
sections: result.sections,
|
|
291
|
-
ranking: result.ranking,
|
|
292
|
-
deletionCandidates: result.deletionCandidates,
|
|
285
|
+
...result,
|
|
293
286
|
});
|
|
294
287
|
return EXIT.OK;
|
|
295
288
|
}
|
|
@@ -340,13 +333,8 @@ export async function benchProbesCommand(positional, flags) {
|
|
|
340
333
|
emitJson({
|
|
341
334
|
ok: true,
|
|
342
335
|
command: 'bench probes',
|
|
343
|
-
root: result.root,
|
|
344
336
|
dryRun,
|
|
345
|
-
|
|
346
|
-
probeCount: result.probeCount,
|
|
347
|
-
probes: result.probes,
|
|
348
|
-
arms: result.arms,
|
|
349
|
-
runs: result.runs,
|
|
337
|
+
...result,
|
|
350
338
|
});
|
|
351
339
|
return EXIT.OK;
|
|
352
340
|
}
|
|
@@ -367,5 +355,42 @@ export async function benchProbesCommand(positional, flags) {
|
|
|
367
355
|
return EXIT.OK;
|
|
368
356
|
}
|
|
369
357
|
|
|
358
|
+
export async function benchVariantsCommand(positional, flags) {
|
|
359
|
+
const root = resolve(flags.root ?? positional[0] ?? process.cwd());
|
|
360
|
+
const { dryRun, apply } = writeMode(flags);
|
|
361
|
+
let result;
|
|
362
|
+
try {
|
|
363
|
+
result = await selectRuleVariants(root, {
|
|
364
|
+
apply,
|
|
365
|
+
harness: flags.harness ?? 'gold',
|
|
366
|
+
model: flags.model,
|
|
367
|
+
reps: flags.reps ?? 1,
|
|
368
|
+
includeAll: flags.all === true,
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return fail(
|
|
372
|
+
`${root}: ${error.message ?? error}`,
|
|
373
|
+
EXIT.CONFIG,
|
|
374
|
+
`mine tasks first with \`${CLI} bench mine --yes\` and rerun \`${CLI} bench variants --yes\``,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (isJson(flags)) {
|
|
378
|
+
emitJson({ ok: true, command: 'bench variants', dryRun, ...result });
|
|
379
|
+
return EXIT.OK;
|
|
380
|
+
}
|
|
381
|
+
const lines = [
|
|
382
|
+
`bench variants ${result.root}`,
|
|
383
|
+
`mode ${result.applied ? 'applied' : dryRun ? 'dry-run (writes nothing)' : 'preview / stored runs (pass --yes to execute canary and lock winners)'}`,
|
|
384
|
+
`harness ${result.harness} model=${result.model}`,
|
|
385
|
+
`slots ${result.slots.length} (canary: ${result.canaryTasks}/${result.totalTasks} tasks)`,
|
|
386
|
+
...result.slots.map(
|
|
387
|
+
(s) => `slot ${s.slotId} status=${s.status} winner=${s.winner} dR=${s.dR} dC=${s.dC} n=${s.n} p=${s.p}`,
|
|
388
|
+
),
|
|
389
|
+
];
|
|
390
|
+
if (!result.applied) lines.push(`remedy run: ${CLI} bench variants --yes`);
|
|
391
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
392
|
+
return EXIT.OK;
|
|
393
|
+
}
|
|
394
|
+
|
|
370
395
|
|
|
371
396
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `simt brief "<task>"` — print the packet (plan 0003 R27).
|
|
2
|
+
//
|
|
3
|
+
// What this is for: an agent that has to explore a repository to find out how to run its
|
|
4
|
+
// tests, which files the task will touch and what governs them spends turns discovering
|
|
5
|
+
// what the repository already knows. The packet is that answer, assembled from the index
|
|
6
|
+
// the repository already has, in the bytes a prompt can carry.
|
|
7
|
+
//
|
|
8
|
+
// The assembly itself is in `../brief/assemble.mjs`, because three doors open onto it —
|
|
9
|
+
// this command, MCP's `brief` tool and the prompt-submit hook — and one implementation
|
|
10
|
+
// behind three doors is the only way the three cannot disagree. This file owns the
|
|
11
|
+
// command line: what a task may be, what the ceiling may be, and how the packet is
|
|
12
|
+
// rendered for a terminal or for `--json`.
|
|
13
|
+
//
|
|
14
|
+
// One thing is deliberately missing: `skills brief` keeps its own behaviour, and MCP's
|
|
15
|
+
// `skills_brief` keeps its own shape. Superseding them changes two accepted decisions
|
|
16
|
+
// (ADR-035, ADR-052) and the MCP contract, and this slice carries no ADR to do it.
|
|
17
|
+
|
|
18
|
+
import { CLI } from '../name.mjs';
|
|
19
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
20
|
+
import { DEFAULT_BRIEF_BYTES, explainPacket } from '../brief/packet.mjs';
|
|
21
|
+
import { assembleBrief } from '../brief/assemble.mjs';
|
|
22
|
+
|
|
23
|
+
export async function briefCommand(args, flags) {
|
|
24
|
+
if (args.length > 1) {
|
|
25
|
+
return fail(`brief takes one task, not ${args.length}`, EXIT.USAGE, `run: ${CLI} brief "<task>"`);
|
|
26
|
+
}
|
|
27
|
+
if (args.length === 1 && flags.task !== undefined) {
|
|
28
|
+
return fail('the task was passed twice, as an argument and as --task', EXIT.USAGE, `run: ${CLI} brief "<task>"`);
|
|
29
|
+
}
|
|
30
|
+
const budget = flags.budget === undefined ? DEFAULT_BRIEF_BYTES : Number(flags.budget);
|
|
31
|
+
if (!Number.isInteger(budget) || budget < 0) {
|
|
32
|
+
return fail(`--budget must be a byte count, not ${flags.budget}`, EXIT.USAGE, `run: ${CLI} brief --budget ${DEFAULT_BRIEF_BYTES}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await assembleBrief({
|
|
36
|
+
root: flags.root ?? process.cwd(),
|
|
37
|
+
task: String(args[0] ?? flags.task ?? ''),
|
|
38
|
+
paths: flags.paths === undefined ? null : flags.paths,
|
|
39
|
+
budget,
|
|
40
|
+
flags,
|
|
41
|
+
});
|
|
42
|
+
if (result.error) return result.error;
|
|
43
|
+
// Unreachable from here — only a delivered brief asks for a cold index to be reported
|
|
44
|
+
// rather than built — but a silent empty packet would be the worst way to find out.
|
|
45
|
+
if (result.cold) {
|
|
46
|
+
return fail('the index is empty, and this command will not assemble a packet from it', EXIT.UNSUPPORTED, `run ${CLI} index`);
|
|
47
|
+
}
|
|
48
|
+
const { root, kind, basis, task, scope, budget: ceiling, items, packed, sections } = result;
|
|
49
|
+
const cut = new Set(packed.cut);
|
|
50
|
+
|
|
51
|
+
if (isJson(flags)) {
|
|
52
|
+
emitJson(
|
|
53
|
+
{
|
|
54
|
+
ok: true,
|
|
55
|
+
command: 'brief',
|
|
56
|
+
root,
|
|
57
|
+
kind,
|
|
58
|
+
basis,
|
|
59
|
+
task,
|
|
60
|
+
scope,
|
|
61
|
+
budget: ceiling,
|
|
62
|
+
bytes: packed.after,
|
|
63
|
+
before: packed.before,
|
|
64
|
+
floorHeld: packed.floorHeld,
|
|
65
|
+
cut: packed.cut,
|
|
66
|
+
sections,
|
|
67
|
+
explain: flags.explain === true ? explainPacket(items, { cut }) : undefined,
|
|
68
|
+
text: packed.text,
|
|
69
|
+
},
|
|
70
|
+
{ pretty: flags.pretty === true, maxBytes: Number(flags.maxBytes ?? 0) || 0 },
|
|
71
|
+
);
|
|
72
|
+
return EXIT.OK;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const body = flags.explain === true ? [packed.text, '', 'EXPLAIN', ...explainPacket(items, { cut })] : [packed.text];
|
|
76
|
+
emit(body.join('\n'), { maxBytes: Number(flags.maxBytes ?? 0) || 0 });
|
|
77
|
+
return EXIT.OK;
|
|
78
|
+
}
|
package/src/commands/compare.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { provenanceDigest } from '../projection/project.mjs';
|
|
|
26
26
|
import { GATES } from '../hooks/gates.mjs';
|
|
27
27
|
import { buildRow, summarise } from '../compare/row.mjs';
|
|
28
28
|
import { discover } from '../compare/discover.mjs';
|
|
29
|
+
import { learnAcrossEstate, renderLearningLines } from '../compare/learn.mjs';
|
|
29
30
|
import { targetRoot, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
30
31
|
|
|
31
32
|
/** Gate kinds, so a row can tell a command gate with no command from a script gate. */
|
|
@@ -65,6 +66,7 @@ export async function compareCommand(positional, flags) {
|
|
|
65
66
|
// bound. Determinism of the output matters more than wall clock on a dashboard.
|
|
66
67
|
const rows = [];
|
|
67
68
|
for (const repo of found.repos) rows.push(await buildRow(repo, context));
|
|
69
|
+
const learning = await learnAcrossEstate(found.repos);
|
|
68
70
|
|
|
69
71
|
const summary = summarise(rows, {
|
|
70
72
|
root,
|
|
@@ -106,6 +108,7 @@ export async function compareCommand(positional, flags) {
|
|
|
106
108
|
shas: summary.shas,
|
|
107
109
|
},
|
|
108
110
|
rows,
|
|
111
|
+
learning,
|
|
109
112
|
worktrees: found.worktrees.map((w) => ({
|
|
110
113
|
name: w.name,
|
|
111
114
|
path: w.path,
|
|
@@ -117,7 +120,7 @@ export async function compareCommand(positional, flags) {
|
|
|
117
120
|
return rows.some((r) => r.problems.length > 0) ? EXIT.FAIL : EXIT.OK;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
emit(render(rows, summary, found), { maxBytes: flags.maxBytes ?? 0 });
|
|
123
|
+
emit(render(rows, summary, found, learning), { maxBytes: flags.maxBytes ?? 0 });
|
|
121
124
|
if (summary.shape === 'repo') {
|
|
122
125
|
note('note one repository compared; pass --root at a workspace directory to compare its siblings');
|
|
123
126
|
} else if (target.source === 'none') {
|
|
@@ -146,7 +149,7 @@ export async function compareCommand(positional, flags) {
|
|
|
146
149
|
* the longest value, and an agent cannot tell which column is which without
|
|
147
150
|
* counting spaces.
|
|
148
151
|
*/
|
|
149
|
-
function render(rows, summary, found) {
|
|
152
|
+
function render(rows, summary, found, learning = null) {
|
|
150
153
|
const lines = [
|
|
151
154
|
// `workspace` and `repo` are the same field with different names, and the name is the
|
|
152
155
|
// honest one: a reader who sees "workspace" and gets one repo was told a lie. `root`
|
|
@@ -204,6 +207,11 @@ function render(rows, summary, found) {
|
|
|
204
207
|
lines.push('');
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
const learningLines = renderLearningLines(learning);
|
|
211
|
+
if (learningLines.length > 0) {
|
|
212
|
+
lines.push(...learningLines, '');
|
|
213
|
+
}
|
|
214
|
+
|
|
207
215
|
return lines.join('\n').trimEnd();
|
|
208
216
|
}
|
|
209
217
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `simt decide` — scaffold an ADR with the next free number, a Status: line,
|
|
2
|
+
// a rulings skeleton, and structured `simt-affects` + `simt-supersedes` frontmatter
|
|
3
|
+
// (plan 0003 R34).
|
|
4
|
+
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
7
|
+
import { CLI } from '../name.mjs';
|
|
8
|
+
import { DECISIONS_DIR } from '../decisions/index.mjs';
|
|
9
|
+
import { lintDecisions, scaffoldDecision } from '../memory/decide.mjs';
|
|
10
|
+
import { parsePathList } from '../argv.mjs';
|
|
11
|
+
|
|
12
|
+
export async function decideCommand(args, flags) {
|
|
13
|
+
const root = flags.root ?? process.cwd();
|
|
14
|
+
|
|
15
|
+
if (flags.check === true) {
|
|
16
|
+
const decisionsDir = join(root, 'docs', 'decisions');
|
|
17
|
+
const allowHistorical = resolve(decisionsDir) === DECISIONS_DIR;
|
|
18
|
+
const res = await lintDecisions(decisionsDir, { allowHistorical });
|
|
19
|
+
|
|
20
|
+
if (isJson(flags)) {
|
|
21
|
+
emitJson({
|
|
22
|
+
ok: res.ok,
|
|
23
|
+
command: 'decide --check',
|
|
24
|
+
root,
|
|
25
|
+
findings: res.findings,
|
|
26
|
+
});
|
|
27
|
+
return res.ok ? EXIT.OK : EXIT.FAIL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (res.findings.length === 0) {
|
|
31
|
+
emit('decisions ok', { maxBytes: flags.maxBytes ?? 0 });
|
|
32
|
+
return EXIT.OK;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit(
|
|
36
|
+
res.findings
|
|
37
|
+
.flatMap((f) => [
|
|
38
|
+
`${f.status.toUpperCase()} ${f.file} ${f.detail}`,
|
|
39
|
+
...(f.remedy ? [` remedy: ${f.remedy}`] : []),
|
|
40
|
+
])
|
|
41
|
+
.join('\n'),
|
|
42
|
+
{ maxBytes: flags.maxBytes ?? 0 },
|
|
43
|
+
);
|
|
44
|
+
return EXIT.FAIL;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mode = writeMode(flags);
|
|
48
|
+
const title = args.join(' ').trim() || flags.task || 'Untitled decision';
|
|
49
|
+
const parsedPaths = parsePathList(flags.paths);
|
|
50
|
+
const explicitPaths = flags.paths !== undefined ? parsedPaths : undefined;
|
|
51
|
+
|
|
52
|
+
const res = await scaffoldDecision(root, {
|
|
53
|
+
title,
|
|
54
|
+
affects: explicitPaths,
|
|
55
|
+
supersedes: flags.supersedes,
|
|
56
|
+
write: mode.apply,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
return fail(
|
|
61
|
+
res.errors[0],
|
|
62
|
+
EXIT.USAGE,
|
|
63
|
+
`usage: ${CLI} decide "<title>" [--supersedes ADR-NNN:<ruling>] [--yes]`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (mode.apply) {
|
|
68
|
+
note(`wrote ${res.relPath}`);
|
|
69
|
+
} else if (flags.dryRun === true) {
|
|
70
|
+
note('dry-run no files written');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isJson(flags)) {
|
|
74
|
+
emitJson({
|
|
75
|
+
ok: true,
|
|
76
|
+
command: 'decide',
|
|
77
|
+
root,
|
|
78
|
+
id: res.id,
|
|
79
|
+
number: res.number,
|
|
80
|
+
title: res.title,
|
|
81
|
+
file: res.file,
|
|
82
|
+
path: res.relPath,
|
|
83
|
+
affects: res.affects,
|
|
84
|
+
supersedes: res.supersedes,
|
|
85
|
+
written: res.written,
|
|
86
|
+
dryRun: !mode.apply,
|
|
87
|
+
});
|
|
88
|
+
return EXIT.OK;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const supersedesLabel =
|
|
92
|
+
res.supersedes.length > 0
|
|
93
|
+
? res.supersedes
|
|
94
|
+
.map((s) => (s.ruling !== null ? `${s.adr} ruling ${s.ruling} (${s.kind})` : s.adr))
|
|
95
|
+
.join('; ')
|
|
96
|
+
: '(none)';
|
|
97
|
+
|
|
98
|
+
const lines = [
|
|
99
|
+
`id ${res.id}`,
|
|
100
|
+
`title ${res.title}`,
|
|
101
|
+
`file ${res.relPath}`,
|
|
102
|
+
`affects ${res.affects.length > 0 ? res.affects.join(', ') : '(none)'}`,
|
|
103
|
+
`supersedes ${supersedesLabel}`,
|
|
104
|
+
'',
|
|
105
|
+
res.text.trimEnd(),
|
|
106
|
+
'',
|
|
107
|
+
mode.apply
|
|
108
|
+
? `wrote ${res.relPath}`
|
|
109
|
+
: flags.dryRun === true
|
|
110
|
+
? 'dry-run; nothing written'
|
|
111
|
+
: `would write ${res.relPath}; apply with: ${CLI} decide ${JSON.stringify(res.title)} --yes`,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
115
|
+
return EXIT.OK;
|
|
116
|
+
}
|
package/src/commands/help.mjs
CHANGED
|
@@ -16,6 +16,12 @@ ${CLI} upgrade move every consumer to this simt-dev SHA, as one s
|
|
|
16
16
|
(dry-run unless --yes; moves a pin only - it never
|
|
17
17
|
adopts or repairs a repo; never commits or pushes)
|
|
18
18
|
|
|
19
|
+
${CLI} brief "<task>" one packet for a task: the kind, the commands, the
|
|
20
|
+
likely files, the tests and the decisions that govern
|
|
21
|
+
them, inside --budget bytes (default 2048; 0 is
|
|
22
|
+
unlimited). Says how many items it cut; --explain
|
|
23
|
+
says why each was kept or dropped
|
|
24
|
+
|
|
19
25
|
${CLI} skills index tier-0 catalog: every skill, one line each
|
|
20
26
|
${CLI} skills brief --task tier-1 pre-assembled brief <- the common case
|
|
21
27
|
${CLI} skills show <name> tier-2 body (respects --max-bytes)
|
|
@@ -28,6 +34,9 @@ ${CLI} skills eval [corpus] measure the resolver against a gold corpus
|
|
|
28
34
|
${CLI} skills install <name> pin a skill into the repo as a local override
|
|
29
35
|
(dry-run unless --yes; a multi-variant skill needs
|
|
30
36
|
--variant or --from rather than a guess)
|
|
37
|
+
${CLI} skills compile [<name>] compile a skill to Agent Skills standard, flag bodies
|
|
38
|
+
over 5000 tokens with a split plan, and report per-skill
|
|
39
|
+
trigger recall and precision from R02 transcripts
|
|
31
40
|
|
|
32
41
|
${CLI} rules list the rule catalogue (--root marks what applies to a repo)
|
|
33
42
|
${CLI} rules show <id> one rule body
|
|
@@ -39,7 +48,8 @@ ${CLI} hooks install [--yes] install the shared dispatcher; report what it wou
|
|
|
39
48
|
${CLI} hooks status what each git event routes to, and whether it is wired
|
|
40
49
|
${CLI} hooks run <event> run one event's gates now, as git would
|
|
41
50
|
(pre-commit, pre-push, post-merge, ...)
|
|
42
|
-
${CLI} agent-hook <event> the entry point a harness calls
|
|
51
|
+
${CLI} agent-hook <event> the entry point a harness calls: pre-tool-use decides,
|
|
52
|
+
prompt-submit delivers the packet as prompt context
|
|
43
53
|
(reads one JSON payload on stdin and answers in that
|
|
44
54
|
harness's dialect; exit 2 blocks; needs --harness)
|
|
45
55
|
${CLI} agent-hooks install write this repo's policies into the harnesses that
|
|
@@ -82,6 +92,17 @@ ${CLI} map [--task "<text>"] [--level outline|names|signatures]
|
|
|
82
92
|
${CLI} evidence the runs recorded against the tree on disk
|
|
83
93
|
${CLI} evidence check <file> a commit message that claims a run this tree does not
|
|
84
94
|
have: a warning, or a block when gates.evidence is "block"
|
|
95
|
+
${CLI} lessons list deterministic lesson candidates in .simt-dev/lessons.jsonl
|
|
96
|
+
and promoted lessons in docs/lessons/*.md
|
|
97
|
+
${CLI} lessons review [<id>] promote a lesson candidate to docs/lessons/NNNN-<slug>.md
|
|
98
|
+
(dry-run unless --yes; writes Appendix A frontmatter)
|
|
99
|
+
${CLI} decide [<title>] scaffold an ADR with the next free number, a Status: line,
|
|
100
|
+
a rulings skeleton, and structured simt-affects +
|
|
101
|
+
simt-supersedes frontmatter (dry-run unless --yes;
|
|
102
|
+
--check lints ADRs for prose-only partial supersessions)
|
|
103
|
+
${CLI} local doctor probe configured local model endpoints (chat and embed)
|
|
104
|
+
(reads local in ${MANIFEST_FILE}; opens no socket when
|
|
105
|
+
unset; refuses non-loopback URLs without --allow-remote)
|
|
85
106
|
${CLI} shared plan what a workspace's repeated rules cost to carry
|
|
86
107
|
once: the shared set, its bytes, and every path that would be written
|
|
87
108
|
(read-only; touches nothing, not even your home directory)
|
|
@@ -117,6 +138,9 @@ ${CLI} bench ablate [--yes] leave-one-out ablation across core, profile and p
|
|
|
117
138
|
${CLI} bench probes [--yes] 12 synthetic rule-compliance probes with deterministic graders
|
|
118
139
|
(dry-run unless --yes; grades git state, transcript and ledger,
|
|
119
140
|
and reports compliance per arm)
|
|
141
|
+
${CLI} bench variants [--yes] evaluate simt-variants rule slots on the canary suite and lock
|
|
142
|
+
winning variants in simt-dev.lock.json (dry-run unless --yes;
|
|
143
|
+
re-opens when model or harness changes)
|
|
120
144
|
${CLI} mcp the read-only surface as MCP tools over stdio
|
|
121
145
|
(--list prints it without reading stdin; init, sync,
|
|
122
146
|
upgrade and the installers stay CLI-only by design)`;
|
|
@@ -124,7 +148,8 @@ ${CLI} mcp the read-only surface as MCP tools over stdio
|
|
|
124
148
|
const FLAGS = `--json structured output (compact; one document, one line)
|
|
125
149
|
--pretty indent --json for a human
|
|
126
150
|
--max-bytes <n> hard ceiling on emitted bytes (0 = unlimited, default 0)
|
|
127
|
-
--budget <n> context budget for
|
|
151
|
+
--budget <n> context budget for a brief (default 2048)
|
|
152
|
+
(skills brief keeps its own default of 20480)
|
|
128
153
|
--plan emit a fetch plan instead of inlining skill bodies
|
|
129
154
|
--skill <name> force a skill into the brief (repeatable)
|
|
130
155
|
--task "<text>" the task text used for capability resolution
|
|
@@ -142,7 +167,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
142
167
|
--frozen check: the lock must match this projection; an output
|
|
143
168
|
change no input change explains fails the gate
|
|
144
169
|
--explain sync: name each line dropped as already inferable, with
|
|
145
|
-
the fact behind it
|
|
170
|
+
the fact behind it; brief: name every item and why it
|
|
171
|
+
was kept or cut, with its section's value
|
|
146
172
|
--all ignore a cap: every ruling of a record, or the skills budget
|
|
147
173
|
--variant <id> which body of a multi-variant skill to use
|
|
148
174
|
--from <repo/path> install from a declared source by name
|
|
@@ -176,6 +202,11 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
176
202
|
--compare <a> <b> bench report: compare two arms with deltas and p-values
|
|
177
203
|
--test <cmd> plan done: the test command that closes the row
|
|
178
204
|
--state <name> plan done: target state (default: Complete)
|
|
205
|
+
--level <name> map: detail level (outline, names, signatures)
|
|
206
|
+
--supersedes <spec> decide: superseded ADR or ruling (ADR-NNN or ADR-NNN:<ruling>[:<kind>]; repeatable)
|
|
207
|
+
--allow-remote local doctor: allow non-loopback model URLs (refused by default)
|
|
208
|
+
--embed index / map / brief: compute or query optional local vector embeddings
|
|
209
|
+
(off by default unless recall@10 gains at least 10 points)
|
|
179
210
|
--help this text
|
|
180
211
|
--version the version, and the SHA it was built from`;
|
|
181
212
|
|
package/src/commands/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
whyCommand as skillsWhy,
|
|
26
26
|
evalCommand as skillsEval,
|
|
27
27
|
} from './skills.mjs';
|
|
28
|
+
import { briefCommand } from './brief.mjs';
|
|
28
29
|
import { syncCommand, checkCommand } from './sync.mjs';
|
|
29
30
|
import { initCommand } from './init.mjs';
|
|
30
31
|
import { ciCommand } from './ci.mjs';
|
|
@@ -34,6 +35,7 @@ import { compareCommand } from './compare.mjs';
|
|
|
34
35
|
import { upgradeCommand } from './upgrade.mjs';
|
|
35
36
|
import { mcpCommand } from './mcp.mjs';
|
|
36
37
|
import { installCommand } from './skillsinstall.mjs';
|
|
38
|
+
import { compileCommand } from './skillscompile.mjs';
|
|
37
39
|
import { listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
|
|
38
40
|
import { tokensCommand } from './tokens.mjs';
|
|
39
41
|
import { harnessCommand, harnessLoadersCommand } from './harness.mjs';
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
benchReportCommand,
|
|
44
46
|
benchAblateCommand,
|
|
45
47
|
benchProbesCommand,
|
|
48
|
+
benchVariantsCommand,
|
|
46
49
|
} from './bench.mjs';
|
|
47
50
|
import { planCommand } from './plan.mjs';
|
|
48
51
|
import { sharedPlanCommand, sharedInstallCommand, sharedWarning } from './shared.mjs';
|
|
@@ -53,7 +56,11 @@ import { testsCommand } from './tests.mjs';
|
|
|
53
56
|
import { whyCommand } from './why.mjs';
|
|
54
57
|
import { gateCommand } from './gate.mjs';
|
|
55
58
|
import { impactCommand } from './impact.mjs';
|
|
59
|
+
import { lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
|
|
60
|
+
import { decideCommand } from './decide.mjs';
|
|
61
|
+
import { localDoctorCommand } from './local.mjs';
|
|
56
62
|
import { parse } from '../argv.mjs';
|
|
63
|
+
import { CLI } from '../name.mjs';
|
|
57
64
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
58
65
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
59
66
|
|
|
@@ -79,6 +86,8 @@ const GROUPS = new Map([
|
|
|
79
86
|
// where", and `install` — the subcommand that touches a reader's home directory — is the
|
|
80
87
|
// one that needs `--yes` before it does anything.
|
|
81
88
|
['shared', 'plan'],
|
|
89
|
+
['lessons', 'list'],
|
|
90
|
+
['local', 'doctor'],
|
|
82
91
|
]);
|
|
83
92
|
|
|
84
93
|
/**
|
|
@@ -95,6 +104,7 @@ const GROUPS = new Map([
|
|
|
95
104
|
* just correct: nothing in the map depends on the call.
|
|
96
105
|
*/
|
|
97
106
|
export const IMPLEMENTED = {
|
|
107
|
+
brief: briefCommand,
|
|
98
108
|
doctor: runDoctor,
|
|
99
109
|
'graph lint': lintCommand,
|
|
100
110
|
'skills index': skillsIndex,
|
|
@@ -104,6 +114,7 @@ export const IMPLEMENTED = {
|
|
|
104
114
|
'skills why': skillsWhy,
|
|
105
115
|
'skills eval': skillsEval,
|
|
106
116
|
'skills install': installCommand,
|
|
117
|
+
'skills compile': compileCommand,
|
|
107
118
|
'rules list': rulesList,
|
|
108
119
|
'rules show': rulesShow,
|
|
109
120
|
'rules why': rulesWhy,
|
|
@@ -121,6 +132,10 @@ export const IMPLEMENTED = {
|
|
|
121
132
|
why: (args, flags) => whyCommand(args, flags, COMMANDS),
|
|
122
133
|
gate: gateCommand,
|
|
123
134
|
impact: (args, flags) => impactCommand(args, flags, COMMANDS),
|
|
135
|
+
'lessons list': lessonsListCommand,
|
|
136
|
+
'lessons review': lessonsReviewCommand,
|
|
137
|
+
decide: decideCommand,
|
|
138
|
+
'local doctor': localDoctorCommand,
|
|
124
139
|
'evidence check': (args, flags) => evidenceCommand(['check', ...args], flags),
|
|
125
140
|
init: initCommand,
|
|
126
141
|
ci: ciCommand,
|
|
@@ -139,6 +154,7 @@ export const IMPLEMENTED = {
|
|
|
139
154
|
'bench report': benchReportCommand,
|
|
140
155
|
'bench ablate': benchAblateCommand,
|
|
141
156
|
'bench probes': benchProbesCommand,
|
|
157
|
+
'bench variants': benchVariantsCommand,
|
|
142
158
|
upgrade: upgradeCommand,
|
|
143
159
|
mcp: mcpCommand,
|
|
144
160
|
};
|
|
@@ -186,7 +202,7 @@ export async function dispatch(argv) {
|
|
|
186
202
|
// simt-dev: they run this tool, not its source tree. The step number is the
|
|
187
203
|
// stable handle; the file that documents it is an implementation detail.
|
|
188
204
|
return fail(
|
|
189
|
-
|
|
205
|
+
`\`${CLI} ${path}\` is declared but not implemented`,
|
|
190
206
|
EXIT.UNSUPPORTED,
|
|
191
207
|
`implementation step ${step} is not built yet`,
|
|
192
208
|
);
|