simt-dev 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/README.md +129 -75
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
- 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/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
- package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
- package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
- package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
- package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
- package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
- package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
- package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
- package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
- package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
- package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
- package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
- package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
- package/package.json +2 -1
- package/rules/core.md +1 -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 +54 -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/ci/workflow.mjs +11 -1
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +371 -0
- package/src/commands/evidence.mjs +96 -0
- package/src/commands/gate.mjs +165 -0
- package/src/commands/harness.mjs +156 -5
- package/src/commands/help.mjs +89 -1
- package/src/commands/hooks.mjs +45 -5
- package/src/commands/impact.mjs +205 -0
- package/src/commands/index.mjs +60 -1
- package/src/commands/map.mjs +91 -0
- package/src/commands/plan.mjs +99 -0
- package/src/commands/repoindex.mjs +52 -0
- package/src/commands/shared.mjs +398 -0
- package/src/commands/sync.mjs +72 -3
- package/src/commands/tests.mjs +192 -0
- package/src/commands/tokens.mjs +61 -1
- package/src/commands/why.mjs +177 -0
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +1 -1
- package/src/hooks/dispatcher.mjs +15 -0
- package/src/hooks/shim.mjs +31 -0
- package/src/index/compact.mjs +228 -0
- package/src/index/extract.mjs +331 -0
- package/src/index/gate.mjs +101 -0
- package/src/index/govern.mjs +222 -0
- package/src/index/impact.mjs +186 -0
- package/src/index/map.mjs +393 -0
- package/src/index/persist.mjs +172 -0
- package/src/index/store.mjs +301 -0
- package/src/index/tests.mjs +294 -0
- package/src/kernel/budget.mjs +100 -0
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/checks.mjs +378 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/ledger/index.mjs +112 -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 +184 -11
- 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/disclosure.mjs +92 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/lock.mjs +93 -0
- package/src/projection/manifest.mjs +78 -3
- package/src/projection/markers.mjs +56 -17
- package/src/projection/project.mjs +24 -17
- package/src/projection/rules.mjs +452 -43
- package/src/projection/shared.mjs +428 -0
- package/src/projection/surfaces.mjs +22 -8
- package/src/projection/targets.mjs +2 -1
- package/src/toolwords.mjs +71 -0
- package/templates/plan/tracker.json +36 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// `simt evidence` — the proof block for this tree, and the commit-message check.
|
|
2
|
+
//
|
|
3
|
+
// The read answers "what has actually been run against what is on disk". The check
|
|
4
|
+
// answers the other half: a message that says the tests passed, when nothing in the
|
|
5
|
+
// ledger was run against this tree (ADR-087).
|
|
6
|
+
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
|
|
9
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
10
|
+
import { readManifest } from '../projection/manifest.mjs';
|
|
11
|
+
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
12
|
+
import { claimsVerification, evidenceMode, readLedger, treeId } from '../ledger/index.mjs';
|
|
13
|
+
|
|
14
|
+
export async function evidenceCommand(args, flags) {
|
|
15
|
+
const root = flags.root ?? process.cwd();
|
|
16
|
+
if (args[0] === 'check') return checkClaim(root, args[1], flags);
|
|
17
|
+
if (args[0] !== undefined) {
|
|
18
|
+
return fail(`unknown evidence subcommand ${args[0]}`, EXIT.USAGE, `run: ${CLI} evidence`);
|
|
19
|
+
}
|
|
20
|
+
return showEvidence(root, flags);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function showEvidence(root, flags) {
|
|
24
|
+
let tree;
|
|
25
|
+
try {
|
|
26
|
+
tree = await treeId(root);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return fail(
|
|
29
|
+
`could not name this tree (${error.message})`,
|
|
30
|
+
EXIT.UNSUPPORTED,
|
|
31
|
+
'run this inside a git repository',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const { entries, errors } = await readLedger(root);
|
|
35
|
+
const mine = entries.filter((e) => e.tree === tree);
|
|
36
|
+
const payload = { ok: errors.length === 0, command: 'evidence', root, tree, entries: mine, errors };
|
|
37
|
+
if (isJson(flags)) {
|
|
38
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
39
|
+
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
40
|
+
}
|
|
41
|
+
const lines = [`tree ${tree}`];
|
|
42
|
+
if (mine.length === 0) lines.push('none no run is recorded for this tree');
|
|
43
|
+
for (const e of mine) {
|
|
44
|
+
lines.push(`${e.exit === 0 ? 'pass' : 'fail'} ${e.cmd} exit ${e.exit} ${e.summary}`);
|
|
45
|
+
}
|
|
46
|
+
for (const error of errors) lines.push(`error ${error}`);
|
|
47
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
48
|
+
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function checkClaim(root, file, flags) {
|
|
52
|
+
if (!file) return fail('evidence check needs the commit message file', EXIT.USAGE, `run: ${CLI} evidence check <file>`);
|
|
53
|
+
let message;
|
|
54
|
+
try {
|
|
55
|
+
message = await readFile(file, 'utf8');
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return fail(`could not read ${file} (${error.message})`, EXIT.USAGE, `pass the path git gives commit-msg`);
|
|
58
|
+
}
|
|
59
|
+
const { manifest, error } = await readManifest(root);
|
|
60
|
+
if (error) return fail(`${MANIFEST_FILE}: ${error}`, EXIT.CONFIG, `fix ${MANIFEST_FILE} so it parses`);
|
|
61
|
+
const mode = evidenceMode(manifest);
|
|
62
|
+
if (mode === null) {
|
|
63
|
+
return fail(
|
|
64
|
+
`${MANIFEST_FILE} gates.evidence must be "warn" or "block"`,
|
|
65
|
+
EXIT.CONFIG,
|
|
66
|
+
`set gates.evidence to "warn" or "block", or remove it`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const claim = claimsVerification(message);
|
|
70
|
+
if (!claim) {
|
|
71
|
+
const payload = { ok: true, command: 'evidence check', claim: false, mode };
|
|
72
|
+
if (isJson(flags)) emitJson(payload, { pretty: flags.pretty === true });
|
|
73
|
+
return EXIT.OK;
|
|
74
|
+
}
|
|
75
|
+
let tree;
|
|
76
|
+
try {
|
|
77
|
+
tree = await treeId(root);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return fail(`could not name this tree (${error.message})`, EXIT.UNSUPPORTED, 'run this inside a git repository');
|
|
80
|
+
}
|
|
81
|
+
const { entries } = await readLedger(root);
|
|
82
|
+
const supported = entries.some((e) => e.tree === tree && e.exit === 0);
|
|
83
|
+
const payload = { ok: supported || mode === 'warn', command: 'evidence check', claim: true, supported, mode, tree };
|
|
84
|
+
if (supported) {
|
|
85
|
+
if (isJson(flags)) emitJson(payload, { pretty: flags.pretty === true });
|
|
86
|
+
return EXIT.OK;
|
|
87
|
+
}
|
|
88
|
+
const detail = 'the message claims a run this tree does not have';
|
|
89
|
+
const remedy = `run the command, then commit, or take the claim out of the message`;
|
|
90
|
+
if (isJson(flags)) {
|
|
91
|
+
emitJson({ ...payload, detail, remedy }, { pretty: flags.pretty === true });
|
|
92
|
+
return mode === 'block' ? EXIT.FAIL : EXIT.OK;
|
|
93
|
+
}
|
|
94
|
+
emit(`${mode === 'block' ? 'BLOCKED' : 'WARNING'} ${detail}\n remedy: ${remedy}`, { maxBytes: flags.maxBytes ?? 0 });
|
|
95
|
+
return mode === 'block' ? EXIT.FAIL : EXIT.OK;
|
|
96
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// `simt gate` runs the tests a change reaches (R16, ADR-094).
|
|
2
|
+
//
|
|
3
|
+
// Paths name the change. `--changed` reads the diff instead. The tests are the
|
|
4
|
+
// ones that import a changed file, directly or through other files, and the
|
|
5
|
+
// runner is the one the package already uses. Nothing else runs. A report
|
|
6
|
+
// this tool knows how to read is compacted; the raw log is kept.
|
|
7
|
+
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
|
|
13
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
14
|
+
import { CLI } from '../name.mjs';
|
|
15
|
+
import { detectTestRunner } from '../bench/mine.mjs';
|
|
16
|
+
import { compactOutput } from '../index/compact.mjs';
|
|
17
|
+
import { readImportEdges, selectTests } from '../index/gate.mjs';
|
|
18
|
+
import { packageOf } from '../index/store.mjs';
|
|
19
|
+
|
|
20
|
+
const exec = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
export async function gateCommand(args, flags) {
|
|
23
|
+
const root = flags.root ?? process.cwd();
|
|
24
|
+
const listed = await tracked(root);
|
|
25
|
+
if (listed.error) return listed.error;
|
|
26
|
+
let changed = args.filter(Boolean);
|
|
27
|
+
if (flags.changed === true) {
|
|
28
|
+
const diff = await changedFiles(root);
|
|
29
|
+
if (diff.error) return diff.error;
|
|
30
|
+
changed = [...new Set([...changed, ...diff.files])].sort();
|
|
31
|
+
}
|
|
32
|
+
if (changed.length === 0) {
|
|
33
|
+
if (flags.changed === true) {
|
|
34
|
+
if (isJson(flags)) {
|
|
35
|
+
emitJson({ ok: true, command: 'gate', root, selected: [], code: 0 }, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
36
|
+
} else {
|
|
37
|
+
emit('selected 0', { maxBytes: flags.maxBytes ?? 0 });
|
|
38
|
+
}
|
|
39
|
+
return EXIT.OK;
|
|
40
|
+
}
|
|
41
|
+
return fail(`nothing changed`, EXIT.USAGE, `pass a path, or ${CLI} gate --changed`);
|
|
42
|
+
}
|
|
43
|
+
const files = new Set(listed.files);
|
|
44
|
+
for (const file of changed) files.add(file);
|
|
45
|
+
const selected = selectTests(changed, await readImportEdges(root, files));
|
|
46
|
+
if (selected.length === 0) {
|
|
47
|
+
const payload = { ok: true, command: 'gate', root, selected, code: 0 };
|
|
48
|
+
if (isJson(flags)) {
|
|
49
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
50
|
+
return EXIT.OK;
|
|
51
|
+
}
|
|
52
|
+
emit('selected 0', { maxBytes: flags.maxBytes ?? 0 });
|
|
53
|
+
return EXIT.OK;
|
|
54
|
+
}
|
|
55
|
+
return runSelected(root, flags, selected);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function runSelected(root, flags, selected) {
|
|
59
|
+
const cache = new Map();
|
|
60
|
+
const groups = new Map();
|
|
61
|
+
for (const file of selected) {
|
|
62
|
+
const pkg = await packageOf(root, file, cache);
|
|
63
|
+
const list = groups.get(pkg);
|
|
64
|
+
if (list) list.push(file);
|
|
65
|
+
else groups.set(pkg, [file]);
|
|
66
|
+
}
|
|
67
|
+
const runs = [];
|
|
68
|
+
let code = 0;
|
|
69
|
+
const text = [];
|
|
70
|
+
const rawParts = [];
|
|
71
|
+
for (const [pkg, files] of [...groups.entries()].sort((a, b) => cmp(a[0], b[0]))) {
|
|
72
|
+
const relative = files.map((file) => (pkg && file.startsWith(`${pkg}/`) ? file.slice(pkg.length + 1) : file));
|
|
73
|
+
const cwd = join(root, pkg);
|
|
74
|
+
const runner = await detectTestRunner(cwd, relative);
|
|
75
|
+
if (!runner) {
|
|
76
|
+
return fail(
|
|
77
|
+
`no runner for ${files.length} test${files.length === 1 ? '' : 's'} in ${pkg || 'this repository'}`,
|
|
78
|
+
EXIT.UNSUPPORTED,
|
|
79
|
+
'declare a package manifest the test runner can see',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const built = runner.buildExec(cwd);
|
|
83
|
+
text.push(`selected ${files.length}`, ...files, '', runner.displayCmd);
|
|
84
|
+
// A parent `node --test` puts NODE_TEST_CONTEXT in the environment. Left
|
|
85
|
+
// there, the child reports to that parent and exits 0 whether or not it failed.
|
|
86
|
+
const env = { ...process.env };
|
|
87
|
+
for (const key of Object.keys(env)) {
|
|
88
|
+
if (key.startsWith('NODE_TEST')) delete env[key];
|
|
89
|
+
}
|
|
90
|
+
const result = await exec(built.cmd, built.args, { cwd, env, maxBuffer: 32 << 20 }).then(
|
|
91
|
+
(out) => ({ code: 0, stdout: out.stdout, stderr: out.stderr }),
|
|
92
|
+
(error) => ({
|
|
93
|
+
code: typeof error.code === 'number' ? error.code : 1,
|
|
94
|
+
stdout: error.stdout ?? '',
|
|
95
|
+
stderr: error.stderr ?? '',
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
if (result.code !== 0) code = result.code;
|
|
99
|
+
const stdout = String(result.stdout ?? '');
|
|
100
|
+
const stderr = String(result.stderr ?? '');
|
|
101
|
+
const body = stderr ? `${stdout}\n${stderr}` : stdout;
|
|
102
|
+
rawParts.push(body);
|
|
103
|
+
const compact = compactOutput(body);
|
|
104
|
+
runs.push({ package: pkg, command: runner.displayCmd, code: result.code, stdout, stderr });
|
|
105
|
+
text.push(compact ?? body);
|
|
106
|
+
}
|
|
107
|
+
const raw = await writeRaw(root, rawParts.join('\n'));
|
|
108
|
+
if (raw) text.push('', `raw ${raw}`);
|
|
109
|
+
const payload = { ok: code === 0, command: 'gate', root, selected, code, raw, runs };
|
|
110
|
+
if (isJson(flags)) {
|
|
111
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
112
|
+
return code === 0 ? EXIT.OK : EXIT.FAIL;
|
|
113
|
+
}
|
|
114
|
+
emit(text.filter((line) => line !== undefined).join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
115
|
+
return code === 0 ? EXIT.OK : EXIT.FAIL;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function writeRaw(root, body) {
|
|
119
|
+
try {
|
|
120
|
+
const dir = join(root, '.simt-dev', 'gate');
|
|
121
|
+
await mkdir(dir, { recursive: true });
|
|
122
|
+
const name = `gate-${Date.now()}.log`;
|
|
123
|
+
await writeFile(join(dir, name), body);
|
|
124
|
+
return `.simt-dev/gate/${name}`;
|
|
125
|
+
} catch {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function tracked(root) {
|
|
131
|
+
try {
|
|
132
|
+
const { stdout } = await exec('git', ['ls-files'], { cwd: root, maxBuffer: 16 << 20 });
|
|
133
|
+
return { files: stdout.split('\n').filter(Boolean) };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
error: fail(
|
|
137
|
+
`could not read the change (${error.message})`,
|
|
138
|
+
EXIT.UNSUPPORTED,
|
|
139
|
+
'run this inside a git repository',
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function changedFiles(root) {
|
|
146
|
+
try {
|
|
147
|
+
const { stdout } = await exec('git', ['diff', '--name-only', 'HEAD'], { cwd: root, maxBuffer: 8 << 20 });
|
|
148
|
+
const untracked = await exec('git', ['ls-files', '--others', '--exclude-standard'], { cwd: root, maxBuffer: 8 << 20 });
|
|
149
|
+
return { files: [...new Set([...stdout.split('\n'), ...untracked.stdout.split('\n')].filter(Boolean))].sort() };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
error: fail(
|
|
153
|
+
`could not read the diff (${error.message})`,
|
|
154
|
+
EXIT.UNSUPPORTED,
|
|
155
|
+
'run this inside a git repository that has a commit',
|
|
156
|
+
),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cmp(a, b) {
|
|
162
|
+
if (a < b) return -1;
|
|
163
|
+
if (a > b) return 1;
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
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
|
|
@@ -46,6 +61,35 @@ ${CLI} ci simt-dev's own .github files: create or refresh th
|
|
|
46
61
|
${CLI} compare org-wide matrix across every sibling repo
|
|
47
62
|
(read-only; exits non-zero on defects, not on
|
|
48
63
|
repos that have not adopted yet)
|
|
64
|
+
${CLI} index rebuild the local file index from git blob hashes
|
|
65
|
+
${CLI} why [path] what governs a file: a decision whose Affects line names
|
|
66
|
+
it, a rule whose scope matches it, a skill, a gate
|
|
67
|
+
or a lesson. With no path, the Affects tokens this
|
|
68
|
+
tree cannot resolve (UNRESOLVED-AFFECTS)
|
|
69
|
+
${CLI} tests [path] the tests that exercise a file: an import, the conventional
|
|
70
|
+
source beside the test, or a coverage record that
|
|
71
|
+
names both (--cochange adds files that only share
|
|
72
|
+
commits, and only those)
|
|
73
|
+
${CLI} gate [path...] the tests a change reaches, and only those
|
|
74
|
+
(--changed reads the diff instead of the paths)
|
|
75
|
+
${CLI} impact <file|symbol> who a change reaches: importers, their tests,
|
|
76
|
+
what governs it, and a contract another repository references
|
|
77
|
+
${CLI} map [--task "<text>"] [--level outline|names|signatures]
|
|
78
|
+
the files this task is most likely to touch, within
|
|
79
|
+
--budget bytes (default 2048; 0 is unlimited). outline
|
|
80
|
+
is paths, names adds exports, signatures adds the line.
|
|
81
|
+
--recall reports gold-file recall@10 on mined tasks
|
|
82
|
+
${CLI} evidence the runs recorded against the tree on disk
|
|
83
|
+
${CLI} evidence check <file> a commit message that claims a run this tree does not
|
|
84
|
+
have: a warning, or a block when gates.evidence is "block"
|
|
85
|
+
${CLI} shared plan what a workspace's repeated rules cost to carry
|
|
86
|
+
once: the shared set, its bytes, and every path that would be written
|
|
87
|
+
(read-only; touches nothing, not even your home directory)
|
|
88
|
+
${CLI} shared install [--yes] carry those rules once per reader, at user level,
|
|
89
|
+
and declare the delta in each repo's manifest (dry-run unless
|
|
90
|
+
--yes; copies land in files the vendors' own docs name, and a reader
|
|
91
|
+
with no such file gets the block printed to paste; ${CLI} doctor
|
|
92
|
+
reports a copy that is missing or stale)
|
|
49
93
|
${CLI} tokens [--repos] what a reader loads at session start, in bytes
|
|
50
94
|
(read-only; exits 0 whatever it finds, because a
|
|
51
95
|
measurement is not a verdict; --repos measures every
|
|
@@ -54,6 +98,25 @@ ${CLI} harness detect which harnesses this repo (and, with --user, you)
|
|
|
54
98
|
are set up for, and which have a rule channel here
|
|
55
99
|
(presence only: a directory found is not a claim about
|
|
56
100
|
what its owner reads; exits 0 whatever it finds)
|
|
101
|
+
${CLI} harness loaders what each reader loads at session start, in bytes,
|
|
102
|
+
and what its size cap drops
|
|
103
|
+
(--table prints the vendor facts without measuring;
|
|
104
|
+
--reader <id> narrows it; every claim cites its source)
|
|
105
|
+
${CLI} bench mine [--yes] mine verified benchmark tasks from git history
|
|
106
|
+
(dry-run unless --yes; validates RED and GOLD twice in
|
|
107
|
+
a temp worktree and stores test patches in the vault)
|
|
108
|
+
${CLI} bench run [--yes] run benchmark tasks across arms in isolated one-commit exports
|
|
109
|
+
(dry-run unless --yes; grades against vault test patches
|
|
110
|
+
and declared gates; --regrade <run> verifies stored runs)
|
|
111
|
+
${CLI} bench report statistical report across benchmark arms (R, Q, C, CPI, cache)
|
|
112
|
+
(read-only; paired bootstrap 95% CIs; --compare <a> <b>
|
|
113
|
+
prints deltas with McNemar and Wilcoxon p-values)
|
|
114
|
+
${CLI} bench ablate [--yes] leave-one-out ablation across core, profile and process sections
|
|
115
|
+
(dry-run unless --yes; 10-task canary with escalation,
|
|
116
|
+
value-per-KB ranking and deletion candidates)
|
|
117
|
+
${CLI} bench probes [--yes] 12 synthetic rule-compliance probes with deterministic graders
|
|
118
|
+
(dry-run unless --yes; grades git state, transcript and ledger,
|
|
119
|
+
and reports compliance per arm)
|
|
57
120
|
${CLI} mcp the read-only surface as MCP tools over stdio
|
|
58
121
|
(--list prints it without reading stdin; init, sync,
|
|
59
122
|
upgrade and the installers stay CLI-only by design)`;
|
|
@@ -76,6 +139,10 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
76
139
|
--dry-run report what would change, change nothing
|
|
77
140
|
--yes assume yes; required in automation, never prompts
|
|
78
141
|
--check report drift and write nothing (sync, ci)
|
|
142
|
+
--frozen check: the lock must match this projection; an output
|
|
143
|
+
change no input change explains fails the gate
|
|
144
|
+
--explain sync: name each line dropped as already inferable, with
|
|
145
|
+
the fact behind it
|
|
79
146
|
--all ignore a cap: every ruling of a record, or the skills budget
|
|
80
147
|
--variant <id> which body of a multi-variant skill to use
|
|
81
148
|
--from <repo/path> install from a declared source by name
|
|
@@ -88,6 +155,27 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
88
155
|
--user harness detect: also report the directories in your home
|
|
89
156
|
--repos tokens: measure every repository under the root, not
|
|
90
157
|
just the one, and report what repeats an earlier one
|
|
158
|
+
--session <path> tokens: split a local Claude Code or Codex transcript
|
|
159
|
+
into rules, file reads and tool output
|
|
160
|
+
--reader <id> harness loaders: report one reader (opencode, claude, codex,
|
|
161
|
+
cursor, antigravity, copilot, deepseek)
|
|
162
|
+
--table harness loaders: print order, caps, imports, skill dirs and
|
|
163
|
+
the hook and MCP config files, without measuring a repo
|
|
164
|
+
--at <path> harness loaders: measure as if the agent were working in
|
|
165
|
+
this directory (default: the root; readers that merge the
|
|
166
|
+
project root down to the working directory cost more here)
|
|
167
|
+
--arm <spec> bench run: arm to evaluate (bare, current, kernel, brief,
|
|
168
|
+
or name=path/to/arm.json; repeatable)
|
|
169
|
+
--harness <id> bench run: adapter to invoke (claude, codex, gemini, opencode,
|
|
170
|
+
cursor, aider, gold, noop; default: gold);
|
|
171
|
+
agent-hook: which dialect to answer in (claude, cursor,
|
|
172
|
+
codex, antigravity)
|
|
173
|
+
--model <id> bench run: model identifier recorded and passed to the adapter
|
|
174
|
+
--reps <n> bench run: repetitions per task and arm (default: 3)
|
|
175
|
+
--regrade <run> bench run: re-grade a stored run from its archived agent patch
|
|
176
|
+
--compare <a> <b> bench report: compare two arms with deltas and p-values
|
|
177
|
+
--test <cmd> plan done: the test command that closes the row
|
|
178
|
+
--state <name> plan done: target state (default: Complete)
|
|
91
179
|
--help this text
|
|
92
180
|
--version the version, and the SHA it was built from`;
|
|
93
181
|
|