simt-dev 0.3.1 → 0.5.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 +157 -0
- package/README.md +271 -62
- package/package.json +6 -2
- package/rules/core.md +1 -1
- package/src/advice/advice.mjs +229 -0
- package/src/agenthooks/policy.mjs +10 -2
- package/src/agenthooks/prompt.mjs +81 -0
- package/src/agenthooks/render.mjs +138 -17
- package/src/argv.mjs +28 -1
- package/src/bench/grade.mjs +3 -0
- package/src/bench/run.mjs +88 -54
- package/src/bench/tasks.mjs +38 -0
- package/src/bench/variants.mjs +456 -0
- package/src/brief/assemble.mjs +368 -0
- package/src/brief/kind.mjs +134 -0
- package/src/brief/packet.mjs +200 -0
- package/src/commands/agenthook.mjs +84 -5
- package/src/commands/bench.mjs +72 -15
- package/src/commands/brief.mjs +78 -0
- package/src/commands/compare.mjs +10 -2
- package/src/commands/decide.mjs +116 -0
- package/src/commands/distill.mjs +36 -0
- package/src/commands/help.mjs +62 -3
- package/src/commands/index.mjs +29 -1
- package/src/commands/lessons.mjs +127 -0
- package/src/commands/local.mjs +97 -0
- package/src/commands/map.mjs +42 -7
- package/src/commands/repoindex.mjs +4 -1
- package/src/commands/review.mjs +65 -0
- package/src/commands/rules.mjs +17 -2
- package/src/commands/skills.mjs +5 -50
- package/src/commands/skillscompile.mjs +255 -0
- package/src/commands/sync.mjs +15 -0
- package/src/commands/tests.mjs +2 -49
- package/src/commands/why.mjs +23 -25
- package/src/compare/learn.mjs +680 -0
- package/src/decisions/index.mjs +116 -65
- package/src/distill/distill.mjs +246 -0
- package/src/distill/engine.mjs +187 -0
- package/src/federation/client.mjs +188 -0
- package/src/federation/config.mjs +134 -0
- package/src/federation/query.mjs +48 -0
- package/src/graph/compile.mjs +475 -0
- package/src/index/gate.mjs +1 -13
- package/src/index/govern.mjs +23 -11
- package/src/index/map.mjs +251 -17
- package/src/index/persist.mjs +74 -7
- package/src/index/store.mjs +222 -8
- package/src/kernel/variants.mjs +353 -0
- package/src/ledger/index.mjs +36 -1
- package/src/local/client.mjs +482 -0
- package/src/local/config.mjs +269 -0
- package/src/mcp/tools.mjs +138 -1
- package/src/memory/decide.mjs +462 -0
- package/src/memory/lessons.mjs +858 -0
- package/src/projection/lock.mjs +39 -7
- package/src/projection/rules.mjs +32 -8
- package/src/resolve/body.mjs +46 -0
- package/src/review/critic.mjs +509 -0
- package/src/toolwords.mjs +6 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `simt decide` — scaffold an ADR with the next free number, a Status: line,
|
|
2
|
+
// a rulings skeleton, and structured `simt-affects` + `simt-supersedes` frontmatter
|
|
3
|
+
// (plan 0003 R34).
|
|
4
|
+
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
7
|
+
import { CLI } from '../name.mjs';
|
|
8
|
+
import { DECISIONS_DIR } from '../decisions/index.mjs';
|
|
9
|
+
import { lintDecisions, scaffoldDecision } from '../memory/decide.mjs';
|
|
10
|
+
import { parsePathList } from '../argv.mjs';
|
|
11
|
+
|
|
12
|
+
export async function decideCommand(args, flags) {
|
|
13
|
+
const root = flags.root ?? process.cwd();
|
|
14
|
+
|
|
15
|
+
if (flags.check === true) {
|
|
16
|
+
const decisionsDir = join(root, 'docs', 'decisions');
|
|
17
|
+
const allowHistorical = resolve(decisionsDir) === DECISIONS_DIR;
|
|
18
|
+
const res = await lintDecisions(decisionsDir, { allowHistorical });
|
|
19
|
+
|
|
20
|
+
if (isJson(flags)) {
|
|
21
|
+
emitJson({
|
|
22
|
+
ok: res.ok,
|
|
23
|
+
command: 'decide --check',
|
|
24
|
+
root,
|
|
25
|
+
findings: res.findings,
|
|
26
|
+
});
|
|
27
|
+
return res.ok ? EXIT.OK : EXIT.FAIL;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (res.findings.length === 0) {
|
|
31
|
+
emit('decisions ok', { maxBytes: flags.maxBytes ?? 0 });
|
|
32
|
+
return EXIT.OK;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
emit(
|
|
36
|
+
res.findings
|
|
37
|
+
.flatMap((f) => [
|
|
38
|
+
`${f.status.toUpperCase()} ${f.file} ${f.detail}`,
|
|
39
|
+
...(f.remedy ? [` remedy: ${f.remedy}`] : []),
|
|
40
|
+
])
|
|
41
|
+
.join('\n'),
|
|
42
|
+
{ maxBytes: flags.maxBytes ?? 0 },
|
|
43
|
+
);
|
|
44
|
+
return EXIT.FAIL;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mode = writeMode(flags);
|
|
48
|
+
const title = args.join(' ').trim() || flags.task || 'Untitled decision';
|
|
49
|
+
const parsedPaths = parsePathList(flags.paths);
|
|
50
|
+
const explicitPaths = flags.paths !== undefined ? parsedPaths : undefined;
|
|
51
|
+
|
|
52
|
+
const res = await scaffoldDecision(root, {
|
|
53
|
+
title,
|
|
54
|
+
affects: explicitPaths,
|
|
55
|
+
supersedes: flags.supersedes,
|
|
56
|
+
write: mode.apply,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
return fail(
|
|
61
|
+
res.errors[0],
|
|
62
|
+
EXIT.USAGE,
|
|
63
|
+
`usage: ${CLI} decide "<title>" [--supersedes ADR-NNN:<ruling>] [--yes]`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (mode.apply) {
|
|
68
|
+
note(`wrote ${res.relPath}`);
|
|
69
|
+
} else if (flags.dryRun === true) {
|
|
70
|
+
note('dry-run no files written');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isJson(flags)) {
|
|
74
|
+
emitJson({
|
|
75
|
+
ok: true,
|
|
76
|
+
command: 'decide',
|
|
77
|
+
root,
|
|
78
|
+
id: res.id,
|
|
79
|
+
number: res.number,
|
|
80
|
+
title: res.title,
|
|
81
|
+
file: res.file,
|
|
82
|
+
path: res.relPath,
|
|
83
|
+
affects: res.affects,
|
|
84
|
+
supersedes: res.supersedes,
|
|
85
|
+
written: res.written,
|
|
86
|
+
dryRun: !mode.apply,
|
|
87
|
+
});
|
|
88
|
+
return EXIT.OK;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const supersedesLabel =
|
|
92
|
+
res.supersedes.length > 0
|
|
93
|
+
? res.supersedes
|
|
94
|
+
.map((s) => (s.ruling !== null ? `${s.adr} ruling ${s.ruling} (${s.kind})` : s.adr))
|
|
95
|
+
.join('; ')
|
|
96
|
+
: '(none)';
|
|
97
|
+
|
|
98
|
+
const lines = [
|
|
99
|
+
`id ${res.id}`,
|
|
100
|
+
`title ${res.title}`,
|
|
101
|
+
`file ${res.relPath}`,
|
|
102
|
+
`affects ${res.affects.length > 0 ? res.affects.join(', ') : '(none)'}`,
|
|
103
|
+
`supersedes ${supersedesLabel}`,
|
|
104
|
+
'',
|
|
105
|
+
res.text.trimEnd(),
|
|
106
|
+
'',
|
|
107
|
+
mode.apply
|
|
108
|
+
? `wrote ${res.relPath}`
|
|
109
|
+
: flags.dryRun === true
|
|
110
|
+
? 'dry-run; nothing written'
|
|
111
|
+
: `would write ${res.relPath}; apply with: ${CLI} decide ${JSON.stringify(res.title)} --yes`,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
115
|
+
return EXIT.OK;
|
|
116
|
+
}
|
|
@@ -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-dev/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
|
+
}
|
package/src/commands/help.mjs
CHANGED
|
@@ -16,6 +16,12 @@ ${CLI} upgrade move every consumer to this simt-dev SHA, as one s
|
|
|
16
16
|
(dry-run unless --yes; moves a pin only - it never
|
|
17
17
|
adopts or repairs a repo; never commits or pushes)
|
|
18
18
|
|
|
19
|
+
${CLI} brief "<task>" one packet for a task: the kind, the commands, the
|
|
20
|
+
likely files, the tests and the decisions that govern
|
|
21
|
+
them, inside --budget bytes (default 2048; 0 is
|
|
22
|
+
unlimited). Says how many items it cut; --explain
|
|
23
|
+
says why each was kept or dropped
|
|
24
|
+
|
|
19
25
|
${CLI} skills index tier-0 catalog: every skill, one line each
|
|
20
26
|
${CLI} skills brief --task tier-1 pre-assembled brief <- the common case
|
|
21
27
|
${CLI} skills show <name> tier-2 body (respects --max-bytes)
|
|
@@ -28,6 +34,9 @@ ${CLI} skills eval [corpus] measure the resolver against a gold corpus
|
|
|
28
34
|
${CLI} skills install <name> pin a skill into the repo as a local override
|
|
29
35
|
(dry-run unless --yes; a multi-variant skill needs
|
|
30
36
|
--variant or --from rather than a guess)
|
|
37
|
+
${CLI} skills compile [<name>] compile a skill to Agent Skills standard, flag bodies
|
|
38
|
+
over 5000 tokens with a split plan, and report per-skill
|
|
39
|
+
trigger recall and precision from R02 transcripts
|
|
31
40
|
|
|
32
41
|
${CLI} rules list the rule catalogue (--root marks what applies to a repo)
|
|
33
42
|
${CLI} rules show <id> one rule body
|
|
@@ -39,7 +48,8 @@ ${CLI} hooks install [--yes] install the shared dispatcher; report what it wou
|
|
|
39
48
|
${CLI} hooks status what each git event routes to, and whether it is wired
|
|
40
49
|
${CLI} hooks run <event> run one event's gates now, as git would
|
|
41
50
|
(pre-commit, pre-push, post-merge, ...)
|
|
42
|
-
${CLI} agent-hook <event> the entry point a harness calls
|
|
51
|
+
${CLI} agent-hook <event> the entry point a harness calls: pre-tool-use decides,
|
|
52
|
+
prompt-submit delivers the packet as prompt context
|
|
43
53
|
(reads one JSON payload on stdin and answers in that
|
|
44
54
|
harness's dialect; exit 2 blocks; needs --harness)
|
|
45
55
|
${CLI} agent-hooks install write this repo's policies into the harnesses that
|
|
@@ -82,6 +92,32 @@ ${CLI} map [--task "<text>"] [--level outline|names|signatures]
|
|
|
82
92
|
${CLI} evidence the runs recorded against the tree on disk
|
|
83
93
|
${CLI} evidence check <file> a commit message that claims a run this tree does not
|
|
84
94
|
have: a warning, or a block when gates.evidence is "block"
|
|
95
|
+
${CLI} lessons list deterministic lesson candidates in .simt-dev/lessons.jsonl
|
|
96
|
+
and promoted lessons in docs/lessons/*.md
|
|
97
|
+
${CLI} lessons review [<id>] promote a lesson candidate to docs/lessons/NNNN-<slug>.md
|
|
98
|
+
(dry-run unless --yes; writes Appendix A frontmatter)
|
|
99
|
+
${CLI} decide [<title>] scaffold an ADR with the next free number, a Status: line,
|
|
100
|
+
a rulings skeleton, and structured simt-affects +
|
|
101
|
+
simt-supersedes frontmatter (dry-run unless --yes;
|
|
102
|
+
--check lints ADRs for prose-only partial supersessions)
|
|
103
|
+
${CLI} local doctor probe configured local model endpoints (chat and embed)
|
|
104
|
+
(reads local in ${MANIFEST_FILE}; opens no socket when
|
|
105
|
+
unset; refuses non-loopback URLs without --allow-remote)
|
|
106
|
+
${CLI} distill <skill|doc> a budgeted proposal summary of a skill body or doc,
|
|
107
|
+
every claim citing the source line range it rests on
|
|
108
|
+
(writes .simt-dev/distilled/<digest>.md; a proposal a
|
|
109
|
+
human merges - never projected or committed;
|
|
110
|
+
deterministic outline by default; --local proposes
|
|
111
|
+
from the configured chat model instead; the cache is
|
|
112
|
+
keyed by source digest and budget, --refresh regenerates)
|
|
113
|
+
${CLI} review [--local] an ADR-008 verdict skeleton for the staged diff,
|
|
114
|
+
the brief listing everything that governs it (R25)
|
|
115
|
+
(deterministic without --local: the brief and its
|
|
116
|
+
governing ids only; --local proposes from the
|
|
117
|
+
configured chat model and writes
|
|
118
|
+
.simt-dev/reviews/<digest>.md — a proposal a human
|
|
119
|
+
reads, verified against the verdict vocabulary and
|
|
120
|
+
the brief's ids; advisory always, never a gate)
|
|
85
121
|
${CLI} shared plan what a workspace's repeated rules cost to carry
|
|
86
122
|
once: the shared set, its bytes, and every path that would be written
|
|
87
123
|
(read-only; touches nothing, not even your home directory)
|
|
@@ -117,6 +153,14 @@ ${CLI} bench ablate [--yes] leave-one-out ablation across core, profile and p
|
|
|
117
153
|
${CLI} bench probes [--yes] 12 synthetic rule-compliance probes with deterministic graders
|
|
118
154
|
(dry-run unless --yes; grades git state, transcript and ledger,
|
|
119
155
|
and reports compliance per arm)
|
|
156
|
+
${CLI} bench variants [--yes] evaluate simt-variants rule slots on the canary suite and lock
|
|
157
|
+
winning variants in simt-dev.lock.json (dry-run unless --yes;
|
|
158
|
+
re-opens when model or harness changes)
|
|
159
|
+
${CLI} bench critic precision and recall of the local critic over a labelled
|
|
160
|
+
set of historical diffs (.simt-dev/critic-labels.jsonl;
|
|
161
|
+
overrides --labels; labels are human-written verdicts
|
|
162
|
+
under ADR-008; the critic is advisory until precision
|
|
163
|
+
reaches 0.8 on 30 labelled diffs)
|
|
120
164
|
${CLI} mcp the read-only surface as MCP tools over stdio
|
|
121
165
|
(--list prints it without reading stdin; init, sync,
|
|
122
166
|
upgrade and the installers stay CLI-only by design)`;
|
|
@@ -124,7 +168,8 @@ ${CLI} mcp the read-only surface as MCP tools over stdio
|
|
|
124
168
|
const FLAGS = `--json structured output (compact; one document, one line)
|
|
125
169
|
--pretty indent --json for a human
|
|
126
170
|
--max-bytes <n> hard ceiling on emitted bytes (0 = unlimited, default 0)
|
|
127
|
-
--budget <n> context budget for
|
|
171
|
+
--budget <n> context budget for a brief (default 2048)
|
|
172
|
+
(skills brief keeps its own default of 20480)
|
|
128
173
|
--plan emit a fetch plan instead of inlining skill bodies
|
|
129
174
|
--skill <name> force a skill into the brief (repeatable)
|
|
130
175
|
--task "<text>" the task text used for capability resolution
|
|
@@ -142,7 +187,8 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
142
187
|
--frozen check: the lock must match this projection; an output
|
|
143
188
|
change no input change explains fails the gate
|
|
144
189
|
--explain sync: name each line dropped as already inferable, with
|
|
145
|
-
the fact behind it
|
|
190
|
+
the fact behind it; brief: name every item and why it
|
|
191
|
+
was kept or cut, with its section's value
|
|
146
192
|
--all ignore a cap: every ruling of a record, or the skills budget
|
|
147
193
|
--variant <id> which body of a multi-variant skill to use
|
|
148
194
|
--from <repo/path> install from a declared source by name
|
|
@@ -176,6 +222,19 @@ const FLAGS = `--json structured output (compact; one document,
|
|
|
176
222
|
--compare <a> <b> bench report: compare two arms with deltas and p-values
|
|
177
223
|
--test <cmd> plan done: the test command that closes the row
|
|
178
224
|
--state <name> plan done: target state (default: Complete)
|
|
225
|
+
--level <name> map: detail level (outline, names, signatures)
|
|
226
|
+
--supersedes <spec> decide: superseded ADR or ruling (ADR-NNN or ADR-NNN:<ruling>[:<kind>]; repeatable)
|
|
227
|
+
--allow-remote local doctor / distill / review: allow non-loopback model
|
|
228
|
+
URLs (refused by default)
|
|
229
|
+
--embed index / map / brief: compute or query optional local vector embeddings
|
|
230
|
+
(off by default unless recall@10 gains at least 10 points)
|
|
231
|
+
--labels <path> bench critic: the labelled set of historical diffs to score
|
|
232
|
+
(default .simt-dev/critic-labels.jsonl; one human-written
|
|
233
|
+
{sha, verdict} pair per line)
|
|
234
|
+
--refresh distill / review: regenerate a proposal that is already cached
|
|
235
|
+
--local distill / review: propose the answer from the configured
|
|
236
|
+
local chat model (local.chat); off by default, which
|
|
237
|
+
uses the deterministic path instead
|
|
179
238
|
--help this text
|
|
180
239
|
--version the version, and the SHA it was built from`;
|
|
181
240
|
|
package/src/commands/index.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
whyCommand as skillsWhy,
|
|
26
26
|
evalCommand as skillsEval,
|
|
27
27
|
} from './skills.mjs';
|
|
28
|
+
import { briefCommand } from './brief.mjs';
|
|
28
29
|
import { syncCommand, checkCommand } from './sync.mjs';
|
|
29
30
|
import { initCommand } from './init.mjs';
|
|
30
31
|
import { ciCommand } from './ci.mjs';
|
|
@@ -34,6 +35,7 @@ import { compareCommand } from './compare.mjs';
|
|
|
34
35
|
import { upgradeCommand } from './upgrade.mjs';
|
|
35
36
|
import { mcpCommand } from './mcp.mjs';
|
|
36
37
|
import { installCommand } from './skillsinstall.mjs';
|
|
38
|
+
import { compileCommand } from './skillscompile.mjs';
|
|
37
39
|
import { listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
|
|
38
40
|
import { tokensCommand } from './tokens.mjs';
|
|
39
41
|
import { harnessCommand, harnessLoadersCommand } from './harness.mjs';
|
|
@@ -43,6 +45,8 @@ import {
|
|
|
43
45
|
benchReportCommand,
|
|
44
46
|
benchAblateCommand,
|
|
45
47
|
benchProbesCommand,
|
|
48
|
+
benchVariantsCommand,
|
|
49
|
+
benchCriticCommand,
|
|
46
50
|
} from './bench.mjs';
|
|
47
51
|
import { planCommand } from './plan.mjs';
|
|
48
52
|
import { sharedPlanCommand, sharedInstallCommand, sharedWarning } from './shared.mjs';
|
|
@@ -53,7 +57,13 @@ import { testsCommand } from './tests.mjs';
|
|
|
53
57
|
import { whyCommand } from './why.mjs';
|
|
54
58
|
import { gateCommand } from './gate.mjs';
|
|
55
59
|
import { impactCommand } from './impact.mjs';
|
|
60
|
+
import { lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
|
|
61
|
+
import { decideCommand } from './decide.mjs';
|
|
62
|
+
import { reviewCommand } from './review.mjs';
|
|
63
|
+
import { localDoctorCommand } from './local.mjs';
|
|
64
|
+
import { distillCommand } from './distill.mjs';
|
|
56
65
|
import { parse } from '../argv.mjs';
|
|
66
|
+
import { CLI } from '../name.mjs';
|
|
57
67
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
58
68
|
import { resolveWorkspace } from '../workspace.mjs';
|
|
59
69
|
|
|
@@ -79,6 +89,8 @@ const GROUPS = new Map([
|
|
|
79
89
|
// where", and `install` — the subcommand that touches a reader's home directory — is the
|
|
80
90
|
// one that needs `--yes` before it does anything.
|
|
81
91
|
['shared', 'plan'],
|
|
92
|
+
['lessons', 'list'],
|
|
93
|
+
['local', 'doctor'],
|
|
82
94
|
]);
|
|
83
95
|
|
|
84
96
|
/**
|
|
@@ -95,6 +107,7 @@ const GROUPS = new Map([
|
|
|
95
107
|
* just correct: nothing in the map depends on the call.
|
|
96
108
|
*/
|
|
97
109
|
export const IMPLEMENTED = {
|
|
110
|
+
brief: briefCommand,
|
|
98
111
|
doctor: runDoctor,
|
|
99
112
|
'graph lint': lintCommand,
|
|
100
113
|
'skills index': skillsIndex,
|
|
@@ -104,6 +117,7 @@ export const IMPLEMENTED = {
|
|
|
104
117
|
'skills why': skillsWhy,
|
|
105
118
|
'skills eval': skillsEval,
|
|
106
119
|
'skills install': installCommand,
|
|
120
|
+
'skills compile': compileCommand,
|
|
107
121
|
'rules list': rulesList,
|
|
108
122
|
'rules show': rulesShow,
|
|
109
123
|
'rules why': rulesWhy,
|
|
@@ -121,6 +135,14 @@ export const IMPLEMENTED = {
|
|
|
121
135
|
why: (args, flags) => whyCommand(args, flags, COMMANDS),
|
|
122
136
|
gate: gateCommand,
|
|
123
137
|
impact: (args, flags) => impactCommand(args, flags, COMMANDS),
|
|
138
|
+
'lessons list': lessonsListCommand,
|
|
139
|
+
'lessons review': lessonsReviewCommand,
|
|
140
|
+
decide: decideCommand,
|
|
141
|
+
distill: distillCommand,
|
|
142
|
+
// R25 machinery needs the runnable command names for Affects command tokens,
|
|
143
|
+
// same wiring `why` and `impact` get through their lambdas.
|
|
144
|
+
review: (args, flags) => reviewCommand(args, flags, COMMANDS),
|
|
145
|
+
'local doctor': localDoctorCommand,
|
|
124
146
|
'evidence check': (args, flags) => evidenceCommand(['check', ...args], flags),
|
|
125
147
|
init: initCommand,
|
|
126
148
|
ci: ciCommand,
|
|
@@ -139,6 +161,12 @@ export const IMPLEMENTED = {
|
|
|
139
161
|
'bench report': benchReportCommand,
|
|
140
162
|
'bench ablate': benchAblateCommand,
|
|
141
163
|
'bench probes': benchProbesCommand,
|
|
164
|
+
'bench variants': benchVariantsCommand,
|
|
165
|
+
// plan 0003 R39. The critic's acceptance bench measures BLOCK-precision and
|
|
166
|
+
// recall over a labelled set; until it reports its floor, the critic entries
|
|
167
|
+
// stay advisory and no gate reads them. COMMANDS rides along so the brief
|
|
168
|
+
// resolves Affects command tokens the way `simt why` does.
|
|
169
|
+
'bench critic': (args, flags) => benchCriticCommand(args, flags, COMMANDS),
|
|
142
170
|
upgrade: upgradeCommand,
|
|
143
171
|
mcp: mcpCommand,
|
|
144
172
|
};
|
|
@@ -186,7 +214,7 @@ export async function dispatch(argv) {
|
|
|
186
214
|
// simt-dev: they run this tool, not its source tree. The step number is the
|
|
187
215
|
// stable handle; the file that documents it is an implementation detail.
|
|
188
216
|
return fail(
|
|
189
|
-
|
|
217
|
+
`\`${CLI} ${path}\` is declared but not implemented`,
|
|
190
218
|
EXIT.UNSUPPORTED,
|
|
191
219
|
`implementation step ${step} is not built yet`,
|
|
192
220
|
);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `simt lessons [list|review]` — deterministic lesson candidates and promotion to
|
|
2
|
+
// `docs/lessons/NNNN-<slug>.md` (plan 0003 R32).
|
|
3
|
+
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
7
|
+
import { CLI } from '../name.mjs';
|
|
8
|
+
import { parsePathList } from '../argv.mjs';
|
|
9
|
+
import {
|
|
10
|
+
LESSONS_DIR,
|
|
11
|
+
LESSONS_FILE,
|
|
12
|
+
captureFromLedger,
|
|
13
|
+
captureRevertedCommits,
|
|
14
|
+
listPromotedLessons,
|
|
15
|
+
promoteLesson,
|
|
16
|
+
readLessonCandidates,
|
|
17
|
+
} from '../memory/lessons.mjs';
|
|
18
|
+
|
|
19
|
+
export async function lessonsListCommand(args, flags) {
|
|
20
|
+
if (args.length > 0) {
|
|
21
|
+
return fail(`unknown lessons argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} lessons`);
|
|
22
|
+
}
|
|
23
|
+
const root = resolve(flags.root ?? process.cwd());
|
|
24
|
+
await captureFromLedger(root);
|
|
25
|
+
await captureRevertedCommits(root);
|
|
26
|
+
|
|
27
|
+
const [{ candidates, errors }, promoted] = await Promise.all([
|
|
28
|
+
readLessonCandidates(root),
|
|
29
|
+
listPromotedLessons(root),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const payload = {
|
|
33
|
+
ok: errors.length === 0,
|
|
34
|
+
command: 'lessons list',
|
|
35
|
+
root,
|
|
36
|
+
ledgerFile: LESSONS_FILE,
|
|
37
|
+
lessonsDir: LESSONS_DIR,
|
|
38
|
+
candidateCount: candidates.length,
|
|
39
|
+
promotedCount: promoted.length,
|
|
40
|
+
candidates,
|
|
41
|
+
promoted,
|
|
42
|
+
errors,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (isJson(flags)) {
|
|
46
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
47
|
+
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const lines = [
|
|
51
|
+
`candidates ${candidates.length} (${LESSONS_FILE})`,
|
|
52
|
+
`promoted ${promoted.length} (${LESSONS_DIR})`,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
if (candidates.length > 0) {
|
|
56
|
+
lines.push('', 'CANDIDATES');
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
const sigShort = c.signature.slice(0, 19);
|
|
59
|
+
const state = c.status === 'promoted' ? `promoted -> ${c.promotedTo}` : 'candidate';
|
|
60
|
+
lines.push(` ${c.id} ${c.kind.padEnd(14)} ${sigShort} ${state} ${c.summary}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (promoted.length > 0) {
|
|
65
|
+
lines.push('', 'LESSONS');
|
|
66
|
+
for (const l of promoted) {
|
|
67
|
+
lines.push(` ${l.id.padEnd(28)} ${(l.signature ?? 'no-signature').slice(0, 19)} ${l.title}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const err of errors) lines.push(`error ${err}`);
|
|
72
|
+
|
|
73
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
74
|
+
return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function lessonsReviewCommand(args, flags) {
|
|
78
|
+
if (args.length > 1) {
|
|
79
|
+
return fail(`unknown lessons review argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} lessons review [<id>] [--yes]`);
|
|
80
|
+
}
|
|
81
|
+
const root = resolve(flags.root ?? process.cwd());
|
|
82
|
+
const mode = writeMode(flags);
|
|
83
|
+
const parsedPaths = parsePathList(flags.paths);
|
|
84
|
+
const paths = parsedPaths.length > 0 ? parsedPaths : undefined;
|
|
85
|
+
const title = flags.task ? String(flags.task) : undefined;
|
|
86
|
+
|
|
87
|
+
const result = await promoteLesson(root, {
|
|
88
|
+
id: args[0],
|
|
89
|
+
apply: mode.apply,
|
|
90
|
+
paths,
|
|
91
|
+
title,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
return fail(result.error, EXIT.FAIL, `record a session failure-then-fix or run: ${CLI} lessons`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const payload = {
|
|
99
|
+
ok: true,
|
|
100
|
+
command: 'lessons review',
|
|
101
|
+
root,
|
|
102
|
+
dryRun: mode.dryRun,
|
|
103
|
+
written: result.written,
|
|
104
|
+
path: result.path,
|
|
105
|
+
candidate: result.candidate,
|
|
106
|
+
markdown: result.markdown,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (isJson(flags)) {
|
|
110
|
+
emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
|
|
111
|
+
return EXIT.OK;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const lines = [
|
|
115
|
+
`promote ${result.candidate.id} (${result.candidate.kind}) -> ${result.path}`,
|
|
116
|
+
`signature ${result.candidate.signature}`,
|
|
117
|
+
`mode ${mode.apply ? 'applied' : mode.dryRun ? 'dry-run (writes nothing)' : `preview (pass --yes to write ${result.path})`}`,
|
|
118
|
+
'',
|
|
119
|
+
result.markdown.trimEnd(),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
123
|
+
if (!mode.apply) {
|
|
124
|
+
note(`note dry-run nothing written; apply with: ${CLI} lessons review --yes`);
|
|
125
|
+
}
|
|
126
|
+
return EXIT.OK;
|
|
127
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// `simt local doctor` — probe configured local model endpoints (plan 0003 R37).
|
|
2
|
+
//
|
|
3
|
+
// Reads `local: { chat: { url, model }, embed: { url, model } }` from `simt.json`.
|
|
4
|
+
// When `local` is unset, opens no socket and exits 0. Refuses non-loopback URLs
|
|
5
|
+
// without `--allow-remote` (ADR-075).
|
|
6
|
+
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
9
|
+
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
10
|
+
import { probeLocal } from '../local/client.mjs';
|
|
11
|
+
|
|
12
|
+
function formatRoleLine(role, report) {
|
|
13
|
+
if (!report) return `${role.padEnd(8)} (unset)`;
|
|
14
|
+
if (report.status === 'refused-remote') {
|
|
15
|
+
return `${role.padEnd(8)} refused-remote ${report.model} @ ${report.url}`;
|
|
16
|
+
}
|
|
17
|
+
if (!report.ok) {
|
|
18
|
+
return `${role.padEnd(8)} ${report.status} ${report.model} @ ${report.url} (${report.error})`;
|
|
19
|
+
}
|
|
20
|
+
const extra =
|
|
21
|
+
role === 'embed' && Number.isInteger(report.dimensions)
|
|
22
|
+
? ` dimensions=${report.dimensions}`
|
|
23
|
+
: '';
|
|
24
|
+
const listed =
|
|
25
|
+
report.modelListed === true
|
|
26
|
+
? ' listed=yes'
|
|
27
|
+
: report.modelListed === false
|
|
28
|
+
? ' listed=no'
|
|
29
|
+
: '';
|
|
30
|
+
return `${role.padEnd(8)} ok ${report.model} @ ${report.url}${extra}${listed}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function remedyFor(result) {
|
|
34
|
+
if (result.status === 'refused-remote') {
|
|
35
|
+
return `set a loopback URL (http://127.0.0.1:<port>/v1) in ${MANIFEST_FILE}, or run: ${CLI} local doctor --allow-remote`;
|
|
36
|
+
}
|
|
37
|
+
if (result.status === 'invalid-config' || result.status === 'invalid-manifest') {
|
|
38
|
+
return `fix the \`local\` configuration in ${MANIFEST_FILE}`;
|
|
39
|
+
}
|
|
40
|
+
if (result.status === 'unreachable') {
|
|
41
|
+
return `start the local model server at the URL in ${MANIFEST_FILE}, then rerun: ${CLI} local doctor`;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function localDoctorCommand(args, flags) {
|
|
47
|
+
const root = resolve(flags.root ?? args[0] ?? process.cwd());
|
|
48
|
+
const allowRemote = flags.allowRemote === true;
|
|
49
|
+
const result = await probeLocal(root, { allowRemote });
|
|
50
|
+
|
|
51
|
+
const code = result.ok
|
|
52
|
+
? EXIT.OK
|
|
53
|
+
: result.status === 'refused-remote' ||
|
|
54
|
+
result.status === 'invalid-config' ||
|
|
55
|
+
result.status === 'invalid-manifest'
|
|
56
|
+
? EXIT.CONFIG
|
|
57
|
+
: EXIT.FAIL;
|
|
58
|
+
|
|
59
|
+
const error = result.ok ? null : result.findings[0] ?? `local model probe failed (${result.status})`;
|
|
60
|
+
const remedy = result.ok ? null : remedyFor(result);
|
|
61
|
+
|
|
62
|
+
if (isJson(flags)) {
|
|
63
|
+
emitJson(
|
|
64
|
+
{
|
|
65
|
+
ok: code === EXIT.OK,
|
|
66
|
+
command: 'local doctor',
|
|
67
|
+
root,
|
|
68
|
+
configured: result.configured,
|
|
69
|
+
status: result.status,
|
|
70
|
+
allowRemote: result.allowRemote,
|
|
71
|
+
remoteBlocked: result.remoteBlocked,
|
|
72
|
+
socketsOpened: result.socketsOpened,
|
|
73
|
+
chat: result.chat,
|
|
74
|
+
embed: result.embed,
|
|
75
|
+
findings: result.findings,
|
|
76
|
+
...(error ? { error, remedy, exitCode: code } : {}),
|
|
77
|
+
},
|
|
78
|
+
{ maxBytes: flags.maxBytes ?? 0 },
|
|
79
|
+
);
|
|
80
|
+
return code;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lines = [
|
|
84
|
+
`root ${root}`,
|
|
85
|
+
`local ${result.configured ? result.status : '(unset) optional adapter is off; no socket opened'}`,
|
|
86
|
+
formatRoleLine('chat', result.chat),
|
|
87
|
+
formatRoleLine('embed', result.embed),
|
|
88
|
+
`sockets ${result.socketsOpened}`,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
92
|
+
|
|
93
|
+
if (!result.ok) {
|
|
94
|
+
return fail(error, code, remedy);
|
|
95
|
+
}
|
|
96
|
+
return EXIT.OK;
|
|
97
|
+
}
|
package/src/commands/map.mjs
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
7
7
|
import { CLI } from '../name.mjs';
|
|
8
|
-
import { readEdges, readFiles, readSymbols, reindex } from '../index/store.mjs';
|
|
9
|
-
import {
|
|
8
|
+
import { readEdges, readEmbeddings, readFiles, readFts, readSymbols, reindex, searchFtsBm25 } from '../index/store.mjs';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_BUDGET,
|
|
11
|
+
embedTaskQuery,
|
|
12
|
+
formatRecall,
|
|
13
|
+
measureRecall,
|
|
14
|
+
rankFiles,
|
|
15
|
+
renderMap,
|
|
16
|
+
} from '../index/map.mjs';
|
|
10
17
|
|
|
11
18
|
const LEVELS = new Set(['outline', 'names', 'signatures']);
|
|
12
19
|
|
|
@@ -25,7 +32,15 @@ export async function mapCommand(args, flags) {
|
|
|
25
32
|
const loaded = await loadGraph(root);
|
|
26
33
|
if (loaded.error) return loaded.error;
|
|
27
34
|
const task = typeof flags.task === 'string' ? flags.task : '';
|
|
28
|
-
const
|
|
35
|
+
const bm25 = task.trim().length > 0 ? await searchFtsBm25(root, task, { fts: loaded.fts }) : [];
|
|
36
|
+
const queryVector =
|
|
37
|
+
flags.embed === true && loaded.embeddings.length > 0
|
|
38
|
+
? await embedTaskQuery(root, task, {
|
|
39
|
+
embed: true,
|
|
40
|
+
allowRemote: flags.allowRemote === true,
|
|
41
|
+
})
|
|
42
|
+
: null;
|
|
43
|
+
const ranked = rankFiles({ ...loaded, bm25, queryVector, task });
|
|
29
44
|
const rendered = renderMap(ranked, loaded.symbols, { level, budget });
|
|
30
45
|
const payload = {
|
|
31
46
|
ok: true,
|
|
@@ -57,7 +72,10 @@ export async function mapCommand(args, flags) {
|
|
|
57
72
|
async function recall(root, flags) {
|
|
58
73
|
let result;
|
|
59
74
|
try {
|
|
60
|
-
result = await measureRecall(root
|
|
75
|
+
result = await measureRecall(root, {
|
|
76
|
+
embed: flags.embed === true,
|
|
77
|
+
allowRemote: flags.allowRemote === true,
|
|
78
|
+
});
|
|
61
79
|
} catch (error) {
|
|
62
80
|
return fail(`could not measure recall (${error.message})`, EXIT.UNSUPPORTED, `run ${CLI} index in a git repository`);
|
|
63
81
|
}
|
|
@@ -70,9 +88,21 @@ async function recall(root, flags) {
|
|
|
70
88
|
return EXIT.OK;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The files, symbols, edges, FTS rows and stored vector embeddings the index holds,
|
|
93
|
+
* building the index once when it is empty.
|
|
94
|
+
*
|
|
95
|
+
* `build: false` refuses that rebuild and answers with `cold: true` instead. The one
|
|
96
|
+
* caller that passes it is the prompt-submit hook (R29), and the reason is the bar that
|
|
97
|
+
* hook has to meet: a delivered packet that built an index would make a person's first
|
|
98
|
+
* prompt in a fresh clone wait on a walk of the whole repository, and a prompt that takes
|
|
99
|
+
* a minute to appear is how a hook gets uninstalled. The next prompt gets the packet, and
|
|
100
|
+
* a command a person typed at a terminal always builds what it needs.
|
|
101
|
+
*/
|
|
102
|
+
export async function loadGraph(root, { build = true } = {}) {
|
|
74
103
|
let files = await readFiles(root);
|
|
75
104
|
if (files.length === 0) {
|
|
105
|
+
if (!build) return { files: [], symbols: [], edges: [], fts: [], embeddings: [], cold: true };
|
|
76
106
|
try {
|
|
77
107
|
await reindex(root);
|
|
78
108
|
} catch (error) {
|
|
@@ -86,6 +116,11 @@ async function loadGraph(root) {
|
|
|
86
116
|
}
|
|
87
117
|
files = await readFiles(root);
|
|
88
118
|
}
|
|
89
|
-
const [symbols, edges] = await Promise.all([
|
|
90
|
-
|
|
119
|
+
const [symbols, edges, fts, embeddings] = await Promise.all([
|
|
120
|
+
readSymbols(root),
|
|
121
|
+
readEdges(root),
|
|
122
|
+
readFts(root),
|
|
123
|
+
readEmbeddings(root),
|
|
124
|
+
]);
|
|
125
|
+
return { files, symbols, edges, fts, embeddings };
|
|
91
126
|
}
|