simt-dev 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -1
- package/README.md +46 -17
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/README.md +53 -0
- package/package.json +4 -2
- package/rules/core.md +2 -2
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +10 -11
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +4 -3
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +238 -15
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/prompt.mjs +6 -2
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shell.mjs +523 -0
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/argv.mjs +62 -9
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/mine.mjs +48 -1
- package/src/bench/probes.mjs +5 -6
- package/src/bench/report.mjs +243 -80
- package/src/bench/run.mjs +12 -11
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +6 -0
- package/src/ci/workflow.mjs +16 -7
- package/src/commands/agent.mjs +206 -0
- package/src/commands/agenthook.mjs +133 -17
- package/src/commands/bench.mjs +2 -1
- package/src/commands/ci.mjs +28 -3
- package/src/commands/compare.mjs +8 -7
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +20 -6
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/evidence.mjs +13 -3
- package/src/commands/explain.mjs +52 -0
- package/src/commands/gate.mjs +85 -19
- package/src/commands/graph.mjs +18 -5
- package/src/commands/help.mjs +96 -26
- package/src/commands/hooks.mjs +258 -67
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/impact.mjs +4 -2
- package/src/commands/index.mjs +69 -102
- package/src/commands/init.mjs +239 -37
- package/src/commands/lease.mjs +304 -0
- package/src/commands/lessons.mjs +77 -9
- package/src/commands/map.mjs +33 -18
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +17 -2
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +4 -0
- package/src/commands/rules.mjs +219 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +329 -206
- package/src/commands/skillscompile.mjs +8 -7
- package/src/commands/skillsinstall.mjs +112 -30
- package/src/commands/slice.mjs +712 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +146 -36
- package/src/commands/tests.mjs +5 -2
- package/src/commands/undo.mjs +114 -0
- package/src/commands/upgrade.mjs +103 -9
- package/src/commands/why.mjs +10 -4
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -1
- package/src/completions.mjs +196 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +254 -0
- package/src/distill/distill.mjs +3 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +270 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +52 -24
- package/src/hooks/capability.mjs +14 -1
- package/src/hooks/dispatcher.mjs +441 -9
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +23 -3
- package/src/hooks/shim.mjs +12 -7
- package/src/identity.mjs +67 -14
- package/src/ignore.mjs +133 -0
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +372 -55
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +545 -0
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +195 -0
- package/src/ledger/index.mjs +91 -25
- package/src/mcp/resources.mjs +5 -3
- package/src/mcp/server.mjs +86 -13
- package/src/mcp/tools.mjs +340 -264
- package/src/memory/decide.mjs +98 -38
- package/src/memory/lessons.mjs +180 -33
- package/src/name.mjs +9 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +37 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +33 -8
- package/src/prbody.mjs +253 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +21 -14
- package/src/projection/manifest.mjs +82 -21
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +54 -3
- package/src/projection/shared.mjs +4 -1
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +69 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +5 -0
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +4 -2
- package/src/secrets.mjs +374 -0
- package/src/state.mjs +76 -0
- package/src/status.mjs +157 -0
- package/src/toolwords.mjs +5 -0
- package/src/tsconfig.mjs +39 -99
- package/templates/plan/tracker.json +1 -1
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
// tool call. Failures of this command exit 1, which a harness reports as a failed hook,
|
|
23
23
|
// and that is the right blast radius for a hook that is broken rather than refusing.
|
|
24
24
|
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { existsSync } from 'node:fs';
|
|
26
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
27
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
28
|
+
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
29
|
+
import { AGENT_HOOK_SHIM, renderAgentHookShim } from '../agenthooks/shim.mjs';
|
|
30
|
+
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
27
31
|
|
|
28
32
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
29
|
-
import { CLI } from '../name.mjs';
|
|
30
|
-
import { ALLOW_ENV, EVENTS, POLICY_IDS } from '../agenthooks/policy.mjs';
|
|
33
|
+
import { CLI, MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
|
|
34
|
+
import { ALLOW_ENV, EVENTS, POLICY_IDS, SESSION_ENV } from '../agenthooks/policy.mjs';
|
|
31
35
|
import { decide, factsFor } from '../agenthooks/decide.mjs';
|
|
36
|
+
import { repoRoot } from '../repo-root.mjs';
|
|
32
37
|
import {
|
|
33
38
|
adapter,
|
|
34
39
|
adapters,
|
|
@@ -95,10 +100,33 @@ export async function agentHookCommand(args, flags) {
|
|
|
95
100
|
// `--root` outranks the payload: a packaged hook runs from the plugin directory and the
|
|
96
101
|
// harness's environment variable names the project, so a payload cwd that is the plugin's
|
|
97
102
|
// own working directory must not win. The repository-install path passes no flag, and the
|
|
98
|
-
// payload's cwd is what the harness reports.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
// payload's cwd is what the harness reports. The answer is then resolved to the
|
|
104
|
+
// repository's top level: a harness reports the directory it happens to be in, and an
|
|
105
|
+
// agent works from subdirectories, so every repository-relative fact (the lease file,
|
|
106
|
+
// `.gitmodules`) must resolve against the root git itself uses, not the cwd (review
|
|
107
|
+
// 0003 Q16).
|
|
108
|
+
const { root: payloadRoot, command, writes, session: payloadSession } = factsFrom(row, payload, { root: flags.root });
|
|
109
|
+
const root = repoRoot(payloadRoot);
|
|
110
|
+
// A relative write path is relative to the directory the harness is in, not to the
|
|
111
|
+
// repository root the facts resolve against: an agent editing `keep.mjs` from `src/`
|
|
112
|
+
// means `src/keep.mjs`, and a path joined to the wrong base is a path the lease file
|
|
113
|
+
// will never meet (review 0003 Q16).
|
|
114
|
+
// Writes are `{path, ...edit}` rows (Q17): the path resolves against the harness
|
|
115
|
+
// cwd, the edit detail travels untouched so the region check sees the old_string.
|
|
116
|
+
const writesAbsolute = writes.map((w) => {
|
|
117
|
+
const path = typeof w === 'string' ? w : w?.path;
|
|
118
|
+
const abs = typeof path === 'string' && isAbsolute(path) ? path : join(payloadRoot, path);
|
|
119
|
+
return typeof w === 'string' ? abs : { ...w, path: abs };
|
|
120
|
+
});
|
|
121
|
+
const facts = await factsFor(root, { command, writes: writesAbsolute });
|
|
122
|
+
// Harness hook processes rarely carry SIMT_SESSION, so the payload's own
|
|
123
|
+
// session id is the fallback: without it an agent is blocked by its own
|
|
124
|
+
// lease (review 0003 Q20). The environment still wins when it names one.
|
|
125
|
+
const session =
|
|
126
|
+
typeof process.env[SESSION_ENV] === 'string' && process.env[SESSION_ENV] !== ''
|
|
127
|
+
? process.env[SESSION_ENV]
|
|
128
|
+
: (payloadSession ?? '');
|
|
129
|
+
const decision = decide({ event, facts, env: { ...process.env, [SESSION_ENV]: session } });
|
|
102
130
|
|
|
103
131
|
// Both notes are about a conclusion the author is about to draw: a lift that did
|
|
104
132
|
// nothing because of a typo, and a lift that is the reason the command they expected to
|
|
@@ -113,7 +141,11 @@ export async function agentHookCommand(args, flags) {
|
|
|
113
141
|
const json = decisionJson(row, decision);
|
|
114
142
|
if (json === null) return EXIT.OK;
|
|
115
143
|
|
|
116
|
-
|
|
144
|
+
// A denial is recorded only for a repository that adopted the tool: hook
|
|
145
|
+
// denials in a never-adopted repo are noise about someone else's policy, and
|
|
146
|
+
// that is how an unadopted checkout accumulated 23 candidates nobody would
|
|
147
|
+
// ever promote (review 0003 Q36). The manifest is the adoption mark.
|
|
148
|
+
if (root && (existsSync(join(root, MANIFEST_FILE)) || existsSync(join(root, MANIFEST_LEGACY)))) {
|
|
117
149
|
try {
|
|
118
150
|
const { recordHookDenial } = await import('../memory/lessons.mjs');
|
|
119
151
|
await recordHookDenial(root, {
|
|
@@ -141,8 +173,24 @@ export async function agentHooksCommand(sub, args, flags) {
|
|
|
141
173
|
return fail(`unknown agent-hooks subcommand ${sub}`, EXIT.USAGE, `run: ${CLI} agent-hooks status`);
|
|
142
174
|
}
|
|
143
175
|
|
|
176
|
+
// The label for a reader the run did not write, by the state that names why:
|
|
177
|
+
// `none` documents no hooks file, `other` is a reader this tool does not write,
|
|
178
|
+
// `skipped` was an adapter the caller did not ask for (ADR-107).
|
|
179
|
+
const OTHER_LABEL = { none: 'NO FILE ', other: 'NOT OURS', skipped: 'SKIPPED ' };
|
|
180
|
+
|
|
144
181
|
async function installHooks(root, flags) {
|
|
145
182
|
const mode = writeMode(flags);
|
|
183
|
+
// `--harness` narrows the install to one reader (ADR-107). The id is validated
|
|
184
|
+
// against the same table the loop reads, so a typo fails before anything is
|
|
185
|
+
// planned; the remedy names the ids the way `agent-hook` does.
|
|
186
|
+
const selected = flags.harness;
|
|
187
|
+
if (selected !== undefined && adapter(selected) === null) {
|
|
188
|
+
return fail(
|
|
189
|
+
`unknown harness \`${selected}\``,
|
|
190
|
+
EXIT.USAGE,
|
|
191
|
+
`--harness <id>, one of: ${adapters().map((a) => a.id).join(', ')}`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
146
194
|
// The delivery is a second entry in the same config, and a repository asks for it by
|
|
147
195
|
// declaring a `brief` key rather than by having the entry. Read through a dynamic
|
|
148
196
|
// import for the same reason the hook delivers through one.
|
|
@@ -153,8 +201,9 @@ async function installHooks(root, flags) {
|
|
|
153
201
|
listPromotedLessons(root).catch(() => []),
|
|
154
202
|
]);
|
|
155
203
|
const hasLessons = promotedLessons.length > 0;
|
|
204
|
+
const rows = selected === undefined ? adapters() : adapters().filter((r) => r.id === selected);
|
|
156
205
|
const plan = [];
|
|
157
|
-
for (const row of
|
|
206
|
+
for (const row of rows) {
|
|
158
207
|
const path = hookPath(row.id);
|
|
159
208
|
const existing = await readIfExists(join(root, path));
|
|
160
209
|
const result = rendered(row, existing, { prompt: optIn.optedIn, post: hasLessons });
|
|
@@ -176,15 +225,40 @@ async function installHooks(root, flags) {
|
|
|
176
225
|
|
|
177
226
|
// The readers this tool does not write an entry for, reported rather than skipped in
|
|
178
227
|
// silence: a command that writes four files and says nothing about the fifth harness
|
|
179
|
-
// in the repository reads as "that one is covered too".
|
|
228
|
+
// in the repository reads as "that one is covered too". A narrowed install adds a
|
|
229
|
+
// third state: an adapter that is this tool's but was not requested (ADR-107).
|
|
180
230
|
const others = [];
|
|
181
231
|
for (const r of hookRows()) {
|
|
182
232
|
if (r.path === null) others.push({ id: r.id, path: null, state: 'none', note: r.note });
|
|
183
233
|
else if (adapter(r.id) === null) others.push({ id: r.id, path: r.path, state: 'other', note: r.note });
|
|
234
|
+
else if (selected !== undefined && r.id !== selected) {
|
|
235
|
+
others.push({ id: r.id, path: r.path, state: 'skipped', note: 'not requested; install without --harness to write every harness' });
|
|
236
|
+
}
|
|
184
237
|
}
|
|
185
238
|
|
|
186
239
|
const refused = plan.filter((p) => p.error);
|
|
187
240
|
|
|
241
|
+
// The shim every harness config points at: without it the entries above name a
|
|
242
|
+
// binary no harness has on PATH (review 0003 Q21). Its remedy installs the
|
|
243
|
+
// dependency the same way the git shims' does — a devDependency where the
|
|
244
|
+
// repository has a package.json, the release binary where it does not.
|
|
245
|
+
const shimText = renderAgentHookShim({ npm: (await readIfExists(join(root, 'package.json'))) !== null });
|
|
246
|
+
const shimBefore = await readBefore(join(root, AGENT_HOOK_SHIM));
|
|
247
|
+
const shimAction = shimBefore === null ? 'create' : shimBefore === shimText ? 'unchanged' : 'update';
|
|
248
|
+
const shimRow = { id: 'shim', path: AGENT_HOOK_SHIM, action: shimAction };
|
|
249
|
+
|
|
250
|
+
// The `--diff` pairs, read before anything is written so the before side is
|
|
251
|
+
// true in preview and in apply alike (R50).
|
|
252
|
+
const diffs =
|
|
253
|
+
flags.diff === true
|
|
254
|
+
? await Promise.all(
|
|
255
|
+
plan
|
|
256
|
+
.filter((p) => p.action !== 'unchanged')
|
|
257
|
+
.map(async (p) => ({ path: p.path, before: await readBefore(join(root, p.path)), after: p.text }))
|
|
258
|
+
.concat(shimAction === 'unchanged' ? [] : [{ path: AGENT_HOOK_SHIM, before: shimBefore, after: shimText }]),
|
|
259
|
+
)
|
|
260
|
+
: [];
|
|
261
|
+
|
|
188
262
|
if (!mode.apply) {
|
|
189
263
|
if (isJson(flags)) {
|
|
190
264
|
emitJson({
|
|
@@ -193,8 +267,10 @@ async function installHooks(root, flags) {
|
|
|
193
267
|
root,
|
|
194
268
|
dryRun: true,
|
|
195
269
|
plan: plan.map(({ text, ...rest }) => rest),
|
|
270
|
+
shim: shimRow,
|
|
196
271
|
notWritten: others,
|
|
197
272
|
filesWritten: false,
|
|
273
|
+
...diffField(diffs, flags),
|
|
198
274
|
});
|
|
199
275
|
return refused.length > 0 ? EXIT.FAIL : EXIT.OK;
|
|
200
276
|
}
|
|
@@ -202,15 +278,18 @@ async function installHooks(root, flags) {
|
|
|
202
278
|
[
|
|
203
279
|
`repo ${root}`,
|
|
204
280
|
...plan.map((p) => `${p.action.toUpperCase().padEnd(9)} ${p.id.padEnd(12)} ${p.path}${p.error ? ` (${p.error})` : ''}`),
|
|
205
|
-
|
|
281
|
+
`${shimAction.toUpperCase().padEnd(9)} shim ${AGENT_HOOK_SHIM}`,
|
|
282
|
+
...others.map((o) => `${OTHER_LABEL[o.state]} ${o.id.padEnd(12)} ${o.path ?? '(no hooks file is documented)'}`),
|
|
206
283
|
...promptLine,
|
|
207
284
|
'',
|
|
208
285
|
refused.length > 0
|
|
209
286
|
? `REFUSED ${refused.length} config file(s) could not be read; nothing written`
|
|
210
|
-
:
|
|
287
|
+
: `PLAN nothing written; apply with: ${CLI} agent-hooks install --yes`,
|
|
211
288
|
].join('\n'),
|
|
212
289
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
213
290
|
);
|
|
291
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
292
|
+
emitDiffs(diffs, flags);
|
|
214
293
|
return refused.length > 0 ? EXIT.FAIL : EXIT.OK;
|
|
215
294
|
}
|
|
216
295
|
|
|
@@ -221,12 +300,23 @@ async function installHooks(root, flags) {
|
|
|
221
300
|
return fail(`refused to write ${refused.map((p) => `${p.path} (${p.error})`).join('; ')}`, EXIT.FAIL);
|
|
222
301
|
}
|
|
223
302
|
|
|
303
|
+
// Journaled so `undo` restores this run (R49).
|
|
304
|
+
const journal = [];
|
|
224
305
|
const written = [];
|
|
225
306
|
for (const p of plan.filter((x) => x.action !== 'unchanged')) {
|
|
226
307
|
await mkdir(join(root, dirname(p.path)), { recursive: true });
|
|
227
|
-
await
|
|
308
|
+
await journaledWriteFile(root, journal, join(root, p.path), p.text);
|
|
228
309
|
written.push(p);
|
|
229
310
|
}
|
|
311
|
+
// The shim is a file like the configs: unchanged installs leave it alone, and a
|
|
312
|
+
// changed one is journaled so `undo` removes or restores it with the configs.
|
|
313
|
+
let shimWritten = false;
|
|
314
|
+
if (shimAction !== 'unchanged') {
|
|
315
|
+
await mkdir(join(root, dirname(AGENT_HOOK_SHIM)), { recursive: true });
|
|
316
|
+
await journaledWriteFile(root, journal, join(root, AGENT_HOOK_SHIM), shimText, { mode: 0o755 });
|
|
317
|
+
shimWritten = true;
|
|
318
|
+
}
|
|
319
|
+
await commitJournal(root, 'agent-hooks install', journal);
|
|
230
320
|
|
|
231
321
|
const notes = [];
|
|
232
322
|
for (const p of written) {
|
|
@@ -241,10 +331,12 @@ async function installHooks(root, flags) {
|
|
|
241
331
|
command: 'agent-hooks install',
|
|
242
332
|
root,
|
|
243
333
|
plan: plan.map(({ text, ...rest }) => rest),
|
|
334
|
+
shim: { ...shimRow, written: shimWritten },
|
|
244
335
|
notWritten: others,
|
|
245
336
|
written: written.map((p) => p.path),
|
|
246
337
|
filesWritten: true,
|
|
247
338
|
notes,
|
|
339
|
+
...diffField(diffs, flags),
|
|
248
340
|
});
|
|
249
341
|
return EXIT.OK;
|
|
250
342
|
}
|
|
@@ -252,13 +344,16 @@ async function installHooks(root, flags) {
|
|
|
252
344
|
[
|
|
253
345
|
`repo ${root}`,
|
|
254
346
|
...plan.map((p) => `${p.action.toUpperCase().padEnd(9)} ${p.id.padEnd(12)} ${p.path}`),
|
|
255
|
-
|
|
347
|
+
`${shimAction.toUpperCase().padEnd(9)} shim ${AGENT_HOOK_SHIM}`,
|
|
348
|
+
...others.map((o) => `${OTHER_LABEL[o.state]} ${o.id.padEnd(12)} ${o.path ?? '(no hooks file is documented)'}`),
|
|
256
349
|
...promptLine,
|
|
257
350
|
'',
|
|
258
|
-
`WROTE ${written.length} hooks file(s); each one runs ${
|
|
351
|
+
`WROTE ${written.length} hooks file(s)${shimWritten ? ' and the shim' : ''}; each one runs the ${AGENT_HOOK_SHIM} shim before the tool call`,
|
|
259
352
|
].join('\n'),
|
|
260
353
|
{ maxBytes: flags.maxBytes ?? 0 },
|
|
261
354
|
);
|
|
355
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
356
|
+
emitDiffs(diffs, flags);
|
|
262
357
|
for (const o of others) note(`note ${o.id}: not written - ${o.note}`);
|
|
263
358
|
for (const n of notes) note(`note ${n}`);
|
|
264
359
|
note(`note never committed or pushed (ADR-001); review, then commit yourself`);
|
|
@@ -268,10 +363,31 @@ async function installHooks(root, flags) {
|
|
|
268
363
|
async function hooksStatus(root, flags) {
|
|
269
364
|
const rows = [];
|
|
270
365
|
const problems = [];
|
|
366
|
+
// The plugin file this tool owns whole is compared against what `install` would write
|
|
367
|
+
// right now, so status reads the same opt-in install reads: a file that matches the
|
|
368
|
+
// preview is present, anything else is drift an install will rewrite.
|
|
369
|
+
const { promptOptIn } = await import('../agenthooks/prompt.mjs');
|
|
370
|
+
const optIn = await promptOptIn(root);
|
|
271
371
|
|
|
272
372
|
for (const row of adapters()) {
|
|
273
373
|
const path = hookPath(row.id);
|
|
274
374
|
const existing = await readIfExists(join(root, path));
|
|
375
|
+
if (row.file) {
|
|
376
|
+
if (existing === null) {
|
|
377
|
+
rows.push({ id: row.id, path, state: 'absent', entries: 0, prompt: row.prompt ? 'absent' : null, note: null });
|
|
378
|
+
} else {
|
|
379
|
+
const present = rendered(row, existing, { prompt: optIn.optedIn }).action === 'unchanged';
|
|
380
|
+
rows.push({
|
|
381
|
+
id: row.id,
|
|
382
|
+
path,
|
|
383
|
+
state: present ? 'present' : 'drift',
|
|
384
|
+
entries: present ? 1 : 0,
|
|
385
|
+
prompt: row.prompt ? (present && optIn.optedIn ? 'present' : 'absent') : null,
|
|
386
|
+
note: present ? null : 'differs from what install would write; run install to restore it',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
275
391
|
if (existing === null) {
|
|
276
392
|
rows.push({ id: row.id, path, state: 'absent', entries: 0, prompt: row.prompt ? 'absent' : null, note: null });
|
|
277
393
|
continue;
|
|
@@ -319,7 +435,7 @@ async function hooksStatus(root, flags) {
|
|
|
319
435
|
|
|
320
436
|
const lines = [`repo ${root}`];
|
|
321
437
|
for (const r of rows) {
|
|
322
|
-
const state = { present: 'PRESENT ', absent: 'ABSENT ', unreadable: 'INVALID ', none: 'NO FILE ', other: 'NOT OURS' }[r.state];
|
|
438
|
+
const state = { present: 'PRESENT ', absent: 'ABSENT ', unreadable: 'INVALID ', drift: 'DRIFT ', none: 'NO FILE ', other: 'NOT OURS' }[r.state];
|
|
323
439
|
lines.push(`${state} ${r.id.padEnd(12)} ${r.path ?? '(no hooks file is documented)'}`);
|
|
324
440
|
if (r.state === 'absent') lines.push(` ${' '.repeat(12)} install: ${CLI} agent-hooks install --yes`);
|
|
325
441
|
// The second event is a separate answer to a separate question: a harness can carry
|
package/src/commands/bench.mjs
CHANGED
|
@@ -217,6 +217,7 @@ export async function benchReportCommand(positional, flags) {
|
|
|
217
217
|
harness: flags.harness,
|
|
218
218
|
model: flags.model,
|
|
219
219
|
compare: compareItems.length === 2 ? compareItems : null,
|
|
220
|
+
crossGroup: Boolean(flags.crossGroup),
|
|
220
221
|
});
|
|
221
222
|
} catch (error) {
|
|
222
223
|
return fail(
|
|
@@ -405,7 +406,7 @@ export async function benchVariantsCommand(positional, flags) {
|
|
|
405
406
|
// and until a bench here shows that, every report calls the critic advisory.
|
|
406
407
|
export async function benchCriticCommand(positional, flags, commands = []) {
|
|
407
408
|
const root = resolve(flags.root ?? positional[0] ?? process.cwd());
|
|
408
|
-
const labelsPath = flags.labels ?? '.simt/critic-labels.jsonl';
|
|
409
|
+
const labelsPath = flags.labels ?? '.simt/local/critic-labels.jsonl';
|
|
409
410
|
const result = await criticBench(root, { labelsPath, flags, commands });
|
|
410
411
|
if (typeof result === 'number') return result;
|
|
411
412
|
|
package/src/commands/ci.mjs
CHANGED
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// commands owns which file would be learning about ownership twice to learn nothing
|
|
19
19
|
// new, and the next .github file would arrive with a third command to remember.
|
|
20
20
|
|
|
21
|
-
import { mkdir, readFile
|
|
21
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
22
22
|
import { dirname } from 'node:path';
|
|
23
|
+
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
24
|
+
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
23
25
|
|
|
24
26
|
import { EXIT, emit, emitJson, isJson, note, writeMode } from '../output/index.mjs';
|
|
25
27
|
import { planWorkflow, renderWorkflow, WORKFLOW_FILE } from '../ci/workflow.mjs';
|
|
@@ -121,18 +123,37 @@ export async function ciCommand(positional, flags) {
|
|
|
121
123
|
return EXIT.FAIL;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// The `--diff` pairs, read before anything is written so the before side is
|
|
127
|
+
// true in preview and in apply alike (R50).
|
|
128
|
+
const renders = new Map(targets().map(({ key, render }) => [key, render]));
|
|
129
|
+
const diffs =
|
|
130
|
+
flags.diff === true
|
|
131
|
+
? await Promise.all(
|
|
132
|
+
plans
|
|
133
|
+
.filter((p) => p.action !== 'unchanged')
|
|
134
|
+
.map(async (p) => ({ path: p.file, before: await readBefore(p.path), after: renders.get(p.key)(p) })),
|
|
135
|
+
)
|
|
136
|
+
: [];
|
|
137
|
+
|
|
124
138
|
if (isJson(flags)) {
|
|
125
|
-
emitJson(
|
|
139
|
+
emitJson(
|
|
140
|
+
{ ok: true, command: 'ci', root, files: todo.map(publicPlan), written: false, dryRun: !mode.apply, ...diffField(diffs, flags) },
|
|
141
|
+
{},
|
|
142
|
+
);
|
|
126
143
|
} else {
|
|
127
144
|
emit(lines.join('\n'));
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
if (!mode.apply) {
|
|
148
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
149
|
+
emitDiffs(diffs, flags);
|
|
131
150
|
note('');
|
|
132
151
|
note(mode.dryRun ? 'dry-run nothing written' : `dry-run nothing written; apply with: ${CLI} ci --yes`);
|
|
133
152
|
return EXIT.OK;
|
|
134
153
|
}
|
|
135
154
|
|
|
155
|
+
// Journaled so `undo` restores this run (R49).
|
|
156
|
+
const journal = [];
|
|
136
157
|
for (const { key, plan, render } of targets()) {
|
|
137
158
|
const p = plans.find((x) => x.key === key);
|
|
138
159
|
if (p.action === 'unchanged') continue;
|
|
@@ -140,9 +161,10 @@ export async function ciCommand(positional, flags) {
|
|
|
140
161
|
// The renderer takes the plan because detection-driven content is a function of
|
|
141
162
|
// this repo's facts: a dependabot config rendered without them would watch
|
|
142
163
|
// nothing, which is the same silent-nothing the detection ruling exists to stop.
|
|
143
|
-
await
|
|
164
|
+
await journaledWriteFile(root, journal, p.path, render(p));
|
|
144
165
|
note(`wrote ${p.file} (${p.action}; the template and this copy now agree)`);
|
|
145
166
|
}
|
|
167
|
+
await commitJournal(root, 'ci', journal);
|
|
146
168
|
if (isJson(flags)) {
|
|
147
169
|
emitJson(
|
|
148
170
|
{
|
|
@@ -151,10 +173,13 @@ export async function ciCommand(positional, flags) {
|
|
|
151
173
|
root,
|
|
152
174
|
files: plans.map((p) => ({ file: p.file, action: p.action, bytes: p.bytes })),
|
|
153
175
|
written: true,
|
|
176
|
+
...diffField(diffs, flags),
|
|
154
177
|
},
|
|
155
178
|
{},
|
|
156
179
|
);
|
|
157
180
|
}
|
|
181
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
182
|
+
emitDiffs(diffs, flags);
|
|
158
183
|
return EXIT.OK;
|
|
159
184
|
}
|
|
160
185
|
|
package/src/commands/compare.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { basename, resolve } from 'node:path';
|
|
|
24
24
|
import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
|
|
25
25
|
import { provenanceDigest } from '../projection/project.mjs';
|
|
26
26
|
import { GATES } from '../hooks/gates.mjs';
|
|
27
|
-
import {
|
|
27
|
+
import { buildRows, resolveConcurrency, summarise } from '../compare/row.mjs';
|
|
28
28
|
import { discover } from '../compare/discover.mjs';
|
|
29
29
|
import { learnAcrossEstate, renderLearningLines } from '../compare/learn.mjs';
|
|
30
30
|
import { targetRoot, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
@@ -60,12 +60,13 @@ export async function compareCommand(positional, flags) {
|
|
|
60
60
|
const expectedSha = await provenanceDigest();
|
|
61
61
|
const context = { expectedSha, gates: new Set(Object.keys(GATES)), gateKinds: GATE_KINDS };
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
// process;
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
// Parallel with a cap, in discovery order. 16 fingerprint+region computations
|
|
64
|
+
// each spawn a git process; unbounded parallelism is a burst of subprocesses
|
|
65
|
+
// with no speedup, and sequential builds are what took 48 s over MCP for 17
|
|
66
|
+
// repos (review 0003 Q38). The cap keeps the burst bounded while overlapping
|
|
67
|
+
// the spawn wait; the rows still come back in input order, so two runs diff
|
|
68
|
+
// cleanly and determinism of the output is untouched.
|
|
69
|
+
const rows = await buildRows(found.repos, context, { concurrency: resolveConcurrency(flags) });
|
|
69
70
|
const learning = await learnAcrossEstate(found.repos);
|
|
70
71
|
|
|
71
72
|
const summary = summarise(rows, {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// `simt completions <shell>` — print a completion script (plan 0003 R51).
|
|
2
|
+
//
|
|
3
|
+
// The script goes to stdout and nothing else does, because the install is a
|
|
4
|
+
// redirect: `source <(simt completions bash)`, or the zsh file dropped onto
|
|
5
|
+
// `$fpath`. A report line above it would be a report line in the user's shell
|
|
6
|
+
// config.
|
|
7
|
+
|
|
8
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
9
|
+
import { SHELLS, renderCompletions } from '../completions.mjs';
|
|
10
|
+
|
|
11
|
+
export async function completionsCommand(args, flags, commands) {
|
|
12
|
+
const shell = args[0];
|
|
13
|
+
if (shell === undefined) {
|
|
14
|
+
return fail('completions needs a shell', EXIT.USAGE, `one of: ${SHELLS.join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
if (!SHELLS.includes(shell)) {
|
|
17
|
+
return fail(`unknown shell \`${shell}\``, EXIT.USAGE, `one of: ${SHELLS.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const script = renderCompletions(shell, commands);
|
|
21
|
+
if (isJson(flags)) {
|
|
22
|
+
emitJson({ ok: true, command: 'completions', shell, script });
|
|
23
|
+
return EXIT.OK;
|
|
24
|
+
}
|
|
25
|
+
emit(script, { maxBytes: flags.maxBytes ?? 0 });
|
|
26
|
+
return EXIT.OK;
|
|
27
|
+
}
|
package/src/commands/decide.mjs
CHANGED
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
// a rulings skeleton, and structured `simt-affects` + `simt-supersedes` frontmatter
|
|
3
3
|
// (plan 0003 R34).
|
|
4
4
|
|
|
5
|
-
import { join
|
|
5
|
+
import { join } from 'node:path';
|
|
6
6
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
7
7
|
import { CLI } from '../name.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { decisionsCutoff, decisionsDir } from '../decisions/dir.mjs';
|
|
9
9
|
import { lintDecisions, scaffoldDecision } from '../memory/decide.mjs';
|
|
10
|
+
import { commitJournal } from '../journal.mjs';
|
|
11
|
+
import { diffField, emitDiffs } from '../diff.mjs';
|
|
10
12
|
import { parsePathList } from '../argv.mjs';
|
|
11
13
|
|
|
12
14
|
export async function decideCommand(args, flags) {
|
|
13
15
|
const root = flags.root ?? process.cwd();
|
|
14
16
|
|
|
15
17
|
if (flags.check === true) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
18
|
+
// Q55: one resolution — the manifest key, then the detected directory —
|
|
19
|
+
// and the cutoff from the same config, never a hard-coded number or path.
|
|
20
|
+
const relDir = await decisionsDir(root);
|
|
21
|
+
const res = await lintDecisions(join(root, relDir), { historicalCutoff: await decisionsCutoff(root) });
|
|
19
22
|
|
|
20
23
|
if (isJson(flags)) {
|
|
21
24
|
emitJson({
|
|
@@ -49,12 +52,17 @@ export async function decideCommand(args, flags) {
|
|
|
49
52
|
const parsedPaths = parsePathList(flags.paths);
|
|
50
53
|
const explicitPaths = flags.paths !== undefined ? parsedPaths : undefined;
|
|
51
54
|
|
|
55
|
+
// Journaled so `undo` restores the scaffold (R49); committed only when the
|
|
56
|
+
// run actually wrote, which commitJournal already guarantees.
|
|
57
|
+
const journal = [];
|
|
52
58
|
const res = await scaffoldDecision(root, {
|
|
53
59
|
title,
|
|
54
60
|
affects: explicitPaths,
|
|
55
61
|
supersedes: flags.supersedes,
|
|
56
62
|
write: mode.apply,
|
|
63
|
+
journal: mode.apply ? journal : undefined,
|
|
57
64
|
});
|
|
65
|
+
if (mode.apply && res.ok) await commitJournal(root, 'decide', journal);
|
|
58
66
|
|
|
59
67
|
if (!res.ok) {
|
|
60
68
|
return fail(
|
|
@@ -70,6 +78,10 @@ export async function decideCommand(args, flags) {
|
|
|
70
78
|
note('dry-run no files written');
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
// A scaffold always creates: the number is free by construction, so the
|
|
82
|
+
// before side is empty and the pair is the whole file (R50).
|
|
83
|
+
const diffs = flags.diff === true ? [{ path: res.relPath, before: null, after: res.text }] : [];
|
|
84
|
+
|
|
73
85
|
if (isJson(flags)) {
|
|
74
86
|
emitJson({
|
|
75
87
|
ok: true,
|
|
@@ -84,10 +96,10 @@ export async function decideCommand(args, flags) {
|
|
|
84
96
|
supersedes: res.supersedes,
|
|
85
97
|
written: res.written,
|
|
86
98
|
dryRun: !mode.apply,
|
|
99
|
+
...diffField(diffs, flags),
|
|
87
100
|
});
|
|
88
101
|
return EXIT.OK;
|
|
89
102
|
}
|
|
90
|
-
|
|
91
103
|
const supersedesLabel =
|
|
92
104
|
res.supersedes.length > 0
|
|
93
105
|
? res.supersedes
|
|
@@ -112,5 +124,7 @@ export async function decideCommand(args, flags) {
|
|
|
112
124
|
];
|
|
113
125
|
|
|
114
126
|
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
127
|
+
// After the report: the report says what, the diff shows it (R50).
|
|
128
|
+
emitDiffs(diffs, flags);
|
|
115
129
|
return EXIT.OK;
|
|
116
130
|
}
|