simt-dev 0.5.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.
- package/CHANGELOG.md +122 -0
- package/README.md +78 -59
- package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +13 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +1 -1
- package/docs/decisions/ADR-097-the-channels-publish-by-hand-and-the-version-rides-in-as-an-input.md +71 -0
- package/docs/decisions/ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md +149 -0
- package/package.json +4 -4
- package/src/agenthooks/render.mjs +87 -21
- package/src/argv.mjs +13 -0
- package/src/bench/ablate.mjs +1 -3
- package/src/bench/grade.mjs +1 -1
- package/src/bench/mine.mjs +3 -3
- package/src/bench/probes.mjs +1 -3
- package/src/bench/report.mjs +1 -3
- package/src/bench/run.mjs +1 -3
- package/src/bench/tasks.mjs +1 -3
- package/src/bench/variants.mjs +1 -3
- package/src/brief/assemble.mjs +2 -4
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +40 -12
- package/src/commands/agenthook.mjs +6 -2
- package/src/commands/bench.mjs +4 -4
- package/src/commands/ci.mjs +1 -1
- package/src/commands/compare.mjs +1 -1
- package/src/commands/distill.mjs +1 -1
- package/src/commands/gate.mjs +2 -2
- package/src/commands/graph.mjs +2 -2
- package/src/commands/harness.mjs +87 -14
- package/src/commands/help.mjs +29 -7
- package/src/commands/hooks.mjs +1 -1
- package/src/commands/index.mjs +17 -3
- package/src/commands/init.mjs +2 -2
- package/src/commands/local.mjs +11 -0
- package/src/commands/mcp.mjs +1 -1
- package/src/commands/pack.mjs +246 -0
- package/src/commands/plugin.mjs +61 -0
- package/src/commands/rules.mjs +1 -1
- package/src/commands/skills.mjs +1 -1
- package/src/commands/skillscompile.mjs +1 -3
- package/src/commands/skillsinstall.mjs +1 -1
- package/src/commands/sync.mjs +8 -8
- package/src/commands/tokens.mjs +1 -1
- package/src/commands/upgrade.mjs +1 -1
- package/src/commands/why.mjs +1 -1
- package/src/compare/discover.mjs +1 -1
- package/src/compare/hooksstate.mjs +19 -7
- package/src/compare/learn.mjs +2 -2
- package/src/compare/row.mjs +3 -3
- package/src/decisions/index.mjs +1 -1
- package/src/distill/distill.mjs +2 -2
- package/src/distribute/release.mjs +142 -0
- package/src/graph/load.mjs +9 -8
- package/src/hooks/dispatcher.mjs +3 -2
- package/src/hooks/shim.mjs +11 -6
- package/src/index/govern.mjs +1 -1
- package/src/index/map.mjs +1 -1
- package/src/index/persist.mjs +2 -2
- package/src/index/store.mjs +50 -20
- package/src/ledger/index.mjs +2 -2
- package/src/local/client.mjs +22 -8
- package/src/local/config.mjs +17 -0
- package/src/locate.mjs +22 -1
- package/src/mcp/prompts.mjs +118 -0
- package/src/mcp/resources.mjs +96 -22
- package/src/mcp/server.mjs +33 -6
- package/src/mcp/tools.mjs +12 -12
- package/src/measure/session.mjs +1 -1
- package/src/memory/decide.mjs +1 -1
- package/src/memory/lessons.mjs +14 -14
- package/src/name.mjs +11 -1
- package/src/package-version.mjs +12 -10
- package/src/packs/manage.mjs +563 -0
- package/src/packs.mjs +15 -5
- package/src/plugins.mjs +164 -0
- package/src/projection/loaders.mjs +84 -10
- package/src/projection/manifest.mjs +4 -4
- package/src/projection/project.mjs +3 -3
- package/src/projection/rules.mjs +24 -4
- package/src/projection/surfaces.mjs +20 -2
- package/src/review/critic.mjs +4 -4
- package/src/toolwords.mjs +8 -5
- package/src/tsconfig.mjs +3 -3
- package/src/upgrade/plan.mjs +1 -1
- /package/bin/{simt-dev.mjs → simt.mjs} +0 -0
package/src/plugins.mjs
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// The package generator (plan 0003 R43): one command builds a Claude Code plugin, a
|
|
2
|
+
// Gemini CLI extension, or a Cursor bundle, and all three are generated from the same
|
|
3
|
+
// tables `agent-hooks install` writes into a repository. A package and a repository
|
|
4
|
+
// install therefore cannot drift apart: the hooks document, the MCP registration and
|
|
5
|
+
// (for Cursor) the kernel rules are all read from the one source, and only the entry
|
|
6
|
+
// point differs — a package ships its own copy of the runtime rather than the binary on
|
|
7
|
+
// PATH, so it installs into a clean profile that has never seen this tool.
|
|
8
|
+
//
|
|
9
|
+
// Each package is self-contained in the way its vendor requires:
|
|
10
|
+
//
|
|
11
|
+
// claude a directory with .claude-plugin/plugin.json, hooks/hooks.json and the
|
|
12
|
+
// runtime at its root; Claude names the root ${CLAUDE_PLUGIN_ROOT} and the
|
|
13
|
+
// project ${CLAUDE_PROJECT_DIR}.
|
|
14
|
+
// gemini a directory with gemini-extension.json, hooks/hooks.json and the runtime at
|
|
15
|
+
// its root; the extension names its own directory ${extensionPath} and the
|
|
16
|
+
// workspace ${workspacePath}. The manifest's name must equal the directory's,
|
|
17
|
+
// so it is taken from the destination rather than fixed.
|
|
18
|
+
// cursor a repository drop-in: .cursor/ with rules/kernel.mdc, hooks.json and
|
|
19
|
+
// mcp.json, and the runtime under .cursor/simt/. Cursor names the project
|
|
20
|
+
// ${CURSOR_PROJECT_DIR}.
|
|
21
|
+
//
|
|
22
|
+
// The skill registry is not shipped in any of them: it is workspace data (ADR-061), and
|
|
23
|
+
// a package that carried one snapshot would ship a registry that is stale the moment it
|
|
24
|
+
// leaves the machine it was built on. The command says so rather than pretending.
|
|
25
|
+
|
|
26
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
27
|
+
import { basename, join } from 'node:path';
|
|
28
|
+
import { hookDocument } from './agenthooks/render.mjs';
|
|
29
|
+
import { desiredRegion } from './projection/rules.mjs';
|
|
30
|
+
import { PACKAGE_NAME, BIN_REL } from './name.mjs';
|
|
31
|
+
|
|
32
|
+
/** The kinds `simt plugin` builds, in the order the help line names them. */
|
|
33
|
+
export const PLUGIN_KINDS = ['claude', 'gemini', 'cursor'];
|
|
34
|
+
|
|
35
|
+
/** Where the runtime copy lands inside a package, relative to the package root. */
|
|
36
|
+
function runtimeDir(kind) {
|
|
37
|
+
return kind === 'cursor' ? join('.cursor', 'simt') : '.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The entry point a package's config names: its own copy of the runtime, not PATH. */
|
|
41
|
+
function entryPoint(kind) {
|
|
42
|
+
const bin = join(runtimeDir(kind), BIN_REL);
|
|
43
|
+
// The vendor's own variables must survive as literal text in the generated config, so
|
|
44
|
+
// they are escaped here: an unescaped ${...} would interpolate a JS binding that does
|
|
45
|
+
// not exist and ship the string "undefined" into the package.
|
|
46
|
+
if (kind === 'claude') return `node "\${CLAUDE_PLUGIN_ROOT}/${bin}"`;
|
|
47
|
+
if (kind === 'gemini') return `node "\${extensionPath}/${bin}"`;
|
|
48
|
+
// Cursor runs from the repository root, so a relative path is the honest one.
|
|
49
|
+
return `node ${bin}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The project root a package's hook resolves against, from the vendor's env var. */
|
|
53
|
+
function rootFlag(kind) {
|
|
54
|
+
if (kind === 'claude') return ` --root "$CLAUDE_PROJECT_DIR"`;
|
|
55
|
+
if (kind === 'cursor') return ` --root "$CURSOR_PROJECT_DIR"`;
|
|
56
|
+
// Gemini documents no project-dir env var, but its base input carries cwd, which the
|
|
57
|
+
// adapter reads; passing a flag the vendor never sets would only expand to "".
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Where the hooks document lands inside a package, relative to the package root. */
|
|
62
|
+
function hooksPath(kind) {
|
|
63
|
+
return kind === 'cursor' ? join('.cursor', 'hooks.json') : join('hooks', 'hooks.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The generated files of one package, relative to the package root: the manifest, the
|
|
70
|
+
* hooks document and the MCP registration. The runtime copy is planned separately because
|
|
71
|
+
* it is many files; this returns only the handful that are authored here.
|
|
72
|
+
*/
|
|
73
|
+
async function packageFiles(kind, { root = process.cwd(), out } = {}) {
|
|
74
|
+
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
75
|
+
const command = `${entryPoint(kind)} agent-hook pre-tool-use --harness ${kind}${rootFlag(kind)}`;
|
|
76
|
+
const files = [];
|
|
77
|
+
|
|
78
|
+
if (kind === 'claude') {
|
|
79
|
+
files.push({
|
|
80
|
+
path: join('.claude-plugin', 'plugin.json'),
|
|
81
|
+
text: json({
|
|
82
|
+
name: PACKAGE_NAME,
|
|
83
|
+
version: pkg.version,
|
|
84
|
+
description: pkg.description,
|
|
85
|
+
mcpServers: { [PACKAGE_NAME]: { command: 'node', args: [`\${CLAUDE_PLUGIN_ROOT}/${BIN_REL}`, 'mcp'] } },
|
|
86
|
+
}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (kind === 'gemini') {
|
|
91
|
+
// The vendor requires the manifest's name to equal the directory's, so it is the
|
|
92
|
+
// destination's basename rather than a fixed string: a package installed under any
|
|
93
|
+
// name is a valid extension, and the name travels with it.
|
|
94
|
+
files.push({
|
|
95
|
+
path: 'gemini-extension.json',
|
|
96
|
+
text: json({
|
|
97
|
+
name: basename(out),
|
|
98
|
+
version: pkg.version,
|
|
99
|
+
description: pkg.description,
|
|
100
|
+
mcpServers: { [PACKAGE_NAME]: { command: 'node', args: [`\${extensionPath}/${BIN_REL}`, 'mcp'], cwd: '${workspacePath}' } },
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (kind === 'cursor') {
|
|
106
|
+
const kernel = await desiredRegion(root, { mode: 'kernel' });
|
|
107
|
+
files.push({
|
|
108
|
+
path: join('.cursor', 'rules', 'kernel.mdc'),
|
|
109
|
+
text: `---\ndescription: the kernel rules this repository enforces\nalwaysApply: true\n---\n${kernel.body}`,
|
|
110
|
+
});
|
|
111
|
+
files.push({
|
|
112
|
+
path: join('.cursor', 'mcp.json'),
|
|
113
|
+
text: json({ mcpServers: { [PACKAGE_NAME]: { command: 'node', args: [join('.cursor', 'simt', BIN_REL), 'mcp'] } } }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
files.push({ path: hooksPath(kind), text: hookDocument(kind, { command }) });
|
|
118
|
+
return files;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build one package into `out`. With `apply` false it returns the plan and writes
|
|
123
|
+
* nothing; with true it copies the runtime and writes every generated file. The plan is
|
|
124
|
+
* the same in both cases, so a dry run and an apply agree on what will land.
|
|
125
|
+
*/
|
|
126
|
+
export async function buildPackage(kind, { root = process.cwd(), out, apply = false } = {}) {
|
|
127
|
+
if (!PLUGIN_KINDS.includes(kind)) {
|
|
128
|
+
throw new Error(`unknown package kind \`${kind}\`; one of: ${PLUGIN_KINDS.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
|
|
132
|
+
const files = await packageFiles(kind, { root, out });
|
|
133
|
+
const notes = [
|
|
134
|
+
'the skill registry is workspace data (ADR-061) and is not shipped; run `simt skills index` in the profile to list what a workspace would offer',
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
if (apply) {
|
|
138
|
+
const runtimeTarget = join(out, runtimeDir(kind));
|
|
139
|
+
await mkdir(runtimeTarget, { recursive: true });
|
|
140
|
+
// The runtime is the same set of paths npm publishes (the `files` field) plus
|
|
141
|
+
// package.json. Copying anything less ships a binary that answers the hook and then
|
|
142
|
+
// dies on the first MCP call: the server reads templates/ and rules/, which a
|
|
143
|
+
// bin+src-only copy leaves behind. The `files` field is the definition of "a working
|
|
144
|
+
// copy of this tool", so it is the set a package ships.
|
|
145
|
+
for (const part of pkg.files) {
|
|
146
|
+
await cp(join(root, part), join(runtimeTarget, part), { recursive: true });
|
|
147
|
+
}
|
|
148
|
+
await cp(join(root, 'package.json'), join(runtimeTarget, 'package.json'));
|
|
149
|
+
for (const f of files) {
|
|
150
|
+
const target = join(out, f.path);
|
|
151
|
+
await mkdir(join(target, '..'), { recursive: true });
|
|
152
|
+
await writeFile(target, f.text);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
kind,
|
|
158
|
+
out,
|
|
159
|
+
apply,
|
|
160
|
+
runtime: { target: runtimeDir(kind), parts: [...pkg.files, 'package.json'] },
|
|
161
|
+
files: files.map(({ text, ...rest }) => rest),
|
|
162
|
+
notes,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
// required field rather than a comment. A reader that publishes no cap has
|
|
39
39
|
// `cap: null`, which means "checked, none found", not "not checked".
|
|
40
40
|
//
|
|
41
|
-
// 4. UNDOCUMENTED IS ITS OWN ANSWER. Copilot has no documented hooks file
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
41
|
+
// 4. UNDOCUMENTED IS ITS OWN ANSWER. Copilot has no documented hooks file. Those cells
|
|
42
|
+
// are `null` with a `note` saying what was looked for. An empty string would read
|
|
43
|
+
// as "no hooks" and an invented path would read as a fact. Gemini CLI spent a
|
|
44
|
+
// season here too — its configuration reference 404ed until it was re-checked on
|
|
45
|
+
// 2026-10-26 and found live, which moved it from a thin row to a full one; the rule
|
|
46
|
+
// is unchanged, the example is.
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* One row per reader. Ordered by how widely the reader is deployed, so a truncated
|
|
@@ -152,7 +154,7 @@ export const LOADERS = [
|
|
|
152
154
|
path: 'opencode.json',
|
|
153
155
|
key: 'mcp.servers',
|
|
154
156
|
verified:
|
|
155
|
-
'OpenCode v2 MCP servers docs: servers are named under mcp.servers; V2 does not place server names directly under mcp',
|
|
157
|
+
'OpenCode v2 MCP servers docs (read 2026-10-14): servers are named under mcp.servers; V2 does not place server names directly under mcp',
|
|
156
158
|
},
|
|
157
159
|
verified:
|
|
158
160
|
'OpenCode v2 instructions, skills, plugins and MCP docs (opencode.ai/v2/docs, read 2026-10-14) — V2 loads AGENTS.md only, combining the global file with every project AGENTS.md from the workspace up to the project root; CLAUDE.md is not a fallback; the instructions array is accepted and not loaded; skills advertise name and description from .opencode/skills, .claude/skills and .agents/skills',
|
|
@@ -244,7 +246,7 @@ export const LOADERS = [
|
|
|
244
246
|
path: '.mcp.json',
|
|
245
247
|
key: null,
|
|
246
248
|
verified:
|
|
247
|
-
'Claude Code MCP docs: project-scoped servers live in .mcp.json in the project root; local and user scope live in ~/.claude.json',
|
|
249
|
+
'Claude Code MCP docs (read 2026-10-14): project-scoped servers live in .mcp.json in the project root; local and user scope live in ~/.claude.json',
|
|
248
250
|
},
|
|
249
251
|
verified:
|
|
250
252
|
'Claude Code memory docs (docs.claude.com/en/docs/claude-code/memory, read 2026-10-14) — CLAUDE.md and CLAUDE.local.md are concatenated from every directory above the working directory, subdirectory files load on demand, `@path` imports expand to four hops, a CLAUDE.md larger than 4 MiB is skipped, and AGENTS.md is read only when no CLAUDE.md exists above the working directory',
|
|
@@ -321,7 +323,7 @@ export const LOADERS = [
|
|
|
321
323
|
mcp: {
|
|
322
324
|
path: '.codex/config.toml',
|
|
323
325
|
key: 'mcp_servers',
|
|
324
|
-
verified: 'Codex MCP docs: MCP configuration is stored in config.toml, ~/.codex/config.toml by default',
|
|
326
|
+
verified: 'Codex MCP docs (read 2026-10-14): MCP configuration is stored in config.toml, ~/.codex/config.toml by default',
|
|
325
327
|
},
|
|
326
328
|
verified:
|
|
327
329
|
'Codex AGENTS.md documentation and configuration reference (developers.openai.com/codex, read 2026-10-14) — the project scope walks from the project root to the working directory, taking at most one file per directory in the order AGENTS.override.md, AGENTS.md, project_doc_fallback_filenames; the merge runs root to cwd and stops adding files at project_doc_max_bytes, 32 KiB by default',
|
|
@@ -387,7 +389,7 @@ export const LOADERS = [
|
|
|
387
389
|
mcp: {
|
|
388
390
|
path: '.cursor/mcp.json',
|
|
389
391
|
key: null,
|
|
390
|
-
verified: 'Cursor MCP docs: "Create .cursor/mcp.json in your project"',
|
|
392
|
+
verified: 'Cursor MCP docs (read 2026-10-14): "Create .cursor/mcp.json in your project"',
|
|
391
393
|
},
|
|
392
394
|
verified:
|
|
393
395
|
'Cursor rules documentation (cursor.com/docs/context/rules, read 2026-10-14) — only .cursor/rules/*.mdc is read, a plain .md there is ignored; alwaysApply decides whether a rule costs bytes at start; @file references in a rule body are not inlined; AGENTS.md is read in the project root and in subdirectories, combined with parents',
|
|
@@ -471,13 +473,85 @@ export const LOADERS = [
|
|
|
471
473
|
mcp: {
|
|
472
474
|
path: '.agents/mcp_config.json',
|
|
473
475
|
key: null,
|
|
474
|
-
verified:
|
|
476
|
+
verified:
|
|
477
|
+
'Antigravity MCP docs (read 2026-10-14): "Configured in your workspace under .agents/mcp_config.json"',
|
|
475
478
|
},
|
|
476
479
|
verified:
|
|
477
480
|
'Google Antigravity rules documentation (antigravity.google/docs/rules/, read 2026-10-14) — rules are cumulative across global, workspace and directory scope; .agents/rules/*.md requires frontmatter with trigger, and an invalid block silently discards the rule; the directory is scanned flat and nested files are ignored unless registered in .agents/rules.json; each rule file is truncated at 24,000 bytes after includes are expanded, and global plus always_on rules share a 20,000-token budget past which the largest are demoted to pointers',
|
|
478
481
|
note:
|
|
479
482
|
'The flat scan is the fact that made the meter wrong before this table existed: a recursive walk of .agents/rules/ counts bytes for files Antigravity never reads.',
|
|
480
483
|
},
|
|
484
|
+
{
|
|
485
|
+
id: 'gemini',
|
|
486
|
+
label: 'Gemini CLI',
|
|
487
|
+
readers: 'Gemini CLI (google-gemini/gemini-cli)',
|
|
488
|
+
// No projection surface of its own: the gemini-md pointer already hands the reader
|
|
489
|
+
// the standard region, and `context.fileName` is the only mechanism by which this
|
|
490
|
+
// reader would load AGENTS.md at all — renaming the context file is the vendor's
|
|
491
|
+
// documented way to point at it. A separate rules row would be a second way to hand
|
|
492
|
+
// one reader the same rules (the ruling recorded on the surfaces `gemini-md`
|
|
493
|
+
// pointer). This FACTS row exists because the reader's own facts differ from every
|
|
494
|
+
// other row's: no published cap, depth-5 imports, and the one MCP registration left
|
|
495
|
+
// that a project file can carry.
|
|
496
|
+
surface: null,
|
|
497
|
+
start: [
|
|
498
|
+
{
|
|
499
|
+
scope: 'project',
|
|
500
|
+
path: 'GEMINI.md',
|
|
501
|
+
tree: 'chain',
|
|
502
|
+
atStart: true,
|
|
503
|
+
why: 'Gemini CLI context docs (docs/cli/gemini-md/): GEMINI.md is collected from the working directory upward through parent directories until the first directory holding a boundary marker (.git by default) or home',
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
scope: 'user',
|
|
507
|
+
path: '~/.gemini/GEMINI.md',
|
|
508
|
+
tree: 'single',
|
|
509
|
+
atStart: true,
|
|
510
|
+
why: 'Gemini CLI context docs: the user-global ~/.gemini/GEMINI.md opens the concatenation, before any project file',
|
|
511
|
+
},
|
|
512
|
+
],
|
|
513
|
+
alsoReads: [
|
|
514
|
+
{
|
|
515
|
+
what: 'the context file under another name — settings.json context.fileName, a string or an array',
|
|
516
|
+
atStart: true,
|
|
517
|
+
why: 'Gemini CLI configuration docs: context.fileName renames the context file (a string or an array), so an installation listing AGENTS.md there reads AGENTS.md as its GEMINI.md',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
what: 'GEMINI.md (or the renamed context file) in subdirectories below the working directory',
|
|
521
|
+
atStart: false,
|
|
522
|
+
why: 'Gemini CLI context docs: directories a tool touches are scanned for a context file up to a trusted root, bounded by context.discoveryMaxDirs (200) - not at session start',
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
order:
|
|
526
|
+
'user-global ~/.gemini/GEMINI.md first, then the project chain general to specific - project-root side first (upward traversal stops at the first directory holding a boundary marker, .git by default) down through each directory to the working directory - then just-in-time files a tool touch discovered; the whole result is one concatenation with origin separators',
|
|
527
|
+
cap: null,
|
|
528
|
+
capNote: 'no byte cap is published for the merged Gemini CLI context (checked 2026-10-26)',
|
|
529
|
+
imports: {
|
|
530
|
+
syntax: '@path',
|
|
531
|
+
depth: 5,
|
|
532
|
+
why: 'Gemini CLI context docs: @path imports resolve relative or absolute to a maximum depth of 5, with cycles prevented by skipping files already on the stack',
|
|
533
|
+
},
|
|
534
|
+
nested:
|
|
535
|
+
'general to specific, all loaded — unlike Codex, every context file on the walk contributes; a nearer file cannot hide a parent one, it just concatenates after it; subdirectory files wait for a tool touch',
|
|
536
|
+
skills: [],
|
|
537
|
+
skillsNote:
|
|
538
|
+
'no project skill directory is documented for Gemini CLI; extensions are the distribution mechanism, and they are tooling not context',
|
|
539
|
+
hooks: {
|
|
540
|
+
path: '.gemini/settings.json',
|
|
541
|
+
verified:
|
|
542
|
+
'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
|
+
},
|
|
544
|
+
mcp: {
|
|
545
|
+
path: '.gemini/settings.json',
|
|
546
|
+
key: 'mcpServers',
|
|
547
|
+
verified:
|
|
548
|
+
'Gemini CLI configuration docs (docs/reference/configuration/, read 2026-10-26): a project .gemini/settings.json (a user ~/.gemini/settings.json merged behind it, objects merged key by key) carries a top-level mcpServers object whose entries name a command, a url or an httpUrl',
|
|
549
|
+
},
|
|
550
|
+
verified:
|
|
551
|
+
'Gemini CLI context and configuration docs (geminicli.com/docs/cli/gemini-md/ and docs/reference/configuration/, read 2026-10-26) — settings live at project .gemini/settings.json and user ~/.gemini/settings.json, merged for objects; the context is user ~/.gemini/GEMINI.md plus the context file (GEMINI.md unless context.fileName renames it, an array is allowed) from the working directory upward through parents to the first .git or home, with @path imports to depth 5; server registration is the mcpServers object in those same settings files; no byte cap is published',
|
|
552
|
+
note:
|
|
553
|
+
'The configuration reference this table once recorded as 404ing is live again as of 2026-10-26, which is why the row is full instead of thin. What still shapes a byte count is the pair of absences the docs confirm rather than deny: no cap, and no promise that AGENTS.md is read at all — context.fileName decides that, and it may name anything.',
|
|
554
|
+
},
|
|
481
555
|
{
|
|
482
556
|
id: 'copilot',
|
|
483
557
|
label: 'GitHub Copilot',
|
|
@@ -533,7 +607,7 @@ export const LOADERS = [
|
|
|
533
607
|
path: '.vscode/mcp.json',
|
|
534
608
|
key: null,
|
|
535
609
|
verified:
|
|
536
|
-
'GitHub Copilot MCP docs: the VS Code extension reads .vscode/mcp.json; Copilot CLI reads .mcp.json, .github/mcp.json or ~/.copilot/mcp-config.json; a repository-level configuration entered on github.com is not a file at all',
|
|
610
|
+
'GitHub Copilot MCP docs (read 2026-10-14): the VS Code extension reads .vscode/mcp.json; Copilot CLI reads .mcp.json, .github/mcp.json or ~/.copilot/mcp-config.json; a repository-level configuration entered on github.com is not a file at all',
|
|
537
611
|
},
|
|
538
612
|
verified:
|
|
539
613
|
'GitHub Copilot documentation (docs.github.com/en/copilot, read 2026-10-14) — repository-wide instructions live at .github/copilot-instructions.md; path-specific instructions live at .github/instructions/**/NAME.instructions.md with applyTo globs and an optional excludeAgent; agent instructions are one or more AGENTS.md anywhere in the repository, nearest wins, or a single root CLAUDE.md or GEMINI.md; no byte cap is published',
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// Inventing a number here would be worse than declaring none.
|
|
13
13
|
//
|
|
14
14
|
// Every default this module produces is explained by the ADR that requires it. The
|
|
15
|
-
// rationale is not duplicated into the JSON (data must stay data); `simt
|
|
15
|
+
// rationale is not duplicated into the JSON (data must stay data); `simt init`
|
|
16
16
|
// prints the reason for each value it writes.
|
|
17
17
|
|
|
18
18
|
import { readFile, rename, writeFile } from 'node:fs/promises';
|
|
@@ -430,7 +430,7 @@ export function buildManifest({ root, stack = [], sha, version, existing = null,
|
|
|
430
430
|
gates: gatesFinal,
|
|
431
431
|
localSkills: existing?.localSkills ?? [],
|
|
432
432
|
// ADR-013: profile ids the repo wants, whether they ship centrally or live
|
|
433
|
-
// in `.simt
|
|
433
|
+
// in `.simt/profiles/`. Detection already selects the right profiles; this
|
|
434
434
|
// is the declaration a reviewer reads to see the repo's intent stated rather
|
|
435
435
|
// than inferred.
|
|
436
436
|
profiles: existing?.profiles ?? [],
|
|
@@ -482,8 +482,8 @@ export function buildManifest({ root, stack = [], sha, version, existing = null,
|
|
|
482
482
|
// simtDevSha is the digest of the rule bytes this projection was rendered from;
|
|
483
483
|
// version is the simt-dev release that produced it. The gate compares the running
|
|
484
484
|
// binary to version and refuses on a mismatch, which is how a repository without
|
|
485
|
-
// npm (
|
|
486
|
-
// adopted. See src/package-version.mjs.
|
|
485
|
+
// npm (the release binary in `.simt/bin`, pinned only by this field) is held to
|
|
486
|
+
// the release it adopted. See src/package-version.mjs.
|
|
487
487
|
projection: { simtDevSha: sha ?? null, version: version ?? null },
|
|
488
488
|
};
|
|
489
489
|
return withRepoKeys(existing, owned);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// which are interchangeable in a profile (ADR-013) - plus one fallback profile
|
|
12
12
|
// so a JS/TS repo is never left with core rules and nothing else.
|
|
13
13
|
//
|
|
14
|
-
// Profiles may live in the repo, not only here. `.simt
|
|
14
|
+
// Profiles may live in the repo, not only here. `.simt/profiles/*.md` uses
|
|
15
15
|
// the same frontmatter contract as `rules/stacks/*.md` and is selected by the
|
|
16
16
|
// same matcher, so supporting an ecosystem nobody here has heard of requires no
|
|
17
17
|
// code change and no release.
|
|
@@ -412,7 +412,7 @@ export async function checkTargets(root, flags = {}) {
|
|
|
412
412
|
//
|
|
413
413
|
// Reported as `orphaned`, never `drift`. ADR-021 ruling 6: `sync` does not delete a
|
|
414
414
|
// file it once generated, so there is nothing for the reader to repair and a
|
|
415
|
-
// `run: simt
|
|
415
|
+
// `run: simt sync --yes` remedy would be a lie. Removal is a human decision.
|
|
416
416
|
findings.push(...(await orphanedRuleFiles(root, resolved)));
|
|
417
417
|
|
|
418
418
|
// Pointers are checked last, and whether one blocks depends on who reads it.
|
|
@@ -604,7 +604,7 @@ async function regionOpts(root) {
|
|
|
604
604
|
* A reader that stops at a byte offset never sees a region that ends past it.
|
|
605
605
|
* The finding names the reader, because "the file is long" does not say who stops.
|
|
606
606
|
*/
|
|
607
|
-
// Exported for the context meter (`simt
|
|
607
|
+
// Exported for the context meter (`simt tokens`), which answers the same question
|
|
608
608
|
// about a region rather than about a diff. It reads the structured fields instead of
|
|
609
609
|
// re-deriving the limit, because a second implementation of "which reader stops, and
|
|
610
610
|
// where" is a second thing to keep right - and a second thing to quote in a message.
|
package/src/projection/rules.mjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// which are interchangeable in a profile (ADR-013) - plus one fallback profile so
|
|
16
16
|
// a JS/TS repo is never left with core rules and nothing else.
|
|
17
17
|
//
|
|
18
|
-
// Profiles may live in the repo, not only here. `.simt
|
|
18
|
+
// Profiles may live in the repo, not only here. `.simt/profiles/*.md` uses the
|
|
19
19
|
// same frontmatter contract as `rules/stacks/*.md` and is selected by the same
|
|
20
20
|
// matcher, so supporting an ecosystem nobody here has heard of requires no code
|
|
21
21
|
// change and no release.
|
|
@@ -46,6 +46,17 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
46
46
|
export const ROOT = resolve(HERE, '..', '..');
|
|
47
47
|
const RULES_DIR = join(ROOT, 'rules');
|
|
48
48
|
|
|
49
|
+
// The orientation pointer that follows the command card in every projected region.
|
|
50
|
+
//
|
|
51
|
+
// The agent playbook — `brief` on a new task, `why` before an edit — lives in this
|
|
52
|
+
// package's README and never reaches the agent working in an adopted repository: the
|
|
53
|
+
// projection carries the rules, not the tool that serves them. Without this line the
|
|
54
|
+
// agent has no way to know `simt brief` exists, and "answers over exploration" has no
|
|
55
|
+
// delivery path in the one file every harness reads. Static on purpose: a byte that
|
|
56
|
+
// varied per repository would break the stable prefix this region exists to protect.
|
|
57
|
+
export const ORIENTATION_POINTER =
|
|
58
|
+
`New task? Run \`${CLI} brief "<task>"\` before exploring; before editing a file, \`${CLI} why <file>\`.`;
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* The identity recorded in generated regions and in the manifest pin.
|
|
51
62
|
*
|
|
@@ -131,7 +142,7 @@ async function walk(dir, prefix = '') {
|
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
/** Repo-local profile directory, relative to a repo root (ADR-013 section 2). */
|
|
134
|
-
export const LOCAL_PROFILE_DIR = '.simt
|
|
145
|
+
export const LOCAL_PROFILE_DIR = '.simt/profiles';
|
|
135
146
|
|
|
136
147
|
/**
|
|
137
148
|
* Which origin outranks which when two profiles claim one `simt-id`.
|
|
@@ -501,7 +512,12 @@ export async function desiredRegion(root, flags) {
|
|
|
501
512
|
// symmetric: packing a line that should have been deferred spends the budget the mode
|
|
502
513
|
// exists to save, while deferring a line that should have been packed costs a reader
|
|
503
514
|
// one `rules show`. The mode is a promise about bytes, so it is the bytes that win.
|
|
504
|
-
|
|
515
|
+
// An explicit `mode` flag overrides the declared one. The caller is the MCP
|
|
516
|
+
// `simt://kernel` resource, which by name promises the kernel's bytes for any
|
|
517
|
+
// repository: the declared mode governs what `sync` writes to disk, not what a read
|
|
518
|
+
// path may look at, so the read renders in the mode the URI names instead of
|
|
519
|
+
// re-reading a manifest it must not obey (ADR-002).
|
|
520
|
+
const mode = typeof flags?.mode === 'string' ? flags.mode : await projectionMode(root);
|
|
505
521
|
const packed = [];
|
|
506
522
|
const deferred = [];
|
|
507
523
|
for (const r of inRegion) {
|
|
@@ -683,6 +699,10 @@ export async function desiredRegion(root, flags) {
|
|
|
683
699
|
const repoRules = kept.filter((r) => !toolRule(r)).sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
684
700
|
let text = render(stable);
|
|
685
701
|
if (card.text) text += `${text === '' ? '' : '\n\n'}${card.text}`;
|
|
702
|
+
// The pointer follows the card: the card says what this repository runs, and the
|
|
703
|
+
// pointer says how the tool serves a task. Adjacent, they read as one block of
|
|
704
|
+
// repo-facing advice instead of two facts the reader has to connect.
|
|
705
|
+
text += `${text === '' ? '' : '\n\n'}${ORIENTATION_POINTER}`;
|
|
686
706
|
const repoText = render(repoRules);
|
|
687
707
|
if (repoText) text += `\n\n${repoText}`;
|
|
688
708
|
const sources = [...stable, ...repoRules].map((r) => r.source);
|
|
@@ -962,7 +982,7 @@ function scopedPaths(rule, resolved) {
|
|
|
962
982
|
|
|
963
983
|
/**
|
|
964
984
|
* Read every rule file once, with its frontmatter parsed, plus any repo-local
|
|
965
|
-
* profile under `.simt
|
|
985
|
+
* profile under `.simt/profiles/`.
|
|
966
986
|
*
|
|
967
987
|
* A rule without a `simt-id` is a defect, not a default: ids are what `rules why`
|
|
968
988
|
* and the generated region's source list refer to.
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// `.agents/rules/` or `.github/instructions/`, and a symlink into a
|
|
31
31
|
// directory whose reader's symlink behaviour is unverified is a guess with a
|
|
32
32
|
// mode bit. So those get machine-written copies — and because every byte is
|
|
33
|
-
// derived from `rules/mdc/*.mdc` and `simt
|
|
33
|
+
// derived from `rules/mdc/*.mdc` and `simt check` compares each against
|
|
34
34
|
// that same derivation, they are not duplication, they are a build output.
|
|
35
35
|
// The difference from that duplicate is that nobody can hand-edit one
|
|
36
36
|
// and have it survive a sync.
|
|
@@ -387,12 +387,30 @@ export const SURFACE_IDS = SURFACES.map((s) => s.id);
|
|
|
387
387
|
* this repository", never "this harness is installed" and never "its rules are not
|
|
388
388
|
* being loaded".
|
|
389
389
|
*
|
|
390
|
+
* The exception that proves the rule is `.gemini`: R42 read Gemini CLI's
|
|
391
|
+
* configuration docs, so its entry carries readsAgentsMd and a `verified` the same way
|
|
392
|
+
* a surface row does. Everyone else stays a bare name until somebody reads their
|
|
393
|
+
* vendor pages — the bar for facts here is a document, not an ML search snippet, and
|
|
394
|
+
* that bar is what keeps this list honest as harnesses come and go.
|
|
395
|
+
*
|
|
390
396
|
* `.github` is absent even though the Copilot row carries it: `.github` holds workflows
|
|
391
397
|
* and issues, so its presence says nothing about whether Copilot is used, and the
|
|
392
398
|
* Copilot row's own detection is not something this command is for.
|
|
393
399
|
*/
|
|
394
400
|
export const CANDIDATE_DIRS = [
|
|
395
|
-
{
|
|
401
|
+
{
|
|
402
|
+
// The one candidate with verified facts (R42): Gemini CLI's configuration reference
|
|
403
|
+
// was live and read on 2026-10-26, so presence now carries provenance instead of
|
|
404
|
+
// the default "a reader may be configured here". AGENTS.md is loaded only when
|
|
405
|
+
// settings.json `context.fileName` names it - the context file is GEMINI.md
|
|
406
|
+
// otherwise - so the fact is "some", which here means one reader under
|
|
407
|
+
// configuration, and the note says exactly that.
|
|
408
|
+
dir: '.gemini',
|
|
409
|
+
harness: 'Gemini CLI',
|
|
410
|
+
readsAgentsMd: 'some',
|
|
411
|
+
readsAgentsMdNote: 'AGENTS.md is loaded only when settings.json context.fileName names it; the context file is GEMINI.md otherwise',
|
|
412
|
+
verified: 'Gemini CLI configuration docs (geminicli.com/docs/reference/configuration/, read 2026-10-26) — a .gemini directory holds the project settings.json, and the reader loads GEMINI.md unless context.fileName renames it',
|
|
413
|
+
},
|
|
396
414
|
{ dir: '.windsurf', harness: 'Windsurf' },
|
|
397
415
|
{ dir: '.zed', harness: 'Zed' },
|
|
398
416
|
{ dir: '.kiro', harness: 'Kiro' },
|
package/src/review/critic.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// consumer: it sends the brief to the endpoint named in the manifest (R37) and asks
|
|
6
6
|
// for an ADR-008 verdict skeleton, then verifies the verdict vocabulary and every
|
|
7
7
|
// cited id against that brief before the reply becomes a proposal under
|
|
8
|
-
// `.simt
|
|
8
|
+
// `.simt/reviews/`. A proposal a human never reads is a dead file, which is the
|
|
9
9
|
// intended failure mode (ADR-075, ADR-090).
|
|
10
10
|
//
|
|
11
|
-
// Nothing here writes outside `.simt
|
|
11
|
+
// Nothing here writes outside `.simt/reviews/`, and nothing here edits a source
|
|
12
12
|
// file — the model is a critic, not an author. The verdict is advisory by
|
|
13
13
|
// construction; no gate reads it. The row's own promotion rule names the test:
|
|
14
14
|
// precision at least 0.8 on a labelled set of historical diffs. Until
|
|
@@ -32,9 +32,9 @@ import { digestOf, promptHash, proposalFrontmatter } from '../distill/engine.mjs
|
|
|
32
32
|
|
|
33
33
|
const exec = promisify(execFile);
|
|
34
34
|
|
|
35
|
-
// Locked on-disk beneath `.simt
|
|
35
|
+
// Locked on-disk beneath `.simt`, like `.simt/distilled` (R38) — a cache of
|
|
36
36
|
// proposals keyed by content digest, never projected and never committed.
|
|
37
|
-
const REVIEW_DIR = '.simt
|
|
37
|
+
const REVIEW_DIR = '.simt/reviews';
|
|
38
38
|
const ENGINE_REVIEW_LOCAL = 'review-local';
|
|
39
39
|
|
|
40
40
|
/** The diff brief's byte cap; `--budget` overrides it, `0` means unlimited. */
|
package/src/toolwords.mjs
CHANGED
|
@@ -21,21 +21,24 @@ import { CHECK_MARKER } from './kernel/checks.mjs';
|
|
|
21
21
|
import { VARIANTS_MARKER } from './kernel/variants.mjs';
|
|
22
22
|
import { LESSON_KEYS } from './memory/lessons.mjs';
|
|
23
23
|
import { DECISION_KEYS } from './memory/decide.mjs';
|
|
24
|
+
import { PROMPT_WORDS } from './mcp/prompts.mjs';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* `simt-` names that belong to this tool and never to a repository.
|
|
27
28
|
*
|
|
28
|
-
* The
|
|
29
|
-
* adopts a repository into — the package, the gate file it puts
|
|
30
|
-
* hooks directory it points `core.hooksPath` at
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* The three markers and the four suffixed names are not frontmatter: they are names this
|
|
30
|
+
* tool writes into files it adopts a repository into — the package, the gate file it puts
|
|
31
|
+
* in `.github`, the hooks directory it points `core.hooksPath` at, and the two prompts it
|
|
32
|
+
* serves over MCP, derived from the module that owns them. `simt-dev` itself is here for
|
|
33
|
+
* the same reason the estate pattern exempts it, and stating it once here is what stops
|
|
34
|
+
* the two readers from disagreeing about the tool's own name.
|
|
33
35
|
*/
|
|
34
36
|
export const TOOL_WORDS = new Set([
|
|
35
37
|
...KNOWN_KEYS,
|
|
36
38
|
...PROFILE_KEYS,
|
|
37
39
|
...LESSON_KEYS,
|
|
38
40
|
...DECISION_KEYS,
|
|
41
|
+
...PROMPT_WORDS,
|
|
39
42
|
INFERABLE_MARKER,
|
|
40
43
|
CHECK_MARKER,
|
|
41
44
|
VARIANTS_MARKER,
|
package/src/tsconfig.mjs
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// tool's output rather than the file's.
|
|
17
17
|
//
|
|
18
18
|
// The whole file is owned by `init` alone. It is not in `src/ci/` and not in
|
|
19
|
-
// `src/projection/baselines.mjs` because it is neither a `.github` file that `simt
|
|
20
|
-
// refreshes nor a marker region: `simt
|
|
19
|
+
// `src/projection/baselines.mjs` because it is neither a `.github` file that `simt ci`
|
|
20
|
+
// refreshes nor a marker region: `simt check` reports its absence and never its contents.
|
|
21
21
|
|
|
22
22
|
import { readFile } from 'node:fs/promises';
|
|
23
23
|
import { join } from 'node:path';
|
|
@@ -174,7 +174,7 @@ export function readJsonc(text) {
|
|
|
174
174
|
*
|
|
175
175
|
* There is no `refresh`. ADR-026 gave simt-dev's own `.github` files a refresh path because
|
|
176
176
|
* this repository is the template's only author; a base file has the same argument, and it
|
|
177
|
-
* is deliberately not extended to it. `simt
|
|
177
|
+
* is deliberately not extended to it. `simt check` in this repository reports the
|
|
178
178
|
* baseline files as unmanaged by design, so a refresh path here would be a way to make this
|
|
179
179
|
* repository's own gate green by rewriting the file it is judging — which is the same
|
|
180
180
|
* mistake as a gate that edits its own expectations.
|
package/src/upgrade/plan.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export function planUpgrade({ name, path, manifest, manifestExists, manifestErro
|
|
|
29
29
|
const primary = list[0] ?? null;
|
|
30
30
|
const base = { name, path, sha, pinnedSha: manifest?.projection?.simtDevSha ?? null };
|
|
31
31
|
|
|
32
|
-
// A repo that never adopted is not behind; it is absent. `simt
|
|
32
|
+
// A repo that never adopted is not behind; it is absent. `simt init` is the command
|
|
33
33
|
// for it, and calling this a failure would make the exit code describe the estate's
|
|
34
34
|
// migration progress rather than this run's correctness.
|
|
35
35
|
if (!manifestExists) {
|
|
File without changes
|