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
package/src/prbody.mjs
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// The `simt pr-body` engine (plan 0003 R55).
|
|
2
|
+
//
|
|
3
|
+
// A pull request body assembled from what the tool already knows, rather than
|
|
4
|
+
// what its author remembers to write: the evidence block from the ledger (R17),
|
|
5
|
+
// the decision records the diff touches, the gate state from the last check,
|
|
6
|
+
// and the residual risk in ADR-008's verdict vocabulary. Read-only throughout:
|
|
7
|
+
// it names the tree, reads the ledger, reads the check cache and diffs names,
|
|
8
|
+
// and writes nothing, so it needs no preview, no `--yes` and no journal entry.
|
|
9
|
+
//
|
|
10
|
+
// The base defaults to the branch's upstream fork point (`origin/main`, then
|
|
11
|
+
// `main`), and `--base` names anything else. When no base resolves, the body
|
|
12
|
+
// falls back to the working-tree diff against HEAD: a report that refused
|
|
13
|
+
// without a base would be useless on a repository with a single branch.
|
|
14
|
+
|
|
15
|
+
import { execFile } from 'node:child_process';
|
|
16
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { promisify } from 'node:util';
|
|
19
|
+
|
|
20
|
+
import { readLedger, treeId } from './ledger/index.mjs';
|
|
21
|
+
import { readCheckCache } from './status.mjs';
|
|
22
|
+
import { readManifest } from './projection/manifest.mjs';
|
|
23
|
+
import { splitFrontmatter } from './memory/decide.mjs';
|
|
24
|
+
import { decisionsDir } from './decisions/dir.mjs';
|
|
25
|
+
|
|
26
|
+
const exec = promisify(execFile);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The ref the body diffs against: the override when given, else the first
|
|
30
|
+
* candidate that shares history with HEAD. Returns the ref, or null when the
|
|
31
|
+
* repository has a single branch and the body should diff the worktree.
|
|
32
|
+
*/
|
|
33
|
+
async function resolveBase(root, override = null) {
|
|
34
|
+
if (override !== null && override !== undefined && String(override) !== '') {
|
|
35
|
+
try {
|
|
36
|
+
await exec('git', ['rev-parse', '--verify', '--quiet', `${override}^{commit}`], { cwd: root });
|
|
37
|
+
return { base: String(override), error: null };
|
|
38
|
+
} catch {
|
|
39
|
+
return { base: null, error: `unknown base ${override}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const candidate of ['origin/main', 'main', 'origin/master', 'master']) {
|
|
43
|
+
try {
|
|
44
|
+
await exec('git', ['merge-base', 'HEAD', candidate], { cwd: root });
|
|
45
|
+
return { base: candidate, error: null };
|
|
46
|
+
} catch {
|
|
47
|
+
// Not present or shares no history; the next candidate may.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { base: null, error: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every path the pull request would carry: the branch range when there is a
|
|
55
|
+
* base, plus whatever the worktree adds on top. Sorted, deduplicated, and
|
|
56
|
+
* without `.simt/` state: the ledger and the check cache prove the body, they
|
|
57
|
+
* are not its content.
|
|
58
|
+
*/
|
|
59
|
+
async function changedPaths(root, base) {
|
|
60
|
+
const paths = new Set();
|
|
61
|
+
const add = (stdout) => {
|
|
62
|
+
for (const line of String(stdout ?? '').split('\n')) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (trimmed !== '' && !trimmed.startsWith('.simt/')) paths.add(trimmed);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
if (base !== null) {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout } = await exec('git', ['diff', '--name-only', `${base}...HEAD`], {
|
|
70
|
+
cwd: root,
|
|
71
|
+
maxBuffer: 16 << 20,
|
|
72
|
+
});
|
|
73
|
+
add(stdout);
|
|
74
|
+
} catch {
|
|
75
|
+
// An unborn HEAD or an unreadable range contributes nothing; the worktree
|
|
76
|
+
// reads below still answer.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const args of [
|
|
80
|
+
['diff', '--name-only', 'HEAD'],
|
|
81
|
+
['ls-files', '--others', '--exclude-standard'],
|
|
82
|
+
]) {
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await exec('git', args, { cwd: root, maxBuffer: 16 << 20 });
|
|
85
|
+
add(stdout);
|
|
86
|
+
} catch {
|
|
87
|
+
// Not a repository, or no commit yet: the body reports the absence.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return [...paths].sort();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The decision records the diff touches: one entry per `docs/decisions/*.md`
|
|
95
|
+
* in the changed paths, carrying the id from the filename and the title and
|
|
96
|
+
* status from the file itself. Sorted by id; a file that will not parse is
|
|
97
|
+
* reported with what it has rather than dropping the row.
|
|
98
|
+
*/
|
|
99
|
+
async function changedDecisions(root, paths) {
|
|
100
|
+
// Q55: the prefix is the repository's own decisions directory, not a literal.
|
|
101
|
+
const relDir = await decisionsDir(root);
|
|
102
|
+
const absDir = join(root, relDir);
|
|
103
|
+
const names = paths.filter((p) => p.startsWith(`${relDir}/`) && p.toLowerCase().endsWith('.md'));
|
|
104
|
+
try {
|
|
105
|
+
await readdir(absDir);
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const name of names.sort()) {
|
|
111
|
+
const id = /^ADR-\d{3,}/i.exec(name.slice(relDir.length + 1))?.[0].toUpperCase() ?? name;
|
|
112
|
+
let title = id;
|
|
113
|
+
let status = 'unknown';
|
|
114
|
+
try {
|
|
115
|
+
const text = await readFile(join(root, name), 'utf8');
|
|
116
|
+
const { body } = splitFrontmatter(text);
|
|
117
|
+
const heading = /^#\s+(.*)$/m.exec(body)?.[1] ?? id;
|
|
118
|
+
title = heading.replace(/^ADR-\d{3,}\s*[—–-]\s*/i, '').trim() || id;
|
|
119
|
+
status = /^Status:\s*(.+)$/im.exec(body)?.[1].trim() ?? 'unknown';
|
|
120
|
+
} catch {
|
|
121
|
+
// An unreadable record is still a changed decision; the body says so
|
|
122
|
+
// rather than silently omitting a file the diff names.
|
|
123
|
+
}
|
|
124
|
+
out.push({ path: name, id, title, status });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Assemble the whole body. Never throws for a missing ledger, cache or base:
|
|
131
|
+
* each absence is a line in the report, because a PR body whose evidence is
|
|
132
|
+
* missing is precisely the case the residual-risk section exists to say out loud.
|
|
133
|
+
*/
|
|
134
|
+
export async function buildPrBody(root, { base = null } = {}) {
|
|
135
|
+
const resolved = await resolveBase(root, base);
|
|
136
|
+
if (resolved.error !== null) return { error: resolved.error };
|
|
137
|
+
|
|
138
|
+
let tree = null;
|
|
139
|
+
let treeError = null;
|
|
140
|
+
try {
|
|
141
|
+
tree = await treeId(root);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
treeError = String(error.message ?? error);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let entries = [];
|
|
147
|
+
let ledgerErrors = [];
|
|
148
|
+
try {
|
|
149
|
+
const ledger = await readLedger(root);
|
|
150
|
+
entries = tree === null ? [] : ledger.entries.filter((e) => e.tree === tree);
|
|
151
|
+
ledgerErrors = ledger.errors;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
ledgerErrors = [String(error.message ?? error)];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cache = await readCheckCache(root).catch(() => null);
|
|
157
|
+
const { manifest } = await readManifest(root).catch(() => ({ manifest: null }));
|
|
158
|
+
const declared = [
|
|
159
|
+
...(manifest?.gates?.preCommit ?? []),
|
|
160
|
+
...(manifest?.gates?.prePush ?? []),
|
|
161
|
+
];
|
|
162
|
+
const evidenceMode = manifest?.gates?.evidence;
|
|
163
|
+
|
|
164
|
+
const paths = await changedPaths(root, resolved.base);
|
|
165
|
+
const decisions = await changedDecisions(root, paths);
|
|
166
|
+
|
|
167
|
+
const passing = entries.filter((e) => e.exit === 0);
|
|
168
|
+
const unreviewed = [];
|
|
169
|
+
if (tree === null) unreviewed.push(`the tree could not be named (${treeError ?? 'not a git repository'})`);
|
|
170
|
+
else if (passing.length === 0) unreviewed.push('no passing run is recorded for this tree');
|
|
171
|
+
if (cache === null) unreviewed.push('gate state unmeasured (no check has run)');
|
|
172
|
+
for (const error of ledgerErrors) unreviewed.push(`ledger unreadable: ${error}`);
|
|
173
|
+
|
|
174
|
+
// ADR-008's vocabulary, exactly: BLOCK for a known failure, APPROVE WITH
|
|
175
|
+
// RESIDUAL RISK when the review budget ran out before something was read,
|
|
176
|
+
// APPROVE only when everything was. The constraint travels with the verdict:
|
|
177
|
+
// a residual-risk body never converts a known failure into an approval.
|
|
178
|
+
const blocking = cache?.blocking ?? 0;
|
|
179
|
+
const verdict = blocking > 0 ? 'BLOCK' : unreviewed.length > 0 ? 'APPROVE WITH RESIDUAL RISK' : 'APPROVE';
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
error: null,
|
|
183
|
+
base: resolved.base,
|
|
184
|
+
tree,
|
|
185
|
+
evidence: { entries, errors: ledgerErrors },
|
|
186
|
+
decisions,
|
|
187
|
+
paths,
|
|
188
|
+
gates: {
|
|
189
|
+
declared,
|
|
190
|
+
evidenceMode: evidenceMode ?? null,
|
|
191
|
+
run: cache === null ? null : { gatesRun: cache.gatesRun, gatesTotal: cache.gatesTotal },
|
|
192
|
+
rulesOk: cache?.rulesOk ?? null,
|
|
193
|
+
hooksOk: cache?.hooksOk ?? null,
|
|
194
|
+
blocking,
|
|
195
|
+
warnings: cache?.warnings ?? 0,
|
|
196
|
+
cached: cache !== null,
|
|
197
|
+
},
|
|
198
|
+
verdict,
|
|
199
|
+
unreviewed,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The markdown body: four sections in a fixed order — evidence, changed
|
|
205
|
+
* decisions, gates, residual risk — so a snapshot can pin it byte for byte.
|
|
206
|
+
*/
|
|
207
|
+
export function renderPrBody(report) {
|
|
208
|
+
const lines = [];
|
|
209
|
+
lines.push('## Evidence');
|
|
210
|
+
lines.push('');
|
|
211
|
+
if (report.tree === null) {
|
|
212
|
+
lines.push('No tree recorded: run this inside a git repository.');
|
|
213
|
+
} else if (report.evidence.entries.length === 0) {
|
|
214
|
+
lines.push(`tree ${report.tree}`);
|
|
215
|
+
lines.push('No run is recorded for this tree.');
|
|
216
|
+
} else {
|
|
217
|
+
lines.push(`tree ${report.tree}`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
for (const e of report.evidence.entries) {
|
|
220
|
+
lines.push(`- ${e.exit === 0 ? 'pass' : 'fail'} \`${e.cmd}\` (exit ${e.exit}, ${e.summary})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const error of report.evidence.errors) lines.push(`- ledger error: ${error}`);
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push('## Changed decisions');
|
|
226
|
+
lines.push('');
|
|
227
|
+
if (report.decisions.length === 0) {
|
|
228
|
+
lines.push(`None${report.base === null ? ' (worktree diff against HEAD)' : ` (range ${report.base}...HEAD, plus worktree)`}.`);
|
|
229
|
+
} else {
|
|
230
|
+
for (const d of report.decisions) lines.push(`- ${d.id} — ${d.title} (${d.status})`);
|
|
231
|
+
}
|
|
232
|
+
lines.push('');
|
|
233
|
+
lines.push('## Gates');
|
|
234
|
+
lines.push('');
|
|
235
|
+
if (report.gates.cached) {
|
|
236
|
+
const g = report.gates;
|
|
237
|
+
lines.push(`- rules: ${g.rulesOk ? 'ok' : 'failing'}, hooks: ${g.hooksOk ? 'ok' : 'failing'}`);
|
|
238
|
+
lines.push(`- gates run: ${g.run.gatesRun}/${g.run.gatesTotal}, blocking: ${g.blocking}, warnings: ${g.warnings}`);
|
|
239
|
+
if (g.declared.length > 0) lines.push(`- declared: ${g.declared.join(', ')}`);
|
|
240
|
+
} else {
|
|
241
|
+
lines.push('No check has run: gate state unmeasured.');
|
|
242
|
+
}
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push('## Residual risk (ADR-008)');
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(`Verdict: ${report.verdict}`);
|
|
247
|
+
if (report.unreviewed.length > 0) {
|
|
248
|
+
lines.push('');
|
|
249
|
+
lines.push('Not reviewed:');
|
|
250
|
+
for (const item of report.unreviewed) lines.push(`- ${item}`);
|
|
251
|
+
}
|
|
252
|
+
return `${lines.join('\n')}\n`;
|
|
253
|
+
}
|
|
@@ -25,7 +25,7 @@ const SKILL_DIRS = ['.agents/skills', '.cursor/skills', '.claude/skills', '.open
|
|
|
25
25
|
* share a name both stay, because dropping one would hide a trigger.
|
|
26
26
|
*
|
|
27
27
|
* @param {string} root
|
|
28
|
-
* @returns {Promise<{name: string, trigger: string, dir: string}[]>}
|
|
28
|
+
* @returns {Promise<{name: string, trigger: string, dir: string, file: string}[]>}
|
|
29
29
|
*/
|
|
30
30
|
export async function installedSkills(root) {
|
|
31
31
|
const seen = new Set();
|
|
@@ -61,7 +61,7 @@ export async function installedSkills(root) {
|
|
|
61
61
|
const fallback = entry.isDirectory() ? entry.name : entry.name.replace(/\.md$/, '');
|
|
62
62
|
const name = typeof meta.name === 'string' && meta.name !== '' ? meta.name : fallback;
|
|
63
63
|
const trigger = typeof meta.description === 'string' ? meta.description.replace(/\s+/g, ' ').trim() : '';
|
|
64
|
-
skills.push({ name, trigger, dir });
|
|
64
|
+
skills.push({ name, trigger, dir, file: key });
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
skills.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
|
|
@@ -146,10 +146,16 @@ export const LOADERS = [
|
|
|
146
146
|
nested: 'cumulative up to the project root at session start; files below the workspace load when that area is read',
|
|
147
147
|
skills: ['.agents/skills', '.opencode/skills', '.claude/skills'],
|
|
148
148
|
hooks: {
|
|
149
|
-
path: '.opencode/plugins/',
|
|
149
|
+
path: '.opencode/plugins/simt-dev.js',
|
|
150
150
|
verified:
|
|
151
151
|
'OpenCode v2 plugins docs: direct .ts and .js files and immediate plugin packages are loaded from every discovered .opencode/plugins/ directory, and the plugins array in opencode.json(c)',
|
|
152
152
|
},
|
|
153
|
+
agents: {
|
|
154
|
+
dir: '.opencode/agents',
|
|
155
|
+
ext: '.md',
|
|
156
|
+
verified:
|
|
157
|
+
'OpenCode v2 agents docs (opencode.ai/v2/docs/agents, read 2026-10-30): subagents are markdown files with a description frontmatter under .opencode/agents/',
|
|
158
|
+
},
|
|
153
159
|
mcp: {
|
|
154
160
|
path: 'opencode.json',
|
|
155
161
|
key: 'mcp.servers',
|
|
@@ -242,6 +248,12 @@ export const LOADERS = [
|
|
|
242
248
|
verified:
|
|
243
249
|
'Claude Code hooks docs, hook locations: project settings at .claude/settings.json and .claude/settings.local.json, user settings at ~/.claude/settings.json',
|
|
244
250
|
},
|
|
251
|
+
agents: {
|
|
252
|
+
dir: '.claude/agents',
|
|
253
|
+
ext: '.md',
|
|
254
|
+
verified:
|
|
255
|
+
'Claude Code subagents docs (code.claude.com/docs/en/sub-agents, read 2026-10-30): project subagents are markdown files with name and description frontmatter under .claude/agents/',
|
|
256
|
+
},
|
|
245
257
|
mcp: {
|
|
246
258
|
path: '.mcp.json',
|
|
247
259
|
key: null,
|
|
@@ -320,6 +332,12 @@ export const LOADERS = [
|
|
|
320
332
|
verified:
|
|
321
333
|
'Codex hooks docs: <repo>/.codex/hooks.json or ~/.codex/hooks.json, or inline [hooks] tables in config.toml',
|
|
322
334
|
},
|
|
335
|
+
agents: {
|
|
336
|
+
dir: '.codex/agents',
|
|
337
|
+
ext: '.toml',
|
|
338
|
+
verified:
|
|
339
|
+
'Codex custom agents docs (developers.openai.com/codex/subagents, read 2026-10-30): standalone TOML files under .codex/agents/ defining name, description and developer_instructions',
|
|
340
|
+
},
|
|
323
341
|
mcp: {
|
|
324
342
|
path: '.codex/config.toml',
|
|
325
343
|
key: 'mcp_servers',
|
|
@@ -386,6 +404,12 @@ export const LOADERS = [
|
|
|
386
404
|
path: '.cursor/hooks.json',
|
|
387
405
|
verified: 'Cursor agent hooks docs: <project-root>/.cursor/hooks.json and ~/.cursor/hooks.json',
|
|
388
406
|
},
|
|
407
|
+
agents: {
|
|
408
|
+
dir: '.cursor/agents',
|
|
409
|
+
ext: '.md',
|
|
410
|
+
verified:
|
|
411
|
+
'Cursor subagents docs (cursor.com/docs/subagents, read 2026-10-30): project subagents are markdown files with name and description frontmatter under .cursor/agents/',
|
|
412
|
+
},
|
|
389
413
|
mcp: {
|
|
390
414
|
path: '.cursor/mcp.json',
|
|
391
415
|
key: null,
|
|
@@ -470,6 +494,12 @@ export const LOADERS = [
|
|
|
470
494
|
path: '.agents/hooks.json',
|
|
471
495
|
verified: 'Antigravity hooks docs: workspace level .agents/hooks.json, global ~/.gemini/config/hooks.json',
|
|
472
496
|
},
|
|
497
|
+
agents: {
|
|
498
|
+
dir: '.agents/agents',
|
|
499
|
+
ext: '.md',
|
|
500
|
+
verified:
|
|
501
|
+
'Google Antigravity subagents docs (antigravity.google/docs/subagents, read 2026-10-30): project subagents are markdown files with name and description frontmatter under .agents/agents/',
|
|
502
|
+
},
|
|
473
503
|
mcp: {
|
|
474
504
|
path: '.agents/mcp_config.json',
|
|
475
505
|
key: null,
|
|
@@ -541,6 +571,12 @@ export const LOADERS = [
|
|
|
541
571
|
verified:
|
|
542
572
|
'Gemini CLI hooks reference (geminicli.com/docs/hooks/reference/, read 2026-10-26): a project .gemini/settings.json carries a top-level hooks object whose events are arrays of {matcher, hooks:[{type:"command",command,timeout? (milliseconds)}]} — the same shape Claude and Codex use; BeforeTool denies with {decision:"deny",reason}, BeforeAgent and AfterTool inject via hookSpecificOutput.additionalContext',
|
|
543
573
|
},
|
|
574
|
+
agents: {
|
|
575
|
+
dir: '.gemini/agents',
|
|
576
|
+
ext: '.md',
|
|
577
|
+
verified:
|
|
578
|
+
'Gemini CLI subagents docs (geminicli.com/docs/core/subagents, read 2026-10-30): project subagents are markdown files with name and description frontmatter under .gemini/agents/',
|
|
579
|
+
},
|
|
544
580
|
mcp: {
|
|
545
581
|
path: '.gemini/settings.json',
|
|
546
582
|
key: 'mcpServers',
|
|
@@ -603,6 +639,8 @@ export const LOADERS = [
|
|
|
603
639
|
skillsNote: 'no project skill directory is documented for Copilot',
|
|
604
640
|
hooks: null,
|
|
605
641
|
hooksNote: 'no hooks file is documented for Copilot in any first-party page checked on 2026-10-14',
|
|
642
|
+
agents: null,
|
|
643
|
+
agentsNote: 'no file-based agents are documented for Copilot in any first-party page checked on 2026-10-30',
|
|
606
644
|
mcp: {
|
|
607
645
|
path: '.vscode/mcp.json',
|
|
608
646
|
key: null,
|
|
@@ -707,6 +745,8 @@ export const LOADERS = [
|
|
|
707
745
|
hooks: null,
|
|
708
746
|
hooksNote:
|
|
709
747
|
'DeepSeek harness hooks docs: the Claude Code and Codex bridges take an explicit configPath, and project-local hooks.json discovery is still marked TODO, so no repository path is loaded by default',
|
|
748
|
+
agents: null,
|
|
749
|
+
agentsNote: 'no file-based agents are documented for the DeepSeek harness in any first-party page checked on 2026-10-30',
|
|
710
750
|
mcp: {
|
|
711
751
|
path: null,
|
|
712
752
|
key: '@deepseek-ai/dsh-mcp-client',
|
package/src/projection/lock.mjs
CHANGED
|
@@ -13,11 +13,10 @@ import { join } from 'node:path';
|
|
|
13
13
|
|
|
14
14
|
import { readLockedVariants } from '../kernel/variants.mjs';
|
|
15
15
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
16
|
+
import { LOCK_FILE, LOCK_LEGACY } from '../name.mjs';
|
|
16
17
|
import { desiredRegion, provenanceDigest, LOCAL_PROFILE_DIR } from './rules.mjs';
|
|
17
18
|
import { readManifest } from './manifest.mjs';
|
|
18
19
|
|
|
19
|
-
export const LOCK_FILE = 'simt-dev.lock.json';
|
|
20
|
-
|
|
21
20
|
/** Manifest keys that change what the region contains. Anything else in the file is
|
|
22
21
|
* the repository's own notes, and hashing it would make a comment fail the lock. */
|
|
23
22
|
const MANIFEST_KEYS = ['mode', 'budget', 'shared', 'surfaces', 'gates'];
|
|
@@ -28,12 +27,15 @@ const MANIFEST_KEYS = ['mode', 'budget', 'shared', 'surfaces', 'gates'];
|
|
|
28
27
|
* @param {string} root
|
|
29
28
|
* @param {object} [flags] the same flags `sync` rendered with, so the output is the
|
|
30
29
|
* body that was written and not a body for a different set of surfaces
|
|
30
|
+
* @param {object} [manifest] the manifest to digest, in place of the one on disk. A
|
|
31
|
+
* writer that is about to write a manifest (a fresh `init`) passes it here so the
|
|
32
|
+
* lock records the state the repo will have, not the state it has (Q29).
|
|
31
33
|
*/
|
|
32
|
-
export async function computeLock(root, flags = {}) {
|
|
34
|
+
export async function computeLock(root, flags = {}, manifest = undefined) {
|
|
33
35
|
const variants =
|
|
34
36
|
flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
|
|
35
37
|
const desired = await desiredRegion(root, { ...flags, variants });
|
|
36
|
-
const inputs = await inputDigest(root, variants);
|
|
38
|
+
const inputs = await inputDigest(root, variants, manifest);
|
|
37
39
|
const outputs = createHash('sha256').update(desired.body).digest('hex');
|
|
38
40
|
const hasVariants =
|
|
39
41
|
variants && typeof variants === 'object' && Object.keys(variants).length > 0;
|
|
@@ -41,7 +43,7 @@ export async function computeLock(root, flags = {}) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
|
-
* Record locked variant winners in `simt
|
|
46
|
+
* Record locked variant winners in `simt.lock.json` alongside updated digests.
|
|
45
47
|
*/
|
|
46
48
|
export async function writeLockVariants(root, nextVariants, flags = {}) {
|
|
47
49
|
const existing = await readLockedVariants(root);
|
|
@@ -53,15 +55,20 @@ export async function writeLockVariants(root, nextVariants, flags = {}) {
|
|
|
53
55
|
|
|
54
56
|
/** Read a lock, or null when the file is not there. A file that does not parse is an
|
|
55
57
|
* error the caller reports, not a missing lock: absence and damage are different
|
|
56
|
-
* repairs.
|
|
58
|
+
* repairs. A lock under the legacy name is a lock (ADR-110): it is read until the
|
|
59
|
+
* next write migrates it, and a permission failure on the new name is not an excuse
|
|
60
|
+
* to fall back to an older file. */
|
|
57
61
|
export async function readLock(root) {
|
|
58
|
-
let text;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
let text = null;
|
|
63
|
+
for (const name of [LOCK_FILE, LOCK_LEGACY]) {
|
|
64
|
+
try {
|
|
65
|
+
text = await readFile(join(root, name), 'utf8');
|
|
66
|
+
break;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code !== 'ENOENT') throw error;
|
|
69
|
+
}
|
|
64
70
|
}
|
|
71
|
+
if (text === null) return { lock: null, error: null };
|
|
65
72
|
try {
|
|
66
73
|
const lock = JSON.parse(text);
|
|
67
74
|
if (typeof lock?.inputs !== 'string' || typeof lock?.outputs !== 'string') {
|
|
@@ -79,13 +86,13 @@ export async function readLock(root) {
|
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
async function inputDigest(root, variants = {}) {
|
|
89
|
+
async function inputDigest(root, variants = {}, manifest = undefined) {
|
|
83
90
|
const hash = createHash('sha256');
|
|
84
91
|
hash.update(`provenance:${await provenanceDigest()}\n`);
|
|
85
92
|
const { stack, facts } = await fingerprint(root);
|
|
86
93
|
hash.update(`signals:${JSON.stringify({ stack, facts })}\n`);
|
|
87
|
-
const { manifest } = await readManifest(root);
|
|
88
94
|
const owned = {};
|
|
95
|
+
if (manifest === undefined) manifest = (await readManifest(root)).manifest;
|
|
89
96
|
if (manifest && typeof manifest === 'object') {
|
|
90
97
|
for (const key of MANIFEST_KEYS) {
|
|
91
98
|
if (manifest[key] !== undefined) owned[key] = manifest[key];
|