simt-dev 0.4.0 → 0.6.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +170 -0
  2. package/README.md +300 -82
  3. package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +13 -0
  4. package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +1 -1
  5. package/docs/decisions/ADR-097-the-channels-publish-by-hand-and-the-version-rides-in-as-an-input.md +71 -0
  6. package/docs/decisions/ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md +149 -0
  7. package/package.json +8 -4
  8. package/src/advice/advice.mjs +229 -0
  9. package/src/agenthooks/render.mjs +87 -21
  10. package/src/argv.mjs +25 -0
  11. package/src/bench/ablate.mjs +1 -3
  12. package/src/bench/grade.mjs +1 -1
  13. package/src/bench/mine.mjs +3 -3
  14. package/src/bench/probes.mjs +1 -3
  15. package/src/bench/report.mjs +1 -3
  16. package/src/bench/run.mjs +4 -18
  17. package/src/bench/tasks.mjs +36 -0
  18. package/src/bench/variants.mjs +1 -3
  19. package/src/brief/assemble.mjs +24 -4
  20. package/src/brief/packet.mjs +4 -1
  21. package/src/ci/dependabot.mjs +1 -1
  22. package/src/ci/workflow.mjs +40 -12
  23. package/src/commands/agenthook.mjs +6 -2
  24. package/src/commands/bench.mjs +35 -3
  25. package/src/commands/ci.mjs +1 -1
  26. package/src/commands/compare.mjs +1 -1
  27. package/src/commands/distill.mjs +36 -0
  28. package/src/commands/gate.mjs +2 -2
  29. package/src/commands/graph.mjs +2 -2
  30. package/src/commands/harness.mjs +87 -14
  31. package/src/commands/help.mjs +54 -4
  32. package/src/commands/hooks.mjs +1 -1
  33. package/src/commands/index.mjs +29 -3
  34. package/src/commands/init.mjs +2 -2
  35. package/src/commands/local.mjs +11 -0
  36. package/src/commands/mcp.mjs +1 -1
  37. package/src/commands/pack.mjs +246 -0
  38. package/src/commands/plugin.mjs +61 -0
  39. package/src/commands/review.mjs +65 -0
  40. package/src/commands/rules.mjs +1 -1
  41. package/src/commands/skills.mjs +3 -34
  42. package/src/commands/skillscompile.mjs +1 -3
  43. package/src/commands/skillsinstall.mjs +1 -1
  44. package/src/commands/sync.mjs +8 -8
  45. package/src/commands/tokens.mjs +1 -1
  46. package/src/commands/upgrade.mjs +1 -1
  47. package/src/commands/why.mjs +6 -3
  48. package/src/compare/discover.mjs +1 -1
  49. package/src/compare/hooksstate.mjs +19 -7
  50. package/src/compare/learn.mjs +2 -2
  51. package/src/compare/row.mjs +3 -3
  52. package/src/decisions/index.mjs +1 -1
  53. package/src/distill/distill.mjs +246 -0
  54. package/src/distill/engine.mjs +187 -0
  55. package/src/distribute/release.mjs +142 -0
  56. package/src/graph/load.mjs +9 -8
  57. package/src/hooks/dispatcher.mjs +3 -2
  58. package/src/hooks/shim.mjs +11 -6
  59. package/src/index/govern.mjs +1 -1
  60. package/src/index/map.mjs +1 -1
  61. package/src/index/persist.mjs +2 -2
  62. package/src/index/store.mjs +50 -20
  63. package/src/ledger/index.mjs +2 -2
  64. package/src/local/client.mjs +22 -8
  65. package/src/local/config.mjs +52 -0
  66. package/src/locate.mjs +22 -1
  67. package/src/mcp/prompts.mjs +118 -0
  68. package/src/mcp/resources.mjs +96 -22
  69. package/src/mcp/server.mjs +33 -6
  70. package/src/mcp/tools.mjs +12 -12
  71. package/src/measure/session.mjs +1 -1
  72. package/src/memory/decide.mjs +1 -1
  73. package/src/memory/lessons.mjs +14 -14
  74. package/src/name.mjs +11 -1
  75. package/src/package-version.mjs +12 -10
  76. package/src/packs/manage.mjs +563 -0
  77. package/src/packs.mjs +15 -5
  78. package/src/plugins.mjs +164 -0
  79. package/src/projection/loaders.mjs +84 -10
  80. package/src/projection/manifest.mjs +4 -4
  81. package/src/projection/project.mjs +3 -3
  82. package/src/projection/rules.mjs +24 -4
  83. package/src/projection/surfaces.mjs +20 -2
  84. package/src/resolve/body.mjs +46 -0
  85. package/src/review/critic.mjs +509 -0
  86. package/src/toolwords.mjs +8 -5
  87. package/src/tsconfig.mjs +3 -3
  88. package/src/upgrade/plan.mjs +1 -1
  89. /package/bin/{simt-dev.mjs → simt.mjs} +0 -0
@@ -2,7 +2,7 @@
2
2
  // and statistical report (`bench report`, R03).
3
3
  //
4
4
  // Like every writer in this CLI, `bench mine` and `bench run` are previews by
5
- // default and write artifacts to `.simt-dev/bench/` only when authorised with
5
+ // default and write artifacts to `.simt/bench/` only when authorised with
6
6
  // `--yes` (and not `--dry-run`). `bench report` is read-only.
7
7
 
8
8
  import { resolve } from 'node:path';
@@ -16,6 +16,7 @@ import { formatRecall, measureRecall } from '../index/map.mjs';
16
16
  import { ablateBench } from '../bench/ablate.mjs';
17
17
  import { runBenchProbes } from '../bench/probes.mjs';
18
18
  import { selectRuleVariants } from '../bench/variants.mjs';
19
+ import { criticBench } from '../review/critic.mjs';
19
20
  import { listFlag } from './skills.mjs';
20
21
 
21
22
  export async function benchMineCommand(positional, flags) {
@@ -90,7 +91,7 @@ export async function benchRunCommand(positional, flags) {
90
91
  return fail(
91
92
  `${root}: ${error.message ?? error}`,
92
93
  EXIT.CONFIG,
93
- `pass a stored run name under .simt-dev/bench/runs/: ${CLI} bench run --regrade <run>`,
94
+ `pass a stored run name under .simt/bench/runs/: ${CLI} bench run --regrade <run>`,
94
95
  );
95
96
  }
96
97
 
@@ -221,7 +222,7 @@ export async function benchReportCommand(positional, flags) {
221
222
  return fail(
222
223
  `${root}: ${error.message ?? error}`,
223
224
  EXIT.CONFIG,
224
- `run benchmark arms first with \`${CLI} bench run --yes\` or fix .simt-dev/bench/prices.json`,
225
+ `run benchmark arms first with \`${CLI} bench run --yes\` or fix .simt/bench/prices.json`,
225
226
  );
226
227
  }
227
228
 
@@ -394,3 +395,34 @@ export async function benchVariantsCommand(positional, flags) {
394
395
 
395
396
 
396
397
 
398
+
399
+ // `simt bench critic` (plan 0003 R39).
400
+ //
401
+ // The row's acceptance measured, or reported missing rather than inferred: the
402
+ // critic runs over a labelled set of historical diffs and the bench reports
403
+ // precision and recall for the BLOCK binary. The metric never gates the critic —
404
+ // the row says precision >= 0.8 on >= 30 labelled diffs promotes it to a gate,
405
+ // and until a bench here shows that, every report calls the critic advisory.
406
+ export async function benchCriticCommand(positional, flags, commands = []) {
407
+ const root = resolve(flags.root ?? positional[0] ?? process.cwd());
408
+ const labelsPath = flags.labels ?? '.simt/critic-labels.jsonl';
409
+ const result = await criticBench(root, { labelsPath, flags, commands });
410
+ if (typeof result === 'number') return result;
411
+
412
+ if (isJson(flags)) {
413
+ emitJson(result, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
414
+ return EXIT.OK;
415
+ }
416
+ const lines = [
417
+ `bench critic ${root}`,
418
+ `labels ${result.labels} (${labelsPath}) measured ${result.measured} errors ${result.errors.length}`,
419
+ `counts tp=${result.tp} fp=${result.fp} fn=${result.fn} tn=${result.tn}`,
420
+ `precision ${result.precision === null ? 'unmeasured' : result.precision.toFixed(3)}`,
421
+ `recall ${result.recall === null ? 'unmeasured' : result.recall.toFixed(3)}`,
422
+ `gated ${result.gated ? 'yes' : 'no'}`,
423
+ ];
424
+ if (result.labelsNote) lines.push(`labels ${result.labelsNote}`);
425
+ for (const error of result.errors) lines.push(`error ${error}`);
426
+ emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
427
+ return EXIT.OK;
428
+ }
@@ -1,4 +1,4 @@
1
- // simt-dev ci — the .github files, in the one repository whose copies are refreshed.
1
+ // simt ci — the .github files, in the one repository whose copies are refreshed.
2
2
  //
3
3
  // Why this exists rather than a flag on `init`: the write-once rule is about a
4
4
  // repository's own file, and simt-dev's own repository has no file of its own here —
@@ -1,4 +1,4 @@
1
- // simt-dev compare - the org-wide matrix.
1
+ // simt compare - the org-wide matrix.
2
2
  //
3
3
  // This command exists because the 2026-10-05 audit had to be reconstructed by
4
4
  // hand, from 16 repositories, into 40 KB of prose. Everything it found is a fact
@@ -0,0 +1,36 @@
1
+ // `simt distill <skill|doc>` (plan 0003 R38).
2
+ //
3
+ // A budgeted summary with citations to source line ranges, written to
4
+ // `.simt/distilled/` as a proposal a human merges. The deterministic engine
5
+ // is the default; a local model proposes only when the caller asks for it and the
6
+ // manifest names one (ADR-090, ADR-075). Model output never lands anywhere
7
+ // projected or committed text does — that is the direction this command cannot
8
+ // take (principle P2).
9
+
10
+ import { CLI } from '../name.mjs';
11
+ import { EXIT, emit, emitJson, isJson } from '../output/index.mjs';
12
+ import { distillTarget } from '../distill/distill.mjs';
13
+
14
+ export async function distillCommand(args, flags) {
15
+ const result = await distillTarget(args, flags);
16
+ if (typeof result === 'number') return result;
17
+
18
+ if (isJson(flags)) {
19
+ emitJson(result, { maxBytes: flags.maxBytes ?? 0 });
20
+ return EXIT.OK;
21
+ }
22
+
23
+ emit(
24
+ [
25
+ `source ${result.target.kind} ${result.target.source} (${result.target.bytes} bytes, ${result.target.lines} lines)`,
26
+ `digest ${result.digest}`,
27
+ `engine ${result.engine}`,
28
+ `budget ${result.budget} bytes`,
29
+ `citations ${result.citations}`,
30
+ ...(result.verified === true ? [] : result.faults.map((f) => `unverified ${f}`)),
31
+ `proposal ${result.proposal.path} (${result.proposal.state}, ${result.proposal.bytes} bytes)`,
32
+ ].join('\n'),
33
+ { maxBytes: flags.maxBytes ?? 0 },
34
+ );
35
+ return EXIT.OK;
36
+ }
@@ -117,11 +117,11 @@ async function runSelected(root, flags, selected) {
117
117
 
118
118
  async function writeRaw(root, body) {
119
119
  try {
120
- const dir = join(root, '.simt-dev', 'gate');
120
+ const dir = join(root, '.simt', 'gate');
121
121
  await mkdir(dir, { recursive: true });
122
122
  const name = `gate-${Date.now()}.log`;
123
123
  await writeFile(join(dir, name), body);
124
- return `.simt-dev/gate/${name}`;
124
+ return `.simt/gate/${name}`;
125
125
  } catch {
126
126
  return '';
127
127
  }
@@ -1,4 +1,4 @@
1
- // `simt-dev graph lint` - verify the hand-authored registry against reality.
1
+ // `simt graph lint` - verify the hand-authored registry against reality.
2
2
  //
3
3
  // Implementation sequence step 2. This command is the reason hand-authoring the
4
4
  // registry is safe: 38 nodes across 6 repos is bookkeeping that rots silently.
@@ -64,7 +64,7 @@ export async function lintCommand(positional, flags) {
64
64
  }
65
65
 
66
66
  if (!registry) {
67
- // The default remedy for this exit code is `run: simt-dev graph lint`, which from
67
+ // The default remedy for this exit code is `run: simt graph lint`, which from
68
68
  // inside `graph lint` is circular: the reader just ran it and got this. Name the
69
69
  // argument the command takes, because an explicit path is the way out (ADR-061).
70
70
  return fail(errors[0] ?? `${path}: unreadable`, EXIT.REGISTRY, `run: ${CLI} graph lint <path-to-registry>`);
@@ -13,6 +13,11 @@
13
13
  // a defect in the repository: the rules in AGENTS.md still reach whichever harnesses
14
14
  // read that file, and a reader who wanted a failing command could not tell the
15
15
  // difference between "no channel" and "broken".
16
+ //
17
+ // R42 added the MCP registration files to the same answer. They are reported from the
18
+ // loader table (one source of paths, with `verified` naming the vendor page) because a
19
+ // detection question deserves the same provenance rule the loader table set: a row with
20
+ // no source is a guess in a report.
16
21
 
17
22
  import { stat } from 'node:fs/promises';
18
23
  import { homedir } from 'node:os';
@@ -24,7 +29,7 @@ import { LOADERS, LOADER_IDS, caps, loader } from '../projection/loaders.mjs';
24
29
  import { viewAll } from '../measure/loaderView.mjs';
25
30
 
26
31
  /**
27
- * `simt-dev harness detect`. The subcommand is validated by dispatch, which rejects a
32
+ * `simt harness detect`. The subcommand is validated by dispatch, which rejects a
28
33
  * second token that is not in the group registry rather than silently routing to the
29
34
  * default; this function therefore has exactly one thing to do and no second spelling
30
35
  * to keep in step with it.
@@ -36,7 +41,12 @@ import { viewAll } from '../measure/loaderView.mjs';
36
41
  export async function harnessCommand(positional, flags) {
37
42
  const root = flags.root ?? process.cwd();
38
43
 
39
- const repo = await scan(root);
44
+ // MCP evidence is project-scope by construction: the loader table carries repo-root
45
+ // paths (.mcp.json, .codex/config.toml, …), and the same path means different scopes
46
+ // per harness in $HOME - Codex registers user servers in its user config, Claude Code
47
+ // does not keep .mcp.json there - so a home scan reports directories only, and stays
48
+ // right by saying less rather than by carrying a second per-harness user-scope table.
49
+ const repo = await scan(root, { mcp: true });
40
50
  // Only on request. The home directory is the reader's, not the repository's, and a
41
51
  // command that reports both by default is a command whose output changes because
42
52
  // somebody installed something elsewhere.
@@ -79,8 +89,13 @@ export async function harnessCommand(positional, flags) {
79
89
  * rather than raised: a permissions error on one candidate must not make the whole
80
90
  * command fail, and the difference between "could not look" and "not there" is one
81
91
  * note in the output rather than a second failure mode nobody asked for.
92
+ *
93
+ * `{ mcp: true }` (repo scans only) adds the MCP registration files the loader table
94
+ * records - single-sourced from `loaders.mjs` because that is the whole point of R42:
95
+ * the evidence is data the tool already verified once, not a second list of paths to
96
+ * keep in step by hand. It is file stat and nothing else: present, labelled, cited.
82
97
  */
83
- async function scan(root) {
98
+ async function scan(root, { mcp = false } = {}) {
84
99
  const rows = [];
85
100
  for (const s of SURFACES) {
86
101
  for (const dir of s.configDirs ?? []) {
@@ -91,9 +106,10 @@ async function scan(root) {
91
106
  }
92
107
  for (const c of CANDIDATE_DIRS) {
93
108
  if (await present(root, c.dir)) {
94
- rows.push(row(c.dir, c.harness, { readsAgentsMd: c.readsAgentsMd ?? null, verified: c.verified ?? null }));
109
+ rows.push(row(c.dir, c.harness, { readsAgentsMd: c.readsAgentsMd ?? null, readsAgentsMdNote: c.readsAgentsMdNote ?? null, verified: c.verified ?? null }));
95
110
  }
96
111
  }
112
+ if (mcp) rows.push(...(await scanMcpFiles(root)));
97
113
  rows.sort((a, b) => (a.dir < b.dir ? -1 : a.dir > b.dir ? 1 : 0));
98
114
  return { root, rows };
99
115
  }
@@ -106,11 +122,49 @@ async function present(root, dir) {
106
122
  }
107
123
  }
108
124
 
125
+ /**
126
+ * The MCP registration files the loader table says a project can carry.
127
+ *
128
+ * A row requires the path to be a FILE the loader table names, and it is skipped when
129
+ * the loader carries none (opencode registers servers in a profile key, DeepSeek as a
130
+ * profile plugin). Nothing reads the file: whether the JSON parses or names servers is
131
+ * the harness's problem at session time, and a service that promised to validate that
132
+ * would be a service somebody had to keep current.
133
+ *
134
+ * This is evidence, not a channel: no rule file is projected into an MCP config, so
135
+ * `channel` stays false and `readsAgentsMd` stays null - the fields say what they mean
136
+ * instead of borrowing the meaning the directory rows give them.
137
+ */
138
+ async function scanMcpFiles(root) {
139
+ const rows = [];
140
+ for (const loader of LOADERS) {
141
+ const mcp = loader.mcp;
142
+ if (mcp?.path == null) continue;
143
+ const file = join(root, mcp.path);
144
+ try {
145
+ if (!(await stat(file)).isFile()) continue;
146
+ } catch {
147
+ continue;
148
+ }
149
+ rows.push({
150
+ dir: mcp.path,
151
+ kind: 'mcp',
152
+ label: loader.label,
153
+ surface: loader.id,
154
+ channel: false,
155
+ readsAgentsMd: null,
156
+ verified: mcp.verified ?? loader.verified ?? null,
157
+ });
158
+ }
159
+ return rows;
160
+ }
161
+
109
162
  /** One present directory, and what - if anything - this tool can project into it. */
110
163
  function row(dir, label, surface) {
111
164
  const id = surface.id ?? null;
112
165
  return {
113
166
  dir,
167
+ kind: 'rules',
114
168
  label,
115
169
  surface: id,
116
170
  // `channel: false` is the honest half of the answer: the directory exists and this
@@ -119,14 +173,20 @@ function row(dir, label, surface) {
119
173
  // identical in a list of names.
120
174
  channel: id !== null && surface.rule != null,
121
175
  readsAgentsMd: surface.readsAgentsMd ?? null,
176
+ // Which reader ("some") or which configuration ("some, under this setting") - the
177
+ // bare three-valued column started life on rows with many readers, where "which" is
178
+ // a per-row note. A row that lacks one renders without it.
179
+ readsAgentsMdNote: surface.readsAgentsMdNote ?? null,
122
180
  verified: surface.verified ?? null,
123
181
  };
124
182
  }
125
183
 
126
184
  function countUnclaimed(scan) {
127
185
  // A candidate that cites a document and reads AGENTS.md is covered by the standard
128
- // region. One that cites nothing is the case the note exists for (ADR-067).
129
- return scan.rows.filter((r) => r.surface === null && r.readsAgentsMd == null).length;
186
+ // region. One that cites nothing is the case the note exists for (ADR-067). An MCP
187
+ // config row is not "unclaimed" - it never claimed a rule channel, so it must not
188
+ // count toward the note, or mcp.json presence would read as a rule gap it is not.
189
+ return scan.rows.filter((r) => r.kind === 'rules' && r.surface === null && r.readsAgentsMd == null).length;
130
190
  }
131
191
 
132
192
  /**
@@ -149,15 +209,28 @@ function render(root, repo, user) {
149
209
  function block(scan, pad) {
150
210
  if (scan.rows.length === 0) return [`${pad}(none present)`];
151
211
  return scan.rows.map((r) => {
212
+ // An MCP row is judged by presence alone, so it is named first: routed last, its
213
+ // non-null `surface` (the loader it belongs to) would collide with the directory
214
+ // rows' meaning and print "AGENTS.md" under a file that carries no rules at all.
152
215
  const through =
153
- r.surface === null
154
- ? r.readsAgentsMd === 'all'
155
- ? 'AGENTS.md'
156
- : 'no rule channel in this release'
157
- : r.channel
158
- ? 'projected rule file'
159
- : 'AGENTS.md';
160
- const agents = r.readsAgentsMd === 'all' ? ' (reads AGENTS.md)' : r.readsAgentsMd === 'some' ? ' (some readers read AGENTS.md)' : '';
216
+ r.kind === 'mcp'
217
+ ? 'MCP config present'
218
+ : r.surface === null
219
+ ? r.readsAgentsMd === 'all'
220
+ ? 'AGENTS.md'
221
+ : 'no rule channel in this release'
222
+ : r.channel
223
+ ? 'projected rule file'
224
+ : 'AGENTS.md';
225
+ // "some" never renders bare when the row knows which: a reader who needs the
226
+ // detail already had it only in the JSON, and the detail is the answer to "is my
227
+ // AGENTS.md read" - the question this text is here to answer.
228
+ const agents =
229
+ r.readsAgentsMd === 'all'
230
+ ? ' (reads AGENTS.md)'
231
+ : r.readsAgentsMd === 'some'
232
+ ? ` (some readers read AGENTS.md${r.readsAgentsMdNote ? `: ${r.readsAgentsMdNote}` : ''})`
233
+ : '';
161
234
  return `${pad}${r.dir.padEnd(12)} ${r.label.padEnd(38)} ${through}${agents}`;
162
235
  });
163
236
  }
@@ -57,6 +57,10 @@ ${CLI} agent-hooks install write this repo's policies into the harnesses tha
57
57
  (dry-run unless --yes; merges into the file, keeps every
58
58
  key it does not own; Codex needs the hook trusted)
59
59
  ${CLI} agent-hooks status what each harness will run, and what is not trusted yet
60
+ ${CLI} plugin <claude|gemini|cursor> generate a harness package from the same tables
61
+ (dry-run unless --yes; writes a self-contained runtime, the
62
+ hooks and MCP registration, and for cursor the kernel rules;
63
+ the skill registry is workspace data and is not shipped)
60
64
  ${CLI} plan status count tracker rows
61
65
  (${MANIFEST_FILE} key plans when set, otherwise
62
66
  the ${DEFAULT_TEMPLATE} template:
@@ -92,7 +96,7 @@ ${CLI} map [--task "<text>"] [--level outline|names|signatures]
92
96
  ${CLI} evidence the runs recorded against the tree on disk
93
97
  ${CLI} evidence check <file> a commit message that claims a run this tree does not
94
98
  have: a warning, or a block when gates.evidence is "block"
95
- ${CLI} lessons list deterministic lesson candidates in .simt-dev/lessons.jsonl
99
+ ${CLI} lessons list deterministic lesson candidates in .simt/lessons.jsonl
96
100
  and promoted lessons in docs/lessons/*.md
97
101
  ${CLI} lessons review [<id>] promote a lesson candidate to docs/lessons/NNNN-<slug>.md
98
102
  (dry-run unless --yes; writes Appendix A frontmatter)
@@ -103,6 +107,21 @@ ${CLI} decide [<title>] scaffold an ADR with the next free number, a Stat
103
107
  ${CLI} local doctor probe configured local model endpoints (chat and embed)
104
108
  (reads local in ${MANIFEST_FILE}; opens no socket when
105
109
  unset; refuses non-loopback URLs without --allow-remote)
110
+ ${CLI} distill <skill|doc> a budgeted proposal summary of a skill body or doc,
111
+ every claim citing the source line range it rests on
112
+ (writes .simt/distilled/<digest>.md; a proposal a
113
+ human merges - never projected or committed;
114
+ deterministic outline by default; --local proposes
115
+ from the configured chat model instead; the cache is
116
+ keyed by source digest and budget, --refresh regenerates)
117
+ ${CLI} review [--local] an ADR-008 verdict skeleton for the staged diff,
118
+ the brief listing everything that governs it (R25)
119
+ (deterministic without --local: the brief and its
120
+ governing ids only; --local proposes from the
121
+ configured chat model and writes
122
+ .simt/reviews/<digest>.md — a proposal a human
123
+ reads, verified against the verdict vocabulary and
124
+ the brief's ids; advisory always, never a gate)
106
125
  ${CLI} shared plan what a workspace's repeated rules cost to carry
107
126
  once: the shared set, its bytes, and every path that would be written
108
127
  (read-only; touches nothing, not even your home directory)
@@ -117,8 +136,9 @@ ${CLI} tokens [--repos] what a reader loads at session start, in bytes
117
136
  repository under the root and what repeats an earlier)
118
137
  ${CLI} harness detect which harnesses this repo (and, with --user, you)
119
138
  are set up for, and which have a rule channel here
120
- (presence only: a directory found is not a claim about
121
- what its owner reads; exits 0 whatever it finds)
139
+ (presence only: a directory or an MCP config file
140
+ found is not a claim about what its owner reads;
141
+ exits 0 whatever it finds)
122
142
  ${CLI} harness loaders what each reader loads at session start, in bytes,
123
143
  and what its size cap drops
124
144
  (--table prints the vendor facts without measuring;
@@ -141,6 +161,23 @@ ${CLI} bench probes [--yes] 12 synthetic rule-compliance probes with determin
141
161
  ${CLI} bench variants [--yes] evaluate simt-variants rule slots on the canary suite and lock
142
162
  winning variants in simt-dev.lock.json (dry-run unless --yes;
143
163
  re-opens when model or harness changes)
164
+ ${CLI} bench critic precision and recall of the local critic over a labelled
165
+ set of historical diffs (.simt/critic-labels.jsonl;
166
+ overrides --labels; labels are human-written verdicts
167
+ under ADR-008; the critic is advisory until precision
168
+ reaches 0.8 on 30 labelled diffs)
169
+ ${CLI} pack new <name> --source <spec>
170
+ scaffold a versioned pack: pack.json, an unmeasured
171
+ bench.json, and one stack rule (dry-run unless --yes;
172
+ refuses a directory that already exists)
173
+ ${CLI} pack lint [dir] validate a pack: identity, source, rules, and the
174
+ bench summary (--require-measured rejects a pack with
175
+ no measured ΔR; a pack vendored under .simt/packs/ is
176
+ also checked against the manifest's pin)
177
+ ${CLI} pack publish [dir] [--yes]
178
+ publish an npm-sourced pack to the registry
179
+ (dry-run unless --yes; skips a version already live;
180
+ a git-sourced pack names the tag-and-push instead)
144
181
  ${CLI} mcp the read-only surface as MCP tools over stdio
145
182
  (--list prints it without reading stdin; init, sync,
146
183
  upgrade and the installers stay CLI-only by design)`;
@@ -155,6 +192,7 @@ const FLAGS = `--json structured output (compact; one document,
155
192
  --task "<text>" the task text used for capability resolution
156
193
  --paths <globs> comma-separated path globs used for path resolution
157
194
  --root <path> repository root (default: cwd)
195
+ --out <path> plugin: where the generated package lands (required)
158
196
  --surfaces <ids> comma-separated editors to write rule files for;
159
197
  "" means AGENTS.md only (default: every surface,
160
198
  or the "surfaces" list in simt-dev.json)
@@ -204,9 +242,21 @@ const FLAGS = `--json structured output (compact; one document,
204
242
  --state <name> plan done: target state (default: Complete)
205
243
  --level <name> map: detail level (outline, names, signatures)
206
244
  --supersedes <spec> decide: superseded ADR or ruling (ADR-NNN or ADR-NNN:<ruling>[:<kind>]; repeatable)
207
- --allow-remote local doctor: allow non-loopback model URLs (refused by default)
245
+ --source <spec> pack new: where the pack's bytes come from
246
+ (npm:<package> or git+https://…; required)
247
+ --require-measured pack lint: reject a pack with no measured ΔR
248
+ (a bench.json marked unmeasured, or missing)
249
+ --allow-remote local doctor / distill / review: allow non-loopback model
250
+ URLs (refused by default)
208
251
  --embed index / map / brief: compute or query optional local vector embeddings
209
252
  (off by default unless recall@10 gains at least 10 points)
253
+ --labels <path> bench critic: the labelled set of historical diffs to score
254
+ (default .simt/critic-labels.jsonl; one human-written
255
+ {sha, verdict} pair per line)
256
+ --refresh distill / review: regenerate a proposal that is already cached
257
+ --local distill / review: propose the answer from the configured
258
+ local chat model (local.chat); off by default, which
259
+ uses the deterministic path instead
210
260
  --help this text
211
261
  --version the version, and the SHA it was built from`;
212
262
 
@@ -1,4 +1,4 @@
1
- // simt-dev hooks install | status | run
1
+ // simt hooks install | status | run
2
2
  //
3
3
  // Three commands with one shared body. `install` writes the shims and points
4
4
  // core.hooksPath at them; `status` reports what is installed and what would
@@ -3,7 +3,7 @@
3
3
  // Every command named in docs/plans/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
- // succeeds. An agent that calls `simt-dev compare` on an unreleased build must get a
6
+ // succeeds. An agent that calls `simt compare` on an unreleased build must get a
7
7
  // failure, not an empty result it will read as "no drift".
8
8
 
9
9
  import {
@@ -34,6 +34,8 @@ import { agentHookCommand, agentHooksCommand } from './agenthook.mjs';
34
34
  import { compareCommand } from './compare.mjs';
35
35
  import { upgradeCommand } from './upgrade.mjs';
36
36
  import { mcpCommand } from './mcp.mjs';
37
+ import { pluginCommand } from './plugin.mjs';
38
+ import { packCommand } from './pack.mjs';
37
39
  import { installCommand } from './skillsinstall.mjs';
38
40
  import { compileCommand } from './skillscompile.mjs';
39
41
  import { listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
@@ -46,6 +48,7 @@ import {
46
48
  benchAblateCommand,
47
49
  benchProbesCommand,
48
50
  benchVariantsCommand,
51
+ benchCriticCommand,
49
52
  } from './bench.mjs';
50
53
  import { planCommand } from './plan.mjs';
51
54
  import { sharedPlanCommand, sharedInstallCommand, sharedWarning } from './shared.mjs';
@@ -58,7 +61,9 @@ import { gateCommand } from './gate.mjs';
58
61
  import { impactCommand } from './impact.mjs';
59
62
  import { lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
60
63
  import { decideCommand } from './decide.mjs';
64
+ import { reviewCommand } from './review.mjs';
61
65
  import { localDoctorCommand } from './local.mjs';
66
+ import { distillCommand } from './distill.mjs';
62
67
  import { parse } from '../argv.mjs';
63
68
  import { CLI } from '../name.mjs';
64
69
  import { fingerprint } from '../resolve/fingerprint.mjs';
@@ -88,6 +93,9 @@ const GROUPS = new Map([
88
93
  ['shared', 'plan'],
89
94
  ['lessons', 'list'],
90
95
  ['local', 'doctor'],
96
+ // Plan 0003 R45. The default is the read: `pack` alone lints the working
97
+ // directory, the way `skills` alone indexes and `graph` alone lints.
98
+ ['pack', 'lint'],
91
99
  ]);
92
100
 
93
101
  /**
@@ -135,6 +143,10 @@ export const IMPLEMENTED = {
135
143
  'lessons list': lessonsListCommand,
136
144
  'lessons review': lessonsReviewCommand,
137
145
  decide: decideCommand,
146
+ distill: distillCommand,
147
+ // R25 machinery needs the runnable command names for Affects command tokens,
148
+ // same wiring `why` and `impact` get through their lambdas.
149
+ review: (args, flags) => reviewCommand(args, flags, COMMANDS),
138
150
  'local doctor': localDoctorCommand,
139
151
  'evidence check': (args, flags) => evidenceCommand(['check', ...args], flags),
140
152
  init: initCommand,
@@ -155,8 +167,22 @@ export const IMPLEMENTED = {
155
167
  'bench ablate': benchAblateCommand,
156
168
  'bench probes': benchProbesCommand,
157
169
  'bench variants': benchVariantsCommand,
170
+ // plan 0003 R39. The critic's acceptance bench measures BLOCK-precision and
171
+ // recall over a labelled set; until it reports its floor, the critic entries
172
+ // stay advisory and no gate reads them. COMMANDS rides along so the brief
173
+ // resolves Affects command tokens the way `simt why` does.
174
+ 'bench critic': (args, flags) => benchCriticCommand(args, flags, COMMANDS),
158
175
  upgrade: upgradeCommand,
159
176
  mcp: mcpCommand,
177
+ // plan 0003 R43. A bare command: the kind is a positional, so there is no group and
178
+ // no default subcommand to resolve.
179
+ plugin: pluginCommand,
180
+ // Plan 0003 R45. A group, not a bare command: `new` and `publish` write (or
181
+ // publish) and `lint` reads, and the dispatcher routes all three through one
182
+ // subcommand switch the way `hooks` does.
183
+ 'pack new': (args, flags) => packCommand('new', args, flags),
184
+ 'pack lint': (args, flags) => packCommand('lint', args, flags),
185
+ 'pack publish': (args, flags) => packCommand('publish', args, flags),
160
186
  };
161
187
 
162
188
  /**
@@ -216,7 +242,7 @@ export async function dispatch(argv) {
216
242
  /**
217
243
  * Normalise argv into a registry key, or null when unrecognised.
218
244
  * A group with an omitted subcommand resolves to that group's default, so
219
- * `simt-dev skills` is `simt-dev skills index`.
245
+ * `simt skills` is `simt skills index`.
220
246
  */
221
247
  function commandPath(first, second) {
222
248
  if (first === undefined) return null;
@@ -225,7 +251,7 @@ function commandPath(first, second) {
225
251
  const group = GROUPS.get(first);
226
252
  if (second === undefined) return `${first} ${group}`;
227
253
  // Reject a second token that is not a subcommand of this group, rather than
228
- // silently routing `simt-dev skills nonsense` to the default subcommand.
254
+ // silently routing `simt skills nonsense` to the default subcommand.
229
255
  //
230
256
  // Read from IMPLEMENTED rather than from a second list. The list existed because
231
257
  // there was no way to enumerate what dispatch could run — `IMPLEMENTED` was an
@@ -1,4 +1,4 @@
1
- // simt-dev init - adopt a repo into the ecosystem, or scaffold a new one.
1
+ // simt init - adopt a repo into the ecosystem, or scaffold a new one.
2
2
  //
3
3
  // The governing idea: init never destroys hand-written work. It writes a manifest
4
4
  // and a generated region, reports every conflict it found BEFORE writing anything,
@@ -647,7 +647,7 @@ function render(payload, manifest, { npm = true, version } = {}) {
647
647
  : ` runs ${CLI} check where a pull request is merged; yours to edit after this`,
648
648
  );
649
649
  } else if (payload.ci.action === 'refresh') {
650
- // init does not write it. `simt-dev ci` owns the template's own copy, and a
650
+ // init does not write it. `simt ci` owns the template's own copy, and a
651
651
  // second writer would be a second answer to who owns it.
652
652
  lines.push(` ${'refresh'.padEnd(9)} ${payload.ci.file} ${payload.ci.reason}`);
653
653
  lines.push(` remedy: ${CLI} ci --yes (init does not write simt-dev's own copy)`);
@@ -30,6 +30,14 @@ function formatRoleLine(role, report) {
30
30
  return `${role.padEnd(8)} ok ${report.model} @ ${report.url}${extra}${listed}`;
31
31
  }
32
32
 
33
+ // `requestJson` shapes a non-OK response as "METHOD url failed (STATUS): message",
34
+ // so the HTTP status is the reliable signal here — 401/403 mean the server is up but
35
+ // rejected the request as unauthorised, which is a different remedy than "start it".
36
+ function isAuthFailure(report) {
37
+ if (!report || typeof report.error !== 'string') return false;
38
+ return /failed \(40[13]\)/.test(report.error);
39
+ }
40
+
33
41
  function remedyFor(result) {
34
42
  if (result.status === 'refused-remote') {
35
43
  return `set a loopback URL (http://127.0.0.1:<port>/v1) in ${MANIFEST_FILE}, or run: ${CLI} local doctor --allow-remote`;
@@ -38,6 +46,9 @@ function remedyFor(result) {
38
46
  return `fix the \`local\` configuration in ${MANIFEST_FILE}`;
39
47
  }
40
48
  if (result.status === 'unreachable') {
49
+ if ([result.chat, result.embed].some(isAuthFailure)) {
50
+ return `the server is up but rejected the request as unauthorised (401/403); set \`local.<role>.apiKey\` in ${MANIFEST_FILE} to the server's bearer token, then rerun: ${CLI} local doctor`;
51
+ }
41
52
  return `start the local model server at the URL in ${MANIFEST_FILE}, then rerun: ${CLI} local doctor`;
42
53
  }
43
54
  return null;
@@ -1,4 +1,4 @@
1
- // simt-dev mcp - serve the tool surface over MCP on stdio.
1
+ // simt mcp - serve the tool surface over MCP on stdio.
2
2
  //
3
3
  // Optional by design (plan 0002 section 5): it adds a transport, not a capability, and
4
4
  // the CLI stays the primary interface because it works in CI, in a hook and in a bare