simt-dev 0.6.3 → 0.7.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 +162 -841
- package/README.md +21 -135
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-014-workspace-is-configuration.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-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- 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/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/package.json +13 -3
- package/rules/core.md +9 -2
- package/rules/mdc/slice-commit.mdc +57 -4
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- 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 +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- 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 +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +10 -3
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/api.mjs +29 -0
- package/src/argv.mjs +37 -3
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/adapters.json +16 -0
- package/src/bench/corpus.mjs +150 -0
- package/src/bench/grade.mjs +23 -49
- package/src/bench/mine.mjs +247 -36
- package/src/bench/probes.mjs +104 -68
- package/src/bench/process.mjs +128 -0
- package/src/bench/report.mjs +260 -84
- package/src/bench/run.mjs +251 -150
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +44 -3
- package/src/ci/workflow.mjs +46 -14
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +205 -0
- package/src/commands/agenthook.mjs +113 -20
- package/src/commands/bench.mjs +192 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +2 -1
- package/src/commands/compare.mjs +50 -49
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +10 -9
- package/src/commands/distill.mjs +4 -3
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +19 -12
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +98 -28
- package/src/commands/graph.mjs +20 -8
- package/src/commands/harness.mjs +3 -2
- package/src/commands/help.mjs +160 -31
- package/src/commands/hooks.mjs +163 -50
- package/src/commands/ignore.mjs +2 -1
- package/src/commands/impact.mjs +8 -9
- package/src/commands/index.mjs +278 -180
- package/src/commands/init.mjs +171 -38
- package/src/commands/lease.mjs +206 -39
- package/src/commands/lessons.mjs +72 -15
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +47 -27
- package/src/commands/mcp.mjs +0 -1
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +3 -3
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +5 -3
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +124 -33
- package/src/commands/shared.mjs +2 -3
- package/src/commands/skills.mjs +82 -45
- package/src/commands/skillscompile.mjs +12 -12
- package/src/commands/skillsinstall.mjs +14 -2
- package/src/commands/slice.mjs +396 -26
- package/src/commands/spec.mjs +151 -0
- package/src/commands/status.mjs +4 -7
- package/src/commands/sync.mjs +50 -33
- package/src/commands/tests.mjs +11 -11
- package/src/commands/tokens.mjs +2 -2
- package/src/commands/undo.mjs +29 -31
- package/src/commands/upgrade.mjs +40 -4
- package/src/commands/why.mjs +19 -12
- 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 -3
- package/src/completions.mjs +5 -0
- package/src/context.mjs +64 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +17 -2
- package/src/distill/engine.mjs +5 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +42 -20
- package/src/hooks/capability.mjs +17 -4
- package/src/hooks/dispatcher.mjs +513 -36
- package/src/hooks/gates.mjs +0 -43
- package/src/hooks/resolve.mjs +11 -10
- package/src/hooks/shim.mjs +17 -9
- package/src/identity.mjs +96 -15
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +372 -84
- 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 +97 -15
- package/src/ledger/index.mjs +120 -25
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/resources.mjs +12 -13
- package/src/mcp/server.mjs +151 -38
- package/src/mcp/tools.mjs +90 -16
- package/src/measure/session.mjs +3 -2
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +111 -3
- package/src/packs/manage.mjs +17 -3
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- 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 +31 -14
- package/src/projection/manifest.mjs +89 -53
- 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 +58 -6
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +88 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +24 -15
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +6 -3
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +8 -0
- package/src/tsconfig.mjs +39 -99
- package/src/workspace.mjs +55 -10
- package/templates/plan/tracker.json +1 -1
- package/docs/decisions/README.md +0 -591
package/src/plan/index.mjs
CHANGED
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
// the file was read. Status and dependency checks never touch it: a 1 MB
|
|
21
21
|
// section that belongs to a later slice is not copied into the index.
|
|
22
22
|
|
|
23
|
-
import { readdir, readFile
|
|
23
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
|
|
26
26
|
import { CLI } from '../name.mjs';
|
|
27
|
+
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
27
28
|
import { loadFormat } from './template.mjs';
|
|
28
29
|
|
|
29
30
|
function clean(cell) {
|
|
@@ -100,10 +101,16 @@ function sliceKey(path, id) {
|
|
|
100
101
|
* empty cell is the pipe that closes it. A further empty cell is real: the
|
|
101
102
|
* evidence column is often blank, and stripping every trailing empty would
|
|
102
103
|
* delete it and shift the state into the wrong column.
|
|
104
|
+
*
|
|
105
|
+
* A pipe inside a code span is content, not a separator: an evidence cell may
|
|
106
|
+
* carry a command line with a pipe in it, and splitting there shifts every
|
|
107
|
+
* later cell left by one (review 0003 Q25). Backticks toggle the span; a
|
|
108
|
+
* `\|` escape is content in either mode.
|
|
103
109
|
*/
|
|
104
110
|
function splitRow(line) {
|
|
105
111
|
const cells = [];
|
|
106
112
|
let current = '';
|
|
113
|
+
let inCode = false;
|
|
107
114
|
for (let i = 0; i < line.length; i += 1) {
|
|
108
115
|
const char = line[i];
|
|
109
116
|
if (char === '\\' && i + 1 < line.length) {
|
|
@@ -111,7 +118,12 @@ function splitRow(line) {
|
|
|
111
118
|
i += 1;
|
|
112
119
|
continue;
|
|
113
120
|
}
|
|
114
|
-
if (char === '
|
|
121
|
+
if (char === '`') {
|
|
122
|
+
inCode = !inCode;
|
|
123
|
+
current += char;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (char === '|' && !inCode) {
|
|
115
127
|
cells.push(current);
|
|
116
128
|
current = '';
|
|
117
129
|
continue;
|
|
@@ -596,9 +608,16 @@ export async function planDone(root, id, { sha = null, test = null, state = null
|
|
|
596
608
|
return { root, id, error: `${id} is no longer on line ${slice.line} of ${slice.docName}`, remedy: `run: ${CLI} plan status` };
|
|
597
609
|
}
|
|
598
610
|
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
611
|
+
const original = found.lines[found.index];
|
|
612
|
+
const cells = splitRow(original);
|
|
613
|
+
if (cells.length !== slice.columnCount) {
|
|
614
|
+
return {
|
|
615
|
+
root,
|
|
616
|
+
id,
|
|
617
|
+
error: `the row for ${id} has ${cells.length} cells; the table declares ${slice.columnCount}`,
|
|
618
|
+
remedy: `fix the row in ${slice.docName} — an unescaped pipe splits it — and retry`,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
602
621
|
const from = cells[slice.stateIdx] ?? '';
|
|
603
622
|
cells[slice.stateIdx] = replaceState(from, display);
|
|
604
623
|
|
|
@@ -613,14 +632,18 @@ export async function planDone(root, id, { sha = null, test = null, state = null
|
|
|
613
632
|
evidenceNote = 'no evidence column in this tracker; the sha was not recorded';
|
|
614
633
|
}
|
|
615
634
|
|
|
616
|
-
|
|
617
|
-
|
|
635
|
+
// The rebuilt line keeps the original's ending: a CRLF file must not gain
|
|
636
|
+
// one LF-terminated line among its CRLF lines (review 0003 Q25).
|
|
637
|
+
const eol = original.endsWith('\r') ? '\r' : '';
|
|
638
|
+
const rebuilt = `| ${cells.join(' | ')} |${eol}`;
|
|
618
639
|
const changedCells = splitRow(original).join('\0') !== cells.join('\0');
|
|
619
640
|
const newLine = changedCells ? rebuilt : original;
|
|
620
641
|
const wouldChange = newLine !== original;
|
|
621
642
|
if (wouldChange && !dryRun) {
|
|
622
643
|
found.lines[found.index] = newLine;
|
|
623
|
-
|
|
644
|
+
const journal = [];
|
|
645
|
+
await journaledWriteFile(root, journal, slice.docPath, found.lines.join('\n'));
|
|
646
|
+
await commitJournal(root, 'plan', journal);
|
|
624
647
|
}
|
|
625
648
|
|
|
626
649
|
const apply = `run: ${CLI} plan done ${id}${sha ? ` ${sha}` : ''}${display === format.doneWrite ? '' : ` --state ${JSON.stringify(display)}`}${test ? ` --test ${JSON.stringify(test)}` : ''} --yes`;
|
package/src/plan/template.mjs
CHANGED
|
@@ -23,7 +23,7 @@ const KNOWN = new Set(['$comment', 'name', 'directories', 'id', 'columns', 'stat
|
|
|
23
23
|
const ID_KEYS = new Set(['pattern', 'headers']);
|
|
24
24
|
const COLUMN_KEYS = new Set(['state', 'depends', 'title', 'evidence']);
|
|
25
25
|
const DIRECTORY_KEYS = new Set(['kind', 'word', 'path']);
|
|
26
|
-
const STATE_KEYS = new Set(['id', 'write', 'read', 'satisfies']);
|
|
26
|
+
const STATE_KEYS = new Set(['id', 'write', 'read', 'satisfies', 'role']);
|
|
27
27
|
const DEPENDENCY_KEYS = new Set(['empty', 'separators', 'notes', 'group']);
|
|
28
28
|
const WAVE_KEYS = new Set(['label', 'members']);
|
|
29
29
|
const SECTION_KEYS = new Set(['depth']);
|
|
@@ -148,6 +148,7 @@ function compileStates(source, value) {
|
|
|
148
148
|
const write = new Map();
|
|
149
149
|
const known = new Set();
|
|
150
150
|
const satisfies = new Set();
|
|
151
|
+
const roles = new Map();
|
|
151
152
|
const order = [];
|
|
152
153
|
const stateNames = [];
|
|
153
154
|
for (let i = 0; i < value.length; i += 1) {
|
|
@@ -162,6 +163,13 @@ function compileStates(source, value) {
|
|
|
162
163
|
}
|
|
163
164
|
if (!Array.isArray(entry.read) || entry.read.length === 0) return bad(source, `${where}.read must list the words that mean this state`);
|
|
164
165
|
if (known.has(entry.id)) return bad(source, `state ${entry.id} is listed twice`);
|
|
166
|
+
if (entry.role !== undefined && (typeof entry.role !== 'string' || !/^[a-z][a-z0-9-]*$/.test(entry.role))) {
|
|
167
|
+
return bad(source, `${where}.role must be a lower-case word`);
|
|
168
|
+
}
|
|
169
|
+
if (typeof entry.role === 'string') {
|
|
170
|
+
if (roles.has(entry.role)) return bad(source, `role ${entry.role} is claimed twice`);
|
|
171
|
+
roles.set(entry.role, entry.write.trim());
|
|
172
|
+
}
|
|
165
173
|
const display = entry.write.trim();
|
|
166
174
|
const aliases = new Set([entry.id, display.toLowerCase()]);
|
|
167
175
|
for (const alias of entry.read) {
|
|
@@ -182,7 +190,7 @@ function compileStates(source, value) {
|
|
|
182
190
|
else if (entry.satisfies != null && entry.satisfies !== false) return bad(source, `${where}.satisfies must be true or false`);
|
|
183
191
|
}
|
|
184
192
|
if (satisfies.size === 0) return bad(source, 'one state must have satisfies true, or nothing can ever be ready');
|
|
185
|
-
return { read, write, known, satisfies, order, stateNames };
|
|
193
|
+
return { read, write, known, satisfies, roles, order, stateNames };
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
function compile(body, source, root) {
|
|
@@ -262,7 +270,7 @@ function compile(body, source, root) {
|
|
|
262
270
|
|
|
263
271
|
const byWord = new Map(directories.directories.map((dir) => [dir.word, dir.kind]));
|
|
264
272
|
const doneWrite = body.states.find((entry) => entry.satisfies === true).write.trim();
|
|
265
|
-
const { read, write } = states;
|
|
273
|
+
const { read, write, roles } = states;
|
|
266
274
|
|
|
267
275
|
return {
|
|
268
276
|
format: {
|
|
@@ -273,6 +281,12 @@ function compile(body, source, root) {
|
|
|
273
281
|
order: states.order,
|
|
274
282
|
ready,
|
|
275
283
|
doneWrite,
|
|
284
|
+
// The display a command moves a row to when work on it starts: the state
|
|
285
|
+
// carrying the `started` role, or null when no state claims it (review
|
|
286
|
+
// 0003 Q23). A null here is not an error — the caller falls back to the
|
|
287
|
+
// in-progress spelling it can read, and `plan done` fails closed on a
|
|
288
|
+
// word the template never defined.
|
|
289
|
+
startedWrite: roles.get('started') ?? null,
|
|
276
290
|
stateNames: states.stateNames,
|
|
277
291
|
satisfies: states.satisfies,
|
|
278
292
|
known: states.known,
|
|
@@ -292,9 +306,17 @@ function compile(body, source, root) {
|
|
|
292
306
|
headingRe: new RegExp(`^(#{1,${depth}})\\s+(.*\\S)\\s*$`),
|
|
293
307
|
readState(raw) {
|
|
294
308
|
const plain = String(raw ?? '').replace(/\*\*/g, '').trim().toLowerCase();
|
|
309
|
+
if (plain === '') return 'unknown';
|
|
295
310
|
if (read.has(plain)) return read.get(plain);
|
|
296
|
-
|
|
297
|
-
|
|
311
|
+
// A cell may carry a note after the state — "In progress (since Monday)".
|
|
312
|
+
// The state is the longest alias that prefixes the text: "in progress"
|
|
313
|
+
// must win over a shorter alias it starts, and a word no state claims
|
|
314
|
+
// is unknown, not a state (review 0003 Q25).
|
|
315
|
+
let best = '';
|
|
316
|
+
for (const alias of read.keys()) {
|
|
317
|
+
if (alias.length > best.length && plain.startsWith(alias)) best = alias;
|
|
318
|
+
}
|
|
319
|
+
return best !== '' ? read.get(best) : 'unknown';
|
|
298
320
|
},
|
|
299
321
|
canonical(input) {
|
|
300
322
|
return write.get(String(input ?? '').trim().toLowerCase()) ?? null;
|
package/src/plugins.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { desiredRegion } from './projection/rules.mjs';
|
|
|
32
32
|
import { PACKAGE_NAME, BIN_REL } from './name.mjs';
|
|
33
33
|
|
|
34
34
|
/** The kinds `simt plugin` builds, in the order the help line names them. */
|
|
35
|
-
export const PLUGIN_KINDS = ['claude', 'gemini', 'cursor'];
|
|
35
|
+
export const PLUGIN_KINDS = ['claude', 'gemini', 'cursor', 'opencode'];
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* The package this code ships in: the runtime a generated package copies. Resolved
|
|
@@ -71,7 +71,11 @@ function rootFlag(kind) {
|
|
|
71
71
|
|
|
72
72
|
/** Where the hooks document lands inside a package, relative to the package root. */
|
|
73
73
|
function hooksPath(kind) {
|
|
74
|
-
|
|
74
|
+
if (kind === 'cursor') return join('.cursor', 'hooks.json');
|
|
75
|
+
// OpenCode loads a local plugin directory's root `server` file as the plugin, so the
|
|
76
|
+
// generated document is that file at the root rather than a hooks config to merge.
|
|
77
|
+
if (kind === 'opencode') return 'server.js';
|
|
78
|
+
return join('hooks', 'hooks.json');
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
package/src/prbody.mjs
CHANGED
|
@@ -21,11 +21,10 @@ import { readLedger, treeId } from './ledger/index.mjs';
|
|
|
21
21
|
import { readCheckCache } from './status.mjs';
|
|
22
22
|
import { readManifest } from './projection/manifest.mjs';
|
|
23
23
|
import { splitFrontmatter } from './memory/decide.mjs';
|
|
24
|
+
import { decisionsDir } from './decisions/dir.mjs';
|
|
24
25
|
|
|
25
26
|
const exec = promisify(execFile);
|
|
26
27
|
|
|
27
|
-
const DECISIONS_DIR = join('docs', 'decisions');
|
|
28
|
-
|
|
29
28
|
/**
|
|
30
29
|
* The ref the body diffs against: the override when given, else the first
|
|
31
30
|
* candidate that shares history with HEAD. Returns the ref, or null when the
|
|
@@ -98,15 +97,18 @@ async function changedPaths(root, base) {
|
|
|
98
97
|
* reported with what it has rather than dropping the row.
|
|
99
98
|
*/
|
|
100
99
|
async function changedDecisions(root, paths) {
|
|
101
|
-
|
|
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'));
|
|
102
104
|
try {
|
|
103
|
-
await readdir(
|
|
105
|
+
await readdir(absDir);
|
|
104
106
|
} catch {
|
|
105
107
|
return [];
|
|
106
108
|
}
|
|
107
109
|
const out = [];
|
|
108
110
|
for (const name of names.sort()) {
|
|
109
|
-
const id = /^ADR-\d{3,}/i.exec(name.slice(
|
|
111
|
+
const id = /^ADR-\d{3,}/i.exec(name.slice(relDir.length + 1))?.[0].toUpperCase() ?? name;
|
|
110
112
|
let title = id;
|
|
111
113
|
let status = 'unknown';
|
|
112
114
|
try {
|
|
@@ -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,10 +13,14 @@ 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
|
-
|
|
20
|
+
/** The lock's own format. Bumped only when a reader must interpret an old file
|
|
21
|
+
* differently; an absent field means 1, because every lock written before this
|
|
22
|
+
* field existed was written by the only format there has been. */
|
|
23
|
+
const LOCK_VERSION = 1;
|
|
20
24
|
|
|
21
25
|
/** Manifest keys that change what the region contains. Anything else in the file is
|
|
22
26
|
* the repository's own notes, and hashing it would make a comment fail the lock. */
|
|
@@ -28,20 +32,23 @@ const MANIFEST_KEYS = ['mode', 'budget', 'shared', 'surfaces', 'gates'];
|
|
|
28
32
|
* @param {string} root
|
|
29
33
|
* @param {object} [flags] the same flags `sync` rendered with, so the output is the
|
|
30
34
|
* body that was written and not a body for a different set of surfaces
|
|
35
|
+
* @param {object} [manifest] the manifest to digest, in place of the one on disk. A
|
|
36
|
+
* writer that is about to write a manifest (a fresh `init`) passes it here so the
|
|
37
|
+
* lock records the state the repo will have, not the state it has (Q29).
|
|
31
38
|
*/
|
|
32
|
-
export async function computeLock(root, flags = {}) {
|
|
39
|
+
export async function computeLock(root, flags = {}, manifest = undefined) {
|
|
33
40
|
const variants =
|
|
34
41
|
flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
|
|
35
42
|
const desired = await desiredRegion(root, { ...flags, variants });
|
|
36
|
-
const inputs = await inputDigest(root, variants);
|
|
43
|
+
const inputs = await inputDigest(root, variants, manifest);
|
|
37
44
|
const outputs = createHash('sha256').update(desired.body).digest('hex');
|
|
38
45
|
const hasVariants =
|
|
39
46
|
variants && typeof variants === 'object' && Object.keys(variants).length > 0;
|
|
40
|
-
return hasVariants ? { inputs, outputs, variants } : { inputs, outputs };
|
|
47
|
+
return hasVariants ? { version: LOCK_VERSION, inputs, outputs, variants } : { version: LOCK_VERSION, inputs, outputs };
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
|
-
* Record locked variant winners in `simt
|
|
51
|
+
* Record locked variant winners in `simt.lock.json` alongside updated digests.
|
|
45
52
|
*/
|
|
46
53
|
export async function writeLockVariants(root, nextVariants, flags = {}) {
|
|
47
54
|
const existing = await readLockedVariants(root);
|
|
@@ -53,17 +60,27 @@ export async function writeLockVariants(root, nextVariants, flags = {}) {
|
|
|
53
60
|
|
|
54
61
|
/** Read a lock, or null when the file is not there. A file that does not parse is an
|
|
55
62
|
* error the caller reports, not a missing lock: absence and damage are different
|
|
56
|
-
* repairs.
|
|
63
|
+
* repairs. A lock under the legacy name is a lock (ADR-110): it is read until the
|
|
64
|
+
* next write migrates it, and a permission failure on the new name is not an excuse
|
|
65
|
+
* to fall back to an older file. */
|
|
57
66
|
export async function readLock(root) {
|
|
58
|
-
let text;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
let text = null;
|
|
68
|
+
for (const name of [LOCK_FILE, LOCK_LEGACY]) {
|
|
69
|
+
try {
|
|
70
|
+
text = await readFile(join(root, name), 'utf8');
|
|
71
|
+
break;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== 'ENOENT') throw error;
|
|
74
|
+
}
|
|
64
75
|
}
|
|
76
|
+
if (text === null) return { lock: null, error: null };
|
|
65
77
|
try {
|
|
66
78
|
const lock = JSON.parse(text);
|
|
79
|
+
// Absent means 1; a version this build does not know is a lock it cannot judge,
|
|
80
|
+
// and reading it anyway would report drift for a format change.
|
|
81
|
+
if (lock.version !== undefined && (!Number.isInteger(lock.version) || lock.version < 1)) {
|
|
82
|
+
return { lock: null, error: 'version must be an integer of at least 1' };
|
|
83
|
+
}
|
|
67
84
|
if (typeof lock?.inputs !== 'string' || typeof lock?.outputs !== 'string') {
|
|
68
85
|
return { lock: null, error: 'must be an object with inputs and outputs' };
|
|
69
86
|
}
|
|
@@ -79,13 +96,13 @@ export async function readLock(root) {
|
|
|
79
96
|
}
|
|
80
97
|
}
|
|
81
98
|
|
|
82
|
-
async function inputDigest(root, variants = {}) {
|
|
99
|
+
async function inputDigest(root, variants = {}, manifest = undefined) {
|
|
83
100
|
const hash = createHash('sha256');
|
|
84
101
|
hash.update(`provenance:${await provenanceDigest()}\n`);
|
|
85
102
|
const { stack, facts } = await fingerprint(root);
|
|
86
103
|
hash.update(`signals:${JSON.stringify({ stack, facts })}\n`);
|
|
87
|
-
const { manifest } = await readManifest(root);
|
|
88
104
|
const owned = {};
|
|
105
|
+
if (manifest === undefined) manifest = (await readManifest(root)).manifest;
|
|
89
106
|
if (manifest && typeof manifest === 'object') {
|
|
90
107
|
for (const key of MANIFEST_KEYS) {
|
|
91
108
|
if (manifest[key] !== undefined) owned[key] = manifest[key];
|