simt-dev 0.7.1 → 0.8.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 +36 -1
- package/README.md +169 -80
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
- package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +3 -1
- package/rules/mdc/intake-questions.mdc +8 -0
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +7 -5
- package/src/argv.mjs +19 -0
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +155 -113
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +12 -26
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +17 -535
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +15 -1037
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +116 -971
- package/src/bench/run.mjs +164 -881
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +11 -13
- package/src/ci/dependabot.mjs +25 -55
- package/src/commands/agent.mjs +1 -9
- package/src/commands/agenthook.mjs +66 -35
- package/src/commands/bench.mjs +2 -2
- package/src/commands/ci.mjs +11 -13
- package/src/commands/docs.mjs +88 -0
- package/src/commands/gate.mjs +10 -13
- package/src/commands/harness.mjs +3 -3
- package/src/commands/help.mjs +44 -3
- package/src/commands/hooks.mjs +20 -20
- package/src/commands/ignore.mjs +11 -7
- package/src/commands/impact.mjs +4 -8
- package/src/commands/index.mjs +49 -9
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +122 -104
- package/src/commands/lease.mjs +67 -18
- package/src/commands/map.mjs +10 -5
- package/src/commands/mcp.mjs +2 -17
- package/src/commands/repoindex.mjs +8 -8
- package/src/commands/rules.mjs +8 -4
- package/src/commands/shared.mjs +12 -16
- package/src/commands/skills.mjs +5 -6
- package/src/commands/skillscompile.mjs +3 -3
- package/src/commands/skillsinstall.mjs +5 -4
- package/src/commands/slice.mjs +199 -64
- package/src/commands/spec.mjs +78 -62
- package/src/commands/status.mjs +24 -12
- package/src/commands/sync.mjs +14 -14
- package/src/commands/tests.mjs +12 -12
- package/src/commands/tokens.mjs +19 -24
- package/src/commands/upgrade.mjs +8 -4
- package/src/commands/why.mjs +6 -13
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/decisions/index.mjs +3 -2
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +2 -1
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +5 -3
- package/src/hooks/dispatcher.mjs +26 -32
- package/src/hooks/gates.mjs +12 -2
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +2 -11
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +16 -11
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +124 -17
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +49 -9
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -1005
- package/src/{bench → measure}/corpus.mjs +1 -1
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/output/index.mjs +122 -31
- package/src/packs/manage.mjs +15 -9
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/manifest.mjs +10 -435
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +16 -547
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +13 -1
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +0 -7
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +8 -12
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +1 -1
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +3 -1
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/src/bench/tasks.mjs +0 -36
package/src/commands/ci.mjs
CHANGED
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
// commands owns which file would be learning about ownership twice to learn nothing
|
|
19
19
|
// new, and the next .github file would arrive with a third command to remember.
|
|
20
20
|
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { commitJournal, journaledWriteFile } from '../journal.mjs';
|
|
21
|
+
import { readFile } from 'node:fs/promises';
|
|
22
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
24
23
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
25
24
|
|
|
26
25
|
import { EXIT, emit, emitJson, isJson, note, writeMode } from '../output/index.mjs';
|
|
@@ -39,7 +38,7 @@ import { commandRoot } from '../repo-root.mjs';
|
|
|
39
38
|
function targets() {
|
|
40
39
|
return [
|
|
41
40
|
{ key: 'workflow', file: WORKFLOW_FILE, plan: planWorkflow, render: () => renderWorkflow({ own: true }) },
|
|
42
|
-
{ key: 'dependabot', file: DEPENDABOT_FILE, plan: planDependabot, render: (plan) => renderDependabot(plan.facts ?? [], { stack: plan.stack ?? [], own: true }) },
|
|
41
|
+
{ key: 'dependabot', file: DEPENDABOT_FILE, plan: planDependabot, render: (plan) => renderDependabot(plan.facts ?? [], { stack: plan.stack ?? [], own: true, root: plan.root }) },
|
|
43
42
|
];
|
|
44
43
|
}
|
|
45
44
|
|
|
@@ -153,19 +152,18 @@ export async function ciCommand(positional, flags) {
|
|
|
153
152
|
return EXIT.OK;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
// Journaled so `undo` restores this run (R49).
|
|
157
|
-
|
|
155
|
+
// Journaled so `undo` restores this run (R49). The renderer takes the plan because
|
|
156
|
+
// detection-driven content is a function of this repo's facts: a dependabot config
|
|
157
|
+
// rendered without them would watch nothing, which is the same silent-nothing the
|
|
158
|
+
// detection ruling exists to stop.
|
|
159
|
+
const changes = [];
|
|
158
160
|
for (const { key, plan, render } of targets()) {
|
|
159
161
|
const p = plans.find((x) => x.key === key);
|
|
160
162
|
if (p.action === 'unchanged') continue;
|
|
161
|
-
await
|
|
162
|
-
// The renderer takes the plan because detection-driven content is a function of
|
|
163
|
-
// this repo's facts: a dependabot config rendered without them would watch
|
|
164
|
-
// nothing, which is the same silent-nothing the detection ruling exists to stop.
|
|
165
|
-
await journaledWriteFile(root, journal, p.path, render(p));
|
|
166
|
-
note(`wrote ${p.file} (${p.action}; the template and this copy now agree)`);
|
|
163
|
+
changes.push(change({ path: p.path, before: await readBefore(p.path), after: render(p), owner: 'ci', reason: 'the template and this copy now agree' }));
|
|
167
164
|
}
|
|
168
|
-
await
|
|
165
|
+
await applyPlan(root, changes, { mode, label: 'ci' });
|
|
166
|
+
for (const c of changes) note(`wrote ${c.path.replace(`${root}/`, '')} (${c.action}; ${c.reason})`);
|
|
169
167
|
if (isJson(flags)) {
|
|
170
168
|
emitJson(
|
|
171
169
|
{
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// `simt docs readme` — put the README's command list back in step with the spec.
|
|
2
|
+
//
|
|
3
|
+
// Shape copied from `ci` and `ignore`: the plan is printed first, nothing is written
|
|
4
|
+
// without `--yes`, `--diff` shows the same plan as a unified diff, and `undo` restores
|
|
5
|
+
// it. The rendering itself lives in `src/docs/commands.mjs` so a test can compare the
|
|
6
|
+
// README against it without running this command.
|
|
7
|
+
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { EXIT, fail, present, writeMode } from '../output/index.mjs';
|
|
11
|
+
import { CLI } from '../name.mjs';
|
|
12
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
13
|
+
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
14
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
15
|
+
import { BEGIN, END, README_FILE, renderCommandBlock, replaceBlock } from '../docs/commands.mjs';
|
|
16
|
+
|
|
17
|
+
export async function docsCommand(sub, args, flags) {
|
|
18
|
+
if (sub !== 'readme') {
|
|
19
|
+
return fail(`unknown docs page \`${sub}\``, EXIT.USAGE, `run: ${CLI} docs readme`);
|
|
20
|
+
}
|
|
21
|
+
const root = commandRoot(flags);
|
|
22
|
+
const path = join(root, README_FILE);
|
|
23
|
+
|
|
24
|
+
let before;
|
|
25
|
+
try {
|
|
26
|
+
before = await readBefore(path);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return fail(
|
|
29
|
+
`${README_FILE} could not be read: ${error.message}`,
|
|
30
|
+
EXIT.FAIL,
|
|
31
|
+
`fix the file's permissions, then re-run: ${CLI} docs readme`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (before === null) {
|
|
35
|
+
return fail(`no ${README_FILE} in ${root}`, EXIT.FAIL, `run this where ${README_FILE} is`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const after = replaceBlock(before, renderCommandBlock());
|
|
39
|
+
if (after === null) {
|
|
40
|
+
return fail(
|
|
41
|
+
`${README_FILE} carries no ${BEGIN} ... ${END} region`,
|
|
42
|
+
EXIT.FAIL,
|
|
43
|
+
`put the two marker lines where the command list belongs, then: ${CLI} docs readme --yes`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mode = writeMode(flags);
|
|
48
|
+
const action = after === before ? 'unchanged' : 'write';
|
|
49
|
+
const payload = {
|
|
50
|
+
ok: true,
|
|
51
|
+
command: 'docs',
|
|
52
|
+
page: 'readme',
|
|
53
|
+
root,
|
|
54
|
+
file: README_FILE,
|
|
55
|
+
action,
|
|
56
|
+
written: mode.apply && action === 'write',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (action === 'unchanged') {
|
|
60
|
+
present(payload, () => `current ${README_FILE}`, flags);
|
|
61
|
+
return EXIT.OK;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const diff = [{ path: README_FILE, before, after }];
|
|
65
|
+
if (!mode.apply) {
|
|
66
|
+
// A rehearsal says which flag it is waiting for, because "not written" is the same
|
|
67
|
+
// sentence for `--dry-run` and for a missing `--yes`, and the two have different fixes.
|
|
68
|
+
present(
|
|
69
|
+
{ ...payload, ...diffField(diff, flags) },
|
|
70
|
+
() => `plan ${README_FILE}: the command list follows the spec${mode.dryRun ? ' (--dry-run)' : ' (re-run with --yes to write)'}`,
|
|
71
|
+
flags,
|
|
72
|
+
);
|
|
73
|
+
emitDiffs(diff, flags);
|
|
74
|
+
return EXIT.OK;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await applyPlan(root, [change({ path, before, after, owner: 'docs readme', reason: 'the command list follows the spec' })], {
|
|
78
|
+
mode,
|
|
79
|
+
label: 'docs readme',
|
|
80
|
+
});
|
|
81
|
+
present(
|
|
82
|
+
{ ...payload, ...diffField(diff, flags) },
|
|
83
|
+
() => `write ${README_FILE} (${Buffer.byteLength(after)} bytes)`,
|
|
84
|
+
flags,
|
|
85
|
+
);
|
|
86
|
+
emitDiffs(diff, flags);
|
|
87
|
+
return EXIT.OK;
|
|
88
|
+
}
|
package/src/commands/gate.mjs
CHANGED
|
@@ -13,12 +13,15 @@ import { promisify } from 'node:util';
|
|
|
13
13
|
|
|
14
14
|
import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
|
|
15
15
|
import { CLI } from '../name.mjs';
|
|
16
|
-
import { detectTestRunner } from '../
|
|
16
|
+
import { detectTestRunner } from '../testing/runners.mjs';
|
|
17
17
|
import { compactOutput } from '../index/compact.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import { coverage } from '../index/coverage.mjs';
|
|
19
|
+
import { readImportEdges } from '../index/gate.mjs';
|
|
19
20
|
import { langOf, packageOf } from '../index/store.mjs';
|
|
20
21
|
import { localRel } from '../state.mjs';
|
|
21
22
|
import { commandRoot } from '../repo-root.mjs';
|
|
23
|
+
import { gitPaths } from '../sys/git.mjs';
|
|
24
|
+
import { cmp } from '../util/sort.mjs';
|
|
22
25
|
|
|
23
26
|
const exec = promisify(execFile);
|
|
24
27
|
|
|
@@ -65,7 +68,7 @@ export async function gateCommand(args, flags) {
|
|
|
65
68
|
const files = new Set(listed.files);
|
|
66
69
|
for (const file of changed) files.add(file);
|
|
67
70
|
const edges = await readImportEdges(root, files);
|
|
68
|
-
const selected =
|
|
71
|
+
const selected = coverage({ imports: edges }, changed);
|
|
69
72
|
// Q40: the selection explains itself. `unmatched` names explicit paths the
|
|
70
73
|
// tracked tree does not carry (untracked but present, so the run proceeds);
|
|
71
74
|
// `graph` is `unresolved` when a language has edges but none of them reaches
|
|
@@ -199,8 +202,7 @@ async function tracked(root) {
|
|
|
199
202
|
try {
|
|
200
203
|
// `-z`: without it git C-quotes a non-ASCII path (`src/\303\274n...`) and
|
|
201
204
|
// the reader's real filename never matches it (review 0003 Q39).
|
|
202
|
-
|
|
203
|
-
return { files: stdout.split('\0').filter(Boolean) };
|
|
205
|
+
return { files: await gitPaths(root, ['ls-files']) };
|
|
204
206
|
} catch (error) {
|
|
205
207
|
return {
|
|
206
208
|
error: fail(
|
|
@@ -214,9 +216,9 @@ async function tracked(root) {
|
|
|
214
216
|
|
|
215
217
|
async function changedFiles(root) {
|
|
216
218
|
try {
|
|
217
|
-
const
|
|
218
|
-
const untracked = await
|
|
219
|
-
return { files: [...new Set([...
|
|
219
|
+
const files = await gitPaths(root, ['diff', '--name-only', 'HEAD'], { maxBuffer: 8 << 20 });
|
|
220
|
+
const untracked = await gitPaths(root, ['ls-files', '--others', '--exclude-standard'], { maxBuffer: 8 << 20 });
|
|
221
|
+
return { files: [...new Set([...files, ...untracked])].sort() };
|
|
220
222
|
} catch (error) {
|
|
221
223
|
return {
|
|
222
224
|
error: fail(
|
|
@@ -228,8 +230,3 @@ async function changedFiles(root) {
|
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
|
|
231
|
-
function cmp(a, b) {
|
|
232
|
-
if (a < b) return -1;
|
|
233
|
-
if (a > b) return 1;
|
|
234
|
-
return 0;
|
|
235
|
-
}
|
package/src/commands/harness.mjs
CHANGED
|
@@ -100,13 +100,13 @@ async function scan(root, { mcp = false } = {}) {
|
|
|
100
100
|
const rows = [];
|
|
101
101
|
for (const s of SURFACES) {
|
|
102
102
|
for (const dir of s.configDirs ?? []) {
|
|
103
|
-
if (await
|
|
103
|
+
if (await isDirectory(root, dir)) {
|
|
104
104
|
rows.push(row(dir, s.label, { id: s.id, verified: s.verified ?? null, readsAgentsMd: s.readsAgentsMd, rule: s.rule }));
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
for (const c of CANDIDATE_DIRS) {
|
|
109
|
-
if (await
|
|
109
|
+
if (await isDirectory(root, c.dir)) {
|
|
110
110
|
rows.push(row(c.dir, c.harness, { readsAgentsMd: c.readsAgentsMd ?? null, readsAgentsMdNote: c.readsAgentsMdNote ?? null, verified: c.verified ?? null }));
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -115,7 +115,7 @@ async function scan(root, { mcp = false } = {}) {
|
|
|
115
115
|
return { root, rows };
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
async function
|
|
118
|
+
async function isDirectory(root, dir) {
|
|
119
119
|
try {
|
|
120
120
|
return (await stat(join(root, dir))).isDirectory();
|
|
121
121
|
} catch {
|
package/src/commands/help.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import { parse, rawJsonFlag } from '../argv.mjs';
|
|
|
4
4
|
import { CLI, MANIFEST_FILE, STATE_DIR } from '../name.mjs';
|
|
5
5
|
import { SESSION_ENV } from '../agenthooks/policy.mjs';
|
|
6
6
|
import { DEFAULT_DIRECTORIES, DEFAULT_TEMPLATE } from '../plan/template.mjs';
|
|
7
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
8
|
+
import { readManifest } from '../projection/manifest.mjs';
|
|
9
|
+
import { assertVersionPin } from '../package-version.mjs';
|
|
7
10
|
|
|
8
11
|
const COMMANDS = `${CLI} init [path] adopt or scaffold a repo into the ecosystem
|
|
9
12
|
(dry-run unless --yes; conflicts block without
|
|
@@ -242,6 +245,7 @@ ${CLI} pack publish [dir] [--yes]
|
|
|
242
245
|
publish an npm-sourced pack to the registry
|
|
243
246
|
(dry-run unless --yes; skips a version already live;
|
|
244
247
|
a git-sourced pack names the tag-and-push instead)
|
|
248
|
+
${CLI} docs readme --yes render the README command list from the spec
|
|
245
249
|
${CLI} mcp the read-only surface as MCP tools over stdio
|
|
246
250
|
(--list prints it without reading stdin; init, sync,
|
|
247
251
|
upgrade and the installers stay CLI-only by design)`;
|
|
@@ -294,6 +298,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
294
298
|
--accept-conflicts init: adopt rules that collide with hand-written ones
|
|
295
299
|
--backup init: tag the pre-adoption commit and branch onto
|
|
296
300
|
simt-dev/adopt before writing (the undo path)
|
|
301
|
+
--no-agents init: skip the agent files and the agent-hooks in one flag;
|
|
302
|
+
the standalone agents / agent-hooks install commands remain
|
|
297
303
|
--spec <name> graph lint: also check every skill against a named
|
|
298
304
|
specification (one exists: agentskills)
|
|
299
305
|
--user harness detect: also report the directories in your home
|
|
@@ -313,7 +319,9 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
313
319
|
--harness <id> bench run: adapter to invoke (claude, codex, gemini, opencode,
|
|
314
320
|
cursor, aider, gold, noop; default: gold);
|
|
315
321
|
agent-hook: which dialect to answer in (claude, cursor,
|
|
316
|
-
codex, antigravity)
|
|
322
|
+
codex, antigravity); agent-hooks / agents install: which
|
|
323
|
+
reader to write (one, not all); init: narrows the agent
|
|
324
|
+
files and the agent-hooks to one reader
|
|
317
325
|
--model <id> bench run: model identifier recorded and passed to the adapter
|
|
318
326
|
--reps <n> bench run: repetitions per task and arm (default: 3)
|
|
319
327
|
--regrade <run> bench run: re-grade a stored run from its archived agent patch
|
|
@@ -321,6 +329,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
321
329
|
--test <cmd> plan done / slice finish: the test command that closes the row
|
|
322
330
|
--state <name> plan done: target state (default: Complete)
|
|
323
331
|
--type <name> slice finish: the Conventional Commits type (default: feat)
|
|
332
|
+
--id <name> slice start: the slice id when -- starts one from a prompt
|
|
333
|
+
(default: a slug of the prompt text)
|
|
324
334
|
--ttl <seconds> lease: how long a claim lasts (default: 3600)
|
|
325
335
|
--level <name> map: detail level (outline, names, signatures)
|
|
326
336
|
--supersedes <spec> decide: superseded ADR or ruling (ADR-NNN or ADR-NNN:<ruling>[:<kind>]; repeatable)
|
|
@@ -385,6 +395,13 @@ export async function help(argv, dispatched) {
|
|
|
385
395
|
emitJson(SPEC.map(specRow));
|
|
386
396
|
return 0;
|
|
387
397
|
}
|
|
398
|
+
// The standard page is where a reader asks "what can this do, and where do I start".
|
|
399
|
+
// A repository that has not been adopted, or that pins a version this binary is not,
|
|
400
|
+
// has a prerequisite worth stating before the catalogue: no manifest means init is the
|
|
401
|
+
// first move, a stale pin means sync is the fix. Fully ready, the page stands on its
|
|
402
|
+
// own and the banner is nothing.
|
|
403
|
+
const banner = await prerequisiteBanner(flags, positional);
|
|
404
|
+
const bannerLines = banner === null ? [] : ['', banner, ''];
|
|
388
405
|
// `--all` prints every command and every flag; the default page lists the core tier and
|
|
389
406
|
// says where the rest is. A reader asking "what can this do" needs the shape, not 23 KB
|
|
390
407
|
// of flag reference (review 0003 Q61).
|
|
@@ -394,7 +411,7 @@ export async function help(argv, dispatched) {
|
|
|
394
411
|
'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
|
|
395
412
|
'',
|
|
396
413
|
`usage: ${CLI} <command> [args] [flags]`,
|
|
397
|
-
|
|
414
|
+
...bannerLines,
|
|
398
415
|
'commands:',
|
|
399
416
|
COMMANDS,
|
|
400
417
|
'',
|
|
@@ -423,7 +440,7 @@ export async function help(argv, dispatched) {
|
|
|
423
440
|
'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
|
|
424
441
|
'',
|
|
425
442
|
`usage: ${CLI} <command> [args] [flags]`,
|
|
426
|
-
|
|
443
|
+
...bannerLines,
|
|
427
444
|
'commands:',
|
|
428
445
|
...[...groups].map(([group, subs]) => ` ${CLI} ${group} <${subs.join('|')}>`),
|
|
429
446
|
...bare.map((row) => ` ${CLI} ${row.path.padEnd(8)} ${row.summary.length > 46 ? `${row.summary.slice(0, 43)}...` : row.summary}`),
|
|
@@ -435,6 +452,30 @@ export async function help(argv, dispatched) {
|
|
|
435
452
|
return 0;
|
|
436
453
|
}
|
|
437
454
|
|
|
455
|
+
/**
|
|
456
|
+
* The prerequisite line for the standard help page, or null when the repository is fully
|
|
457
|
+
* ready. No manifest (or one that cannot be read) means the adoption has not happened and
|
|
458
|
+
* `init` is the first move; a manifest that pins a version the running binary is not means
|
|
459
|
+
* `sync` is the fix. These are the two states in which a reader opening help is about to
|
|
460
|
+
* run a command that would fail in a predictable way, so the page says so before it lists
|
|
461
|
+
* the commands.
|
|
462
|
+
*/
|
|
463
|
+
async function prerequisiteBanner(flags, positional) {
|
|
464
|
+
const root = commandRoot(flags, positional);
|
|
465
|
+
const { manifest, exists, error } = await readManifest(root);
|
|
466
|
+
if (!exists) {
|
|
467
|
+
return `prerequisite no ${MANIFEST_FILE} here; run: ${CLI} init <path>`;
|
|
468
|
+
}
|
|
469
|
+
if (error !== null) {
|
|
470
|
+
return `prerequisite ${MANIFEST_FILE} cannot be read (${error}); run: ${CLI} init <path>`;
|
|
471
|
+
}
|
|
472
|
+
const stale = assertVersionPin(manifest);
|
|
473
|
+
if (stale !== null) {
|
|
474
|
+
return `prerequisite ${stale}; run: ${CLI} sync`;
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
438
479
|
async function version() {
|
|
439
480
|
const { describeVersion } = await import('../package-version.mjs');
|
|
440
481
|
return `${describeVersion()}\n`;
|
package/src/commands/hooks.mjs
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { mkdir, readFile, readdir, realpath, rm, stat } from 'node:fs/promises';
|
|
16
16
|
import { repoContext } from '../context.mjs';
|
|
17
|
-
import { commitJournal, journaledGitConfig, journaledRemoveFile
|
|
17
|
+
import { commitJournal, journaledGitConfig, journaledRemoveFile } from '../journal.mjs';
|
|
18
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
18
19
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
19
20
|
import { invalidateCheckCache } from '../status.mjs';
|
|
20
21
|
import { isAbsolute, join, relative } from 'node:path';
|
|
@@ -23,13 +24,13 @@ import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/i
|
|
|
23
24
|
import { MANIFEST, readManifest } from '../projection/manifest.mjs';
|
|
24
25
|
import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_DIR_LEGACY, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderEvidenceShim, renderShim } from '../hooks/shim.mjs';
|
|
25
26
|
import { GATE_KEY, gateCount, readHooksPath, readHooksState, wantsShim } from '../compare/hooksstate.mjs';
|
|
26
|
-
import {
|
|
27
|
-
import { gateNames } from '../hooks/gates.mjs';
|
|
27
|
+
import { gateNames, getGate } from '../hooks/gates.mjs';
|
|
28
28
|
import { renderGateReport, runGates } from '../hooks/dispatcher.mjs';
|
|
29
29
|
import { CLI } from '../name.mjs';
|
|
30
30
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
31
31
|
import { packageVersion, hasNpm } from '../package-version.mjs';
|
|
32
32
|
import { commandRoot } from '../repo-root.mjs';
|
|
33
|
+
import { readIfExists } from '../util/fs.mjs';
|
|
33
34
|
|
|
34
35
|
export async function hooksCommand(sub, args, flags) {
|
|
35
36
|
const root = commandRoot(flags, args);
|
|
@@ -374,26 +375,31 @@ export async function applyHooksInstall(root, planned, journal) {
|
|
|
374
375
|
const wrote = [];
|
|
375
376
|
const removed = [];
|
|
376
377
|
await mkdir(join(root, HOOKS_DIR), { recursive: true });
|
|
378
|
+
const hookChanges = [];
|
|
377
379
|
for (const event of wanted) {
|
|
378
380
|
const path = join(root, fileFor(event));
|
|
379
|
-
|
|
381
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: renderShim(event, { npm, manager, version }), owner: 'hooks install', reason: event, mode: 0o755 }));
|
|
380
382
|
wrote.push(fileFor(event));
|
|
381
383
|
}
|
|
382
384
|
for (const c of chained) {
|
|
383
385
|
const path = join(root, fileFor(c.event));
|
|
384
|
-
|
|
386
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: renderChainedShim(c.event, c.target), owner: 'hooks install', reason: c.event, mode: 0o755 }));
|
|
385
387
|
wrote.push(fileFor(c.event));
|
|
386
388
|
}
|
|
387
389
|
for (const h of obsolete) {
|
|
388
|
-
|
|
390
|
+
const obsoletePath = join(root, fileFor(h.event));
|
|
391
|
+
hookChanges.push(
|
|
392
|
+
change({ path: obsoletePath, kind: 'remove', before: await readBefore(obsoletePath), after: null, owner: 'hooks install', reason: h.event }),
|
|
393
|
+
);
|
|
389
394
|
wrote.push(fileFor(h.event));
|
|
390
395
|
}
|
|
391
396
|
// After the chain loop, so a chained commit-msg cannot replace the claim check.
|
|
392
397
|
if (evidenceSource) {
|
|
393
398
|
const path = join(root, evidenceFile);
|
|
394
|
-
await
|
|
399
|
+
hookChanges.push(change({ path, before: await readBefore(path), after: evidenceSource, owner: 'hooks install', reason: 'the evidence shim', mode: 0o755 }));
|
|
395
400
|
wrote.push(evidenceFile);
|
|
396
401
|
}
|
|
402
|
+
await applyPlan(root, hookChanges, { mode: { apply: true }, journal, commit: false });
|
|
397
403
|
// `core.hooksPath` tells git to read ONE directory for EVERY event, so leaving it
|
|
398
404
|
// pointing at a directory of no shims stops the repository's own hooks - the ones in
|
|
399
405
|
// `.git/hooks` that predate this tool - from running, silently. Unsetting it is the
|
|
@@ -416,6 +422,7 @@ export async function applyHooksInstall(root, planned, journal) {
|
|
|
416
422
|
await rm(join(root, HOOKS_DIR_LEGACY), { recursive: true });
|
|
417
423
|
}
|
|
418
424
|
return { wrote, removed };
|
|
425
|
+
|
|
419
426
|
}
|
|
420
427
|
|
|
421
428
|
/**
|
|
@@ -749,7 +756,7 @@ function describeGates(manifest, key) {
|
|
|
749
756
|
const list = manifest?.gates?.[key];
|
|
750
757
|
if (!Array.isArray(list) || list.length === 0) return '(none declared)';
|
|
751
758
|
return list.map((name) => {
|
|
752
|
-
const gate =
|
|
759
|
+
const gate = getGate(name, manifest?.gates?.commands);
|
|
753
760
|
return gate ? name : `${name} (UNKNOWN - the hook will fail)`;
|
|
754
761
|
}).join(', ');
|
|
755
762
|
}
|
|
@@ -818,7 +825,7 @@ async function hooksStatus(root, flags, ctx = repoContext(root)) {
|
|
|
818
825
|
lines.push('', 'would run:');
|
|
819
826
|
for (const key of ['preCommit', 'prePush']) {
|
|
820
827
|
for (const name of manifest?.gates?.[key] ?? []) {
|
|
821
|
-
const known =
|
|
828
|
+
const known = getGate(name, commands);
|
|
822
829
|
// A declared command wins for every kind, so the status must say so too —
|
|
823
830
|
// printing "run script" for a gate the dispatcher will shell out to `./gradlew`
|
|
824
831
|
// for is the same one-question-two-answers defect ADR-018 was written about.
|
|
@@ -837,7 +844,7 @@ async function hooksStatus(root, flags, ctx = repoContext(root)) {
|
|
|
837
844
|
if (undeclared.length > 0) {
|
|
838
845
|
lines.push('', `declared but never run: ${undeclared.join(', ')}`);
|
|
839
846
|
}
|
|
840
|
-
const unknown = [...(manifest?.gates?.preCommit ?? []), ...(manifest?.gates?.prePush ?? [])].filter((n) => !
|
|
847
|
+
const unknown = [...(manifest?.gates?.preCommit ?? []), ...(manifest?.gates?.prePush ?? [])].filter((n) => !getGate(n, commands));
|
|
841
848
|
if (unknown.length > 0) {
|
|
842
849
|
lines.push('', `unknown gates (the hook will fail): ${unknown.join(', ')}`);
|
|
843
850
|
lines.push(`known gates: ${gateNames().join(', ')}`);
|
|
@@ -869,8 +876,9 @@ function collectProblems({ exists, current, installed, manifest, foreign = [] })
|
|
|
869
876
|
if (exists) {
|
|
870
877
|
for (const key of ['preCommit', 'prePush']) {
|
|
871
878
|
for (const name of manifest?.gates?.[key] ?? []) {
|
|
872
|
-
|
|
873
|
-
|
|
879
|
+
const gate = getGate(name, manifest?.gates?.commands);
|
|
880
|
+
if (!gate) problems.push(`gate "${name}" in ${key} is not a known gate`);
|
|
881
|
+
else if (gate.kind === 'command' && !manifest?.gates?.commands?.[name]) {
|
|
874
882
|
problems.push(`gate "${name}" in ${key} is a command gate with no declared command; the hook will fail`);
|
|
875
883
|
}
|
|
876
884
|
}
|
|
@@ -878,11 +886,3 @@ function collectProblems({ exists, current, installed, manifest, foreign = [] })
|
|
|
878
886
|
}
|
|
879
887
|
return problems;
|
|
880
888
|
}
|
|
881
|
-
|
|
882
|
-
async function readIfExists(path) {
|
|
883
|
-
try {
|
|
884
|
-
return await readFile(path, 'utf8');
|
|
885
|
-
} catch {
|
|
886
|
-
return null;
|
|
887
|
-
}
|
|
888
|
-
}
|
package/src/commands/ignore.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { join } from 'node:path';
|
|
|
11
11
|
|
|
12
12
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
13
13
|
import { CLI } from '../name.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
15
15
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
16
16
|
import { CLAUDE_SETTINGS, IGNORE_FILE, parseIgnoreList, planIgnore } from '../ignore.mjs';
|
|
17
17
|
import { commandRoot } from '../repo-root.mjs';
|
|
@@ -125,12 +125,16 @@ export async function ignoreCommand(args, flags) {
|
|
|
125
125
|
|
|
126
126
|
// Journaled so `undo` restores this run (R49). Every note writes to stderr,
|
|
127
127
|
// so the `--json` document remains the only bytes on stdout.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
await applyPlan(
|
|
129
|
+
root,
|
|
130
|
+
await Promise.all(
|
|
131
|
+
changed.map(async (t) =>
|
|
132
|
+
change({ path: t.path, before: await readBefore(join(root, t.path)), after: t.after, owner: 'ignore', reason: t.action }),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
{ mode: { apply: true }, label: 'ignore' },
|
|
136
|
+
);
|
|
137
|
+
for (const t of changed) note(`wrote ${t.path} (${t.action})`);
|
|
134
138
|
|
|
135
139
|
if (isJson(flags)) emitJson(payload(true));
|
|
136
140
|
// After the report: the report says what, the diff shows it (R50).
|
package/src/commands/impact.mjs
CHANGED
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
// the file, the CODEOWNERS line, and — when a workspace is configured —
|
|
5
5
|
// another repository that references a contract this change touches.
|
|
6
6
|
|
|
7
|
-
import { execFile } from 'node:child_process';
|
|
8
7
|
import { readFile } from 'node:fs/promises';
|
|
9
8
|
import { basename, join, resolve } from 'node:path';
|
|
10
|
-
import { promisify } from 'node:util';
|
|
11
9
|
|
|
12
10
|
import { EXIT, fail, present } from '../output/index.mjs';
|
|
13
11
|
import { CLI } from '../name.mjs';
|
|
@@ -22,12 +20,12 @@ import {
|
|
|
22
20
|
ownersOf,
|
|
23
21
|
relatedContracts,
|
|
24
22
|
} from '../index/impact.mjs';
|
|
25
|
-
import {
|
|
23
|
+
import { governs, sourceRoots } from '../index/govern.mjs';
|
|
26
24
|
import { decisionRecords, ruleScopes } from './why.mjs';
|
|
27
25
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
28
26
|
import { commandRoot } from '../repo-root.mjs';
|
|
27
|
+
import { gitPaths } from '../sys/git.mjs';
|
|
29
28
|
|
|
30
|
-
const exec = promisify(execFile);
|
|
31
29
|
const TEXT_EXT = new Set(['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'json', 'yaml', 'yml', 'md', 'toml', 'xml', 'py', 'go', 'kt', 'java', 'rs']);
|
|
32
30
|
const READ_CAP = 256 * 1024;
|
|
33
31
|
const FILE_CAP = 2000;
|
|
@@ -88,8 +86,7 @@ async function governance(root, file, files, commands) {
|
|
|
88
86
|
const records = await decisionRecords(root);
|
|
89
87
|
const rules = await ruleScopes(root);
|
|
90
88
|
const ctx = { files, commands: new Set(commands), roots: sourceRoots(files) };
|
|
91
|
-
|
|
92
|
-
return sortHits([...hits, ...scopeHits(file, { rules })]);
|
|
89
|
+
return governs(file, { records, rules, ctx });
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
async function readOwners(root, file) {
|
|
@@ -190,8 +187,7 @@ async function tracked(root) {
|
|
|
190
187
|
try {
|
|
191
188
|
// `-z`: without it git C-quotes a non-ASCII path and the reader's real
|
|
192
189
|
// filename never matches it (review 0003 Q39).
|
|
193
|
-
|
|
194
|
-
return { files: stdout.split('\0').filter(Boolean) };
|
|
190
|
+
return { files: await gitPaths(root, ['ls-files']) };
|
|
195
191
|
} catch (error) {
|
|
196
192
|
return {
|
|
197
193
|
error: fail(
|
package/src/commands/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Command registry and dispatch.
|
|
2
2
|
//
|
|
3
|
-
// Every command named in docs/
|
|
3
|
+
// Every command named in docs/research/0002-simt-dev-cli.md section 5 is registered
|
|
4
4
|
// here from day one. A command that is declared but not yet implemented exits
|
|
5
5
|
// UNSUPPORTED with a pointer to its implementation step; it never silently
|
|
6
6
|
// succeeds. An agent that calls `simt compare` on an unreleased build must get a
|
|
@@ -21,9 +21,10 @@ import {
|
|
|
21
21
|
jsonFailMode,
|
|
22
22
|
jsonIndentDefault,
|
|
23
23
|
maxBytesLimit,
|
|
24
|
+
withOutputState,
|
|
24
25
|
} from '../output/index.mjs';
|
|
25
26
|
import { help } from './help.mjs';
|
|
26
|
-
import { GROUP_DEFAULTS, SPEC, SPEC_PATHS } from './spec.mjs';
|
|
27
|
+
import { ALIASES, GROUP_DEFAULTS, SPEC, SPEC_PATHS } from './spec.mjs';
|
|
27
28
|
import { parse, rawJsonFlag } from '../argv.mjs';
|
|
28
29
|
import { CLI } from '../name.mjs';
|
|
29
30
|
import { packageVersion } from '../package-version.mjs';
|
|
@@ -44,7 +45,10 @@ const DECLARED = new Map(SPEC.map((row) => [row.path, new Set(row.flags ?? [])])
|
|
|
44
45
|
/** path -> the values a flag of that path accepts, where the set is closed. */
|
|
45
46
|
const VALUES = new Map(SPEC.map((row) => [row.path, row.values ?? {}]));
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
// `dashdash` is the parser's record that `--` appeared, not a flag any reader typed:
|
|
49
|
+
// it is an envelope fact like `root`, so the per-command declaration check skips it
|
|
50
|
+
// and lets the one command that reads it (slice start) act on it.
|
|
51
|
+
export const GLOBAL_FLAGS = new Set(['root', 'json', 'pretty', 'help', 'version', 'maxBytes', 'dashdash']);
|
|
48
52
|
|
|
49
53
|
/**
|
|
50
54
|
* Every command this binary dispatches, in one place.
|
|
@@ -149,6 +153,12 @@ export const IMPLEMENTED = {
|
|
|
149
153
|
// Plan 0003 R54. A bare command: paths claim, no paths list, so the argument itself
|
|
150
154
|
// is the mode and there is no group to resolve.
|
|
151
155
|
lease: async () => (await import('./lease.mjs')).leaseCommand,
|
|
156
|
+
// Review 0003 Q60. The README's command list is the last copy of the command table
|
|
157
|
+
// the spec owns; the renderer lives in src/docs so a test can hold the file to it.
|
|
158
|
+
'docs readme': async () => {
|
|
159
|
+
const { docsCommand } = await import('./docs.mjs');
|
|
160
|
+
return (args, flags) => docsCommand('readme', args, flags);
|
|
161
|
+
},
|
|
152
162
|
distill: async () => (await import('./distill.mjs')).distillCommand,
|
|
153
163
|
// R25 machinery needs the runnable command names for Affects command tokens,
|
|
154
164
|
// same wiring `why` and `impact` get through their lambdas.
|
|
@@ -245,8 +255,25 @@ export const IMPLEMENTED = {
|
|
|
245
255
|
},
|
|
246
256
|
};
|
|
247
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Resolve one command path to its handler, its declared flags and its closed value sets.
|
|
260
|
+
*
|
|
261
|
+
* The library surface needs the same answer `dispatch` gets and must not build a second
|
|
262
|
+
* registry to get it: a command reachable from the CLI and absent from `run` is the drift
|
|
263
|
+
* `test/spec.test.mjs` exists to catch, and it would be invisible here (review 0003 Q63).
|
|
264
|
+
*/
|
|
265
|
+
export async function resolveCommand(path) {
|
|
266
|
+
const load = IMPLEMENTED[path];
|
|
267
|
+
if (!load) return null;
|
|
268
|
+
return {
|
|
269
|
+
run: await load(),
|
|
270
|
+
declared: DECLARED.get(path) ?? new Set(),
|
|
271
|
+
values: VALUES.get(path) ?? {},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
248
275
|
/** The release that turns the tolerated flag into a usage error (review 0003 Q62). */
|
|
249
|
-
export const DEPRECATION_RELEASE = '0.
|
|
276
|
+
export const DEPRECATION_RELEASE = '0.9.0';
|
|
250
277
|
|
|
251
278
|
/**
|
|
252
279
|
* Whether a tolerance that ends at `removedIn` has ended, comparing the way versions
|
|
@@ -275,7 +302,18 @@ export function refusesAt(removedIn, version) {
|
|
|
275
302
|
*/
|
|
276
303
|
export const COMMANDS = SPEC_PATHS;
|
|
277
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Dispatch one argv.
|
|
307
|
+
*
|
|
308
|
+
* The state is per call: an MCP server and the tests both dispatch more than once in a
|
|
309
|
+
* process, and a shared envelope let one run's ceiling, notices and command path land in
|
|
310
|
+
* another run's document (review 0003 Q63/Q65).
|
|
311
|
+
*/
|
|
278
312
|
export async function dispatch(argv) {
|
|
313
|
+
return withOutputState(() => dispatchInState(argv));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function dispatchInState(argv) {
|
|
279
317
|
const { flags, positional, parseError } = parseOrExit(argv);
|
|
280
318
|
// One run, one set of notices: a process can dispatch repeatedly (review 0003 Q62).
|
|
281
319
|
clearWarnings();
|
|
@@ -383,10 +421,14 @@ export async function dispatch(argv) {
|
|
|
383
421
|
* Normalise argv into a registry key, or null when unrecognised.
|
|
384
422
|
* A group with an omitted subcommand resolves to that group's default, so
|
|
385
423
|
* `simt skills` is `simt skills index`.
|
|
424
|
+
*
|
|
425
|
+
* An alias resolves here rather than as a second registry entry: the spec path stays the
|
|
426
|
+
* one dispatch key, so an alias cannot load a different handler or escape the flag
|
|
427
|
+
* declaration the path carries (review 0003 Q81).
|
|
386
428
|
*/
|
|
387
429
|
function commandPath(first, second) {
|
|
388
430
|
if (first === undefined) return null;
|
|
389
|
-
if (!GROUPS.has(first)) return first;
|
|
431
|
+
if (!GROUPS.has(first)) return ALIASES.get(first) ?? first;
|
|
390
432
|
|
|
391
433
|
const group = GROUPS.get(first);
|
|
392
434
|
if (second === undefined) return `${first} ${group}`;
|
|
@@ -400,10 +442,8 @@ function commandPath(first, second) {
|
|
|
400
442
|
// `doctor` is not a group and this branch is only reached when `GROUPS.has(first)`.
|
|
401
443
|
// Two lists of the same fact is the defect S15 was about in the help text.
|
|
402
444
|
const key = `${first} ${second}`;
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
return key;
|
|
445
|
+
if (key in IMPLEMENTED) return key;
|
|
446
|
+
return ALIASES.get(key) ?? null;
|
|
407
447
|
}
|
|
408
448
|
|
|
409
449
|
function parseOrExit(argv) {
|