simt-dev 0.6.3 → 0.7.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 +35 -0
- package/README.md +2 -2
- package/agents/simt-agent.md +84 -0
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
- package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
- package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
- package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
- package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
- package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
- package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
- package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
- package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
- package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
- package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
- package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
- package/docs/decisions/README.md +9 -0
- package/package.json +4 -2
- package/rules/core.md +2 -2
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +3 -3
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/dart.md +1 -1
- package/rules/stacks/deno.md +1 -1
- package/rules/stacks/dotnet.md +1 -1
- package/rules/stacks/elixir.md +1 -1
- package/rules/stacks/go.md +1 -1
- package/rules/stacks/jvm.md +3 -2
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/ruby.md +1 -1
- package/rules/stacks/rust.md +1 -1
- package/rules/stacks/swift.md +1 -1
- package/rules/stacks/typescript.md +2 -2
- package/rules/stacks/uv.md +3 -2
- package/skills/simt-commands/SKILL.md +70 -0
- package/src/advice/advice.mjs +7 -3
- package/src/agenthooks/decide.mjs +141 -9
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/prompt.mjs +6 -2
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/argv.mjs +8 -0
- package/src/bench/ablate.mjs +31 -14
- package/src/bench/mine.mjs +48 -1
- package/src/bench/probes.mjs +5 -6
- package/src/bench/report.mjs +243 -80
- package/src/bench/run.mjs +12 -11
- package/src/bench/variants.mjs +112 -40
- package/src/brief/assemble.mjs +6 -0
- package/src/ci/workflow.mjs +16 -7
- package/src/commands/agent.mjs +206 -0
- package/src/commands/agenthook.mjs +109 -15
- package/src/commands/bench.mjs +2 -1
- package/src/commands/compare.mjs +8 -7
- package/src/commands/decide.mjs +6 -5
- package/src/commands/evidence.mjs +13 -3
- package/src/commands/gate.mjs +85 -19
- package/src/commands/graph.mjs +18 -5
- package/src/commands/help.mjs +35 -14
- package/src/commands/hooks.mjs +68 -25
- package/src/commands/impact.mjs +4 -2
- package/src/commands/index.mjs +17 -4
- package/src/commands/init.mjs +169 -37
- package/src/commands/lease.mjs +203 -35
- package/src/commands/lessons.mjs +65 -9
- package/src/commands/map.mjs +33 -18
- package/src/commands/plugin.mjs +1 -1
- package/src/commands/repoindex.mjs +1 -0
- package/src/commands/rules.mjs +92 -18
- package/src/commands/skills.mjs +60 -25
- package/src/commands/skillscompile.mjs +8 -7
- package/src/commands/skillsinstall.mjs +12 -0
- package/src/commands/slice.mjs +377 -18
- package/src/commands/sync.mjs +37 -22
- package/src/commands/tests.mjs +5 -2
- package/src/commands/undo.mjs +24 -16
- package/src/commands/upgrade.mjs +33 -1
- package/src/commands/why.mjs +10 -4
- package/src/compare/hooksstate.mjs +23 -1
- package/src/compare/learn.mjs +82 -57
- package/src/compare/parallel.mjs +50 -0
- package/src/compare/row.mjs +26 -1
- package/src/completions.mjs +5 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index.mjs +109 -11
- package/src/distill/distill.mjs +3 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +18 -6
- package/src/graph/load.mjs +38 -19
- package/src/hooks/capability.mjs +14 -1
- package/src/hooks/dispatcher.mjs +335 -13
- package/src/hooks/resolve.mjs +9 -3
- package/src/hooks/shim.mjs +12 -7
- package/src/identity.mjs +67 -14
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +307 -47
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +296 -77
- package/src/kernel/card.mjs +37 -12
- package/src/kernel/checks.mjs +44 -9
- package/src/kernel/variants.mjs +27 -16
- package/src/lease.mjs +97 -15
- package/src/ledger/index.mjs +91 -25
- package/src/mcp/resources.mjs +5 -3
- package/src/mcp/server.mjs +81 -22
- package/src/mcp/tools.mjs +45 -9
- package/src/memory/decide.mjs +98 -43
- package/src/memory/lessons.mjs +168 -33
- package/src/name.mjs +9 -0
- package/src/plan/index.mjs +31 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +6 -2
- package/src/prbody.mjs +7 -5
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/disclosure.mjs +2 -2
- package/src/projection/loaders.mjs +41 -1
- package/src/projection/lock.mjs +21 -14
- package/src/projection/manifest.mjs +69 -19
- package/src/projection/markers.mjs +43 -3
- package/src/projection/mdc.mjs +34 -7
- package/src/projection/project.mjs +162 -18
- package/src/projection/rules.mjs +54 -3
- package/src/projection/surfaces.mjs +9 -1
- package/src/repo-root.mjs +69 -0
- package/src/resolve/body.mjs +4 -0
- package/src/resolve/fingerprint.mjs +5 -0
- package/src/resolve/local.mjs +58 -0
- package/src/review/critic.mjs +4 -2
- package/src/secrets.mjs +165 -22
- package/src/state.mjs +76 -0
- package/src/status.mjs +9 -4
- package/src/toolwords.mjs +5 -0
- package/src/tsconfig.mjs +39 -99
- package/templates/plan/tracker.json +1 -1
package/src/commands/rules.mjs
CHANGED
|
@@ -20,7 +20,9 @@ import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
|
20
20
|
import { loadRules, select } from '../projection/project.mjs';
|
|
21
21
|
import { loadMdcRules } from '../projection/mdc.mjs';
|
|
22
22
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
23
|
-
import { byId, citations,
|
|
23
|
+
import { byId, citations, loadDecisionsCached, redactEstate } from '../decisions/index.mjs';
|
|
24
|
+
import { DECISIONS_DIR } from '../decisions/index.mjs';
|
|
25
|
+
import { decisionsDir } from '../decisions/dir.mjs';
|
|
24
26
|
import { nearMatches } from '../resolve/near.mjs';
|
|
25
27
|
import { CLI } from '../name.mjs';
|
|
26
28
|
|
|
@@ -175,15 +177,36 @@ function rulesFailure(message, remedy, exitCode) {
|
|
|
175
177
|
throw Object.assign(new Error(message), { remedy, exitCode });
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/**
|
|
181
|
+
* The merged decision index: the package's shipped records underlay the
|
|
182
|
+
* repository's own, which win on an id collision (review 0003 Q34).
|
|
183
|
+
*
|
|
184
|
+
* Before this, a consumer with its own ADR-001/002 read `rules why core` as
|
|
185
|
+
* settled by *their* ADR-001 while the package's ADR-003…009 — the ones the
|
|
186
|
+
* shipped rules actually cite — reported MISSING. The overlay keeps both
|
|
187
|
+
* visible: every decision carries which layer it came from, so a same-number
|
|
188
|
+
* collision reads as two records, never as one answer wearing the wrong title.
|
|
189
|
+
*
|
|
190
|
+
* @returns {{ok: boolean, decisions: object[], errors: string[], origins: Record<string,'repo'|'package'>}}
|
|
191
|
+
*/
|
|
178
192
|
async function loadDecisionsFor(root, all) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
});
|
|
193
|
+
// Q55: one resolution (manifest key, then detection) and the shared cache,
|
|
194
|
+
// so `rules why` answers from the repository's own records without re-parsing
|
|
195
|
+
// every record on every call. No root means the package's own records.
|
|
196
|
+
const pkg = await loadDecisionsCached({ root, dir: DECISIONS_DIR, all });
|
|
197
|
+
if (!root) return { ...pkg, origins: Object.fromEntries(pkg.decisions.map((d) => [d.id, 'package'])) };
|
|
198
|
+
const relDir = await decisionsDir(root);
|
|
199
|
+
const absDir = join(root, relDir);
|
|
200
|
+
const hasLocal = (await readdir(absDir).catch(() => [])).some((n) => /^ADR-\d{3,}-.*\.md$/i.test(n));
|
|
201
|
+
if (!hasLocal) return { ...pkg, origins: Object.fromEntries(pkg.decisions.map((d) => [d.id, 'package'])) };
|
|
202
|
+
const local = await loadDecisionsCached({ root, dir: absDir, all });
|
|
203
|
+
const merged = new Map(pkg.decisions.map((d) => [d.id, d]));
|
|
204
|
+
const origins = Object.fromEntries(pkg.decisions.map((d) => [d.id, 'package']));
|
|
205
|
+
for (const d of local.decisions) {
|
|
206
|
+
merged.set(d.id, d);
|
|
207
|
+
origins[d.id] = 'repo';
|
|
208
|
+
}
|
|
209
|
+
return { ok: pkg.ok && local.ok, decisions: [...merged.values()], errors: [...pkg.errors, ...local.errors], origins };
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
/** The catalogue both surfaces answer from: every rule, its decisions, and the selection. */
|
|
@@ -217,20 +240,22 @@ export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
|
217
240
|
const { rules } = await load(root);
|
|
218
241
|
if (/^ADR-\d{3,}$/i.test(what)) {
|
|
219
242
|
const id = what.toUpperCase();
|
|
220
|
-
const { ok, decisions, errors } = await loadDecisionsFor(root, all);
|
|
243
|
+
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, all);
|
|
221
244
|
if (!ok) {
|
|
222
245
|
rulesFailure(errors[0], 'the decision records must be readable for provenance to work', EXIT.CONFIG);
|
|
223
246
|
}
|
|
224
|
-
const record = decisions.find((x) => x.id === id);
|
|
247
|
+
const record = decisions.find((x) => x.id === id) ?? null;
|
|
225
248
|
// Was 'list them with: ls docs/decisions/' - a path relative to simt-dev's own
|
|
226
249
|
// tree, which resolves to nothing in the repo the reader is standing in. The
|
|
227
|
-
// remedy must be a command they can run
|
|
228
|
-
|
|
250
|
+
// remedy must be a command they can run (Q34: the decisions list, not the
|
|
251
|
+
// rules list - the reader asked about a decision, not a rule).
|
|
252
|
+
if (!record) rulesFailure(`no decision record ${id}`, `list the known decisions with: ${CLI} rules decisions`, EXIT.USAGE);
|
|
229
253
|
const replacement = record.supersededBy ? decisions.find((x) => x.id === record.supersededBy) : null;
|
|
230
254
|
return {
|
|
231
255
|
direction: 'decision',
|
|
232
256
|
id,
|
|
233
|
-
adr: record,
|
|
257
|
+
adr: { ...record, origin: origins[id] ?? 'package' },
|
|
258
|
+
origins,
|
|
234
259
|
supersededBy: replacement ? { id: replacement.id, title: replacement.title } : null,
|
|
235
260
|
inForce: replacement
|
|
236
261
|
? { id: replacement.id, status: replacement.status, summary: replacement.summary }
|
|
@@ -247,7 +272,7 @@ export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
|
247
272
|
EXIT.USAGE,
|
|
248
273
|
);
|
|
249
274
|
}
|
|
250
|
-
const { ok, decisions, errors } = await loadDecisionsFor(root, all);
|
|
275
|
+
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, all);
|
|
251
276
|
if (!ok) {
|
|
252
277
|
rulesFailure(errors[0], 'the decision records must be readable for provenance to work', EXIT.CONFIG);
|
|
253
278
|
}
|
|
@@ -261,9 +286,10 @@ export async function rulesWhyFacts(root, what, { all = false } = {}) {
|
|
|
261
286
|
title: rule.title ?? null,
|
|
262
287
|
source: rule.source,
|
|
263
288
|
origin: rule.origin,
|
|
264
|
-
adrs: cited.map((c) => index.get(c) ?? { id: c, missing: true }),
|
|
289
|
+
adrs: cited.map((c) => (index.has(c) ? { ...index.get(c), origin: origins[c] ?? 'package' } : { id: c, missing: true })),
|
|
265
290
|
uncited: cited.length === 0,
|
|
266
291
|
others: others.map((r) => r.id),
|
|
292
|
+
origins,
|
|
267
293
|
};
|
|
268
294
|
}
|
|
269
295
|
|
|
@@ -351,10 +377,11 @@ function ruleDirection(facts, flags) {
|
|
|
351
377
|
lines.push('', `settled by ${facts.adrs.length} decision${facts.adrs.length === 1 ? '' : 's'}`);
|
|
352
378
|
for (const d of facts.adrs) {
|
|
353
379
|
if (d.missing) {
|
|
354
|
-
lines.push(` ${d.id} MISSING - the rule cites it, but this installation has
|
|
380
|
+
lines.push(` ${d.id} MISSING - the rule cites it, but neither this repo nor this installation has a record of it`);
|
|
381
|
+
lines.push(` remedy: list the known decisions with: ${CLI} rules decisions`);
|
|
355
382
|
continue;
|
|
356
383
|
}
|
|
357
|
-
lines.push(` ${d.id} ${d.title}`);
|
|
384
|
+
lines.push(` ${d.id} [${d.origin ?? 'package'}] ${d.title}`);
|
|
358
385
|
lines.push(` ${d.status}`);
|
|
359
386
|
if (d.supersededBy) {
|
|
360
387
|
// Never print a reversed ruling as if it were current.
|
|
@@ -379,6 +406,7 @@ function adrDirection(facts, flags) {
|
|
|
379
406
|
const lines = [
|
|
380
407
|
`${d.id} - ${d.title}`,
|
|
381
408
|
'',
|
|
409
|
+
`origin ${d.origin ?? 'package'}${d.origin === 'repo' ? ' (this repository overlays the shipped record of the same number)' : ''}`,
|
|
382
410
|
`status ${d.status}`,
|
|
383
411
|
`restates ${restates.length > 0 ? restates.join(', ') : '(no rule cites it)'}`,
|
|
384
412
|
];
|
|
@@ -462,4 +490,50 @@ function adrDirection(facts, flags) {
|
|
|
462
490
|
// A superseded record is not a failure; it is the supersession rule working.
|
|
463
491
|
// The caller asked a question and got a true answer.
|
|
464
492
|
return EXIT.OK;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// --- decisions ------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* `simt rules decisions`: every known decision record, both layers.
|
|
499
|
+
*
|
|
500
|
+
* The shipped records underlay the repository's own, which win on a number
|
|
501
|
+
* collision (review 0003 Q34). The column that matters is `origin`: a reader in
|
|
502
|
+
* a consumer repository must be able to tell the package's ADR-001 from their
|
|
503
|
+
* own, because they are two records that happen to share a number.
|
|
504
|
+
*/
|
|
505
|
+
export async function decisionsCommand(args, flags) {
|
|
506
|
+
const root = flags.root ?? null;
|
|
507
|
+
const { ok, decisions, errors, origins } = await loadDecisionsFor(root, flags.all === true);
|
|
508
|
+
if (!ok) {
|
|
509
|
+
return fail(errors[0], EXIT.CONFIG, 'the decision records must be readable to list them');
|
|
510
|
+
}
|
|
511
|
+
const rows = [...decisions]
|
|
512
|
+
.sort((a, b) => Number(a.id.replace(/^ADR-/i, '')) - Number(b.id.replace(/^ADR-/i, '')))
|
|
513
|
+
.map((d) => ({
|
|
514
|
+
id: d.id,
|
|
515
|
+
title: d.title,
|
|
516
|
+
status: d.status,
|
|
517
|
+
origin: origins[d.id] ?? 'package',
|
|
518
|
+
supersededBy: d.supersededBy ?? null,
|
|
519
|
+
file: d.file,
|
|
520
|
+
}));
|
|
521
|
+
|
|
522
|
+
if (isJson(flags)) {
|
|
523
|
+
emitJson({ ok: true, command: 'rules decisions', root, decisions: rows });
|
|
524
|
+
return EXIT.OK;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const repo = rows.filter((r) => r.origin === 'repo').length;
|
|
528
|
+
const lines = [
|
|
529
|
+
`${rows.length} decisions${root && repo > 0 ? ` (${repo} from this repo overlay the shipped set)` : ''}`,
|
|
530
|
+
'',
|
|
531
|
+
'id origin title',
|
|
532
|
+
];
|
|
533
|
+
for (const r of rows) {
|
|
534
|
+
const head = r.status.length > 80 ? `${r.status.slice(0, 80)}...` : r.status;
|
|
535
|
+
lines.push(`${r.id.padEnd(8)} ${r.origin.padEnd(7)} ${r.title} — ${head}`);
|
|
536
|
+
}
|
|
537
|
+
emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
|
|
538
|
+
return EXIT.OK;
|
|
465
539
|
}
|
package/src/commands/skills.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// token instead of guessing. Suppressing it is how a discovery tool becomes a
|
|
10
10
|
// black box.
|
|
11
11
|
|
|
12
|
-
import { readFile } from 'node:fs/promises';
|
|
12
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
|
|
@@ -20,6 +20,7 @@ import { unknownSkills, scoreCorpus } from '../resolve/eval.mjs';
|
|
|
20
20
|
import { PACKAGE_ROOT, resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
21
21
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
22
22
|
import { tokenize, rankSkills, closure, selectWithinBudget, resolveAlias, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
|
|
23
|
+
import { localSkillNodes } from '../resolve/local.mjs';
|
|
23
24
|
import { nearMatches } from '../resolve/near.mjs';
|
|
24
25
|
import { CLI } from '../name.mjs';
|
|
25
26
|
import { parsePathList } from '../argv.mjs';
|
|
@@ -75,18 +76,25 @@ async function skillsRegistry(args) {
|
|
|
75
76
|
return registry;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The named skill: the registry first, then the repo's own skills, because a skill
|
|
81
|
+
* the repo installed is addressable by the same name (plan 0005 S3). The local lookup
|
|
82
|
+
* costs a fingerprint plus a directory scan, so it runs only when the registry did
|
|
83
|
+
* not answer.
|
|
84
|
+
*/
|
|
85
|
+
async function findSkill(registry, root, name, verb) {
|
|
79
86
|
if (!name) skillsFailure(`${verb} needs a skill name`, `usage: ${CLI} skills ${verb} <name>`, EXIT.USAGE);
|
|
80
87
|
const skill = registry.skills.find((s) => s.name === name);
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
if (skill) return skill;
|
|
89
|
+
const repoStack = (await fingerprint(root)).signals;
|
|
90
|
+
const local = (await localSkillNodes(root, registry, repoStack)).find((s) => s.name === name);
|
|
91
|
+
if (local) return local;
|
|
92
|
+
const near = nearMatches(name, registry.skills.map((s) => s.name));
|
|
93
|
+
skillsFailure(
|
|
94
|
+
`no skill named "${name}"${near.length ? ` (did you mean ${near.join(', ')}?)` : ''}`,
|
|
95
|
+
`run: ${CLI} skills find <query>`,
|
|
96
|
+
EXIT.USAGE,
|
|
97
|
+
);
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
/** The JSON-safe skill facts both surfaces spread: absent lists read as empty, not missing. */
|
|
@@ -116,6 +124,10 @@ export async function skillsBriefFacts(root, args = {}) {
|
|
|
116
124
|
const task = args.task ?? null;
|
|
117
125
|
const repoFacts = { root, stack: repoStack, facts: repo.facts };
|
|
118
126
|
|
|
127
|
+
// The repo's own skills rank alongside the registry, so every command agrees on
|
|
128
|
+
// the names a reader in this repo can address (plan 0005 S3).
|
|
129
|
+
const pool = [...registry.skills, ...(await localSkillNodes(root, registry, repoStack))];
|
|
130
|
+
|
|
119
131
|
// A brief with no task and no paths resolves on stack alone, which produces a
|
|
120
132
|
// plausible-looking list the agent may act on. It warns rather than silently
|
|
121
133
|
// answering; under `--json` the warning is a field, not prose on stdout.
|
|
@@ -125,7 +137,7 @@ export async function skillsBriefFacts(root, args = {}) {
|
|
|
125
137
|
'pass --task "<what you are doing>" or stage files so the diff drives selection, ' +
|
|
126
138
|
'or pass --all to take everything for this stack.';
|
|
127
139
|
if (args.all) {
|
|
128
|
-
const usable =
|
|
140
|
+
const usable = pool.filter((s) => stackCompatible(s, repoStack));
|
|
129
141
|
return {
|
|
130
142
|
kind: 'all',
|
|
131
143
|
repoStack,
|
|
@@ -153,12 +165,12 @@ export async function skillsBriefFacts(root, args = {}) {
|
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
const context = { repoStack, taskTokens, paths };
|
|
156
|
-
const { selected, excluded } = rankSkills(
|
|
168
|
+
const { selected, excluded } = rankSkills(pool, context, { forced });
|
|
157
169
|
|
|
158
170
|
// Hard dependencies are pulled in even when they did not score, and even past
|
|
159
171
|
// budget: a skill that is useless without its dependency is worse than a large
|
|
160
172
|
// context, because it silently misleads.
|
|
161
|
-
const expanded = closure(selected,
|
|
173
|
+
const expanded = closure(selected, pool);
|
|
162
174
|
const { picked, deferred, used: inlineBytes } = selectWithinBudget(expanded, {
|
|
163
175
|
budget,
|
|
164
176
|
all: args.all === true,
|
|
@@ -216,31 +228,53 @@ export async function skillsBriefFacts(root, args = {}) {
|
|
|
216
228
|
};
|
|
217
229
|
}
|
|
218
230
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Raw search across capability, keyword and summary.
|
|
233
|
+
*
|
|
234
|
+
* Local nodes are admitted on `score > 0`, not the `selectionVerdict` rule the
|
|
235
|
+
* registry nodes keep: that rule exists to compensate for find having no repo, and
|
|
236
|
+
* a local skill has one - its presence in the repo is the scoping signal the rule
|
|
237
|
+
* stands in for.
|
|
238
|
+
*/
|
|
239
|
+
export async function skillsFindFacts(root, query, args = {}) {
|
|
221
240
|
const q = String(query ?? '').trim();
|
|
222
241
|
if (!q) skillsFailure('find needs a query', `usage: ${CLI} skills find <query>`, EXIT.USAGE);
|
|
223
242
|
const registry = await skillsRegistry(args);
|
|
224
243
|
const tokens = tokenize(q);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
.
|
|
228
|
-
|
|
244
|
+
const context = { repoStack: [], taskTokens: tokens, paths: [] };
|
|
245
|
+
const results = [
|
|
246
|
+
...registry.skills
|
|
247
|
+
.map((skill) => ({ skill, ...scoreSkill(skill, context) }))
|
|
248
|
+
.filter((r) => selectionVerdict(r.evidence, r.reasons) === null),
|
|
249
|
+
...(await localSkillNodes(root, registry, (await fingerprint(root)).signals))
|
|
250
|
+
.map((skill) => ({ skill, ...scoreSkill(skill, context) }))
|
|
251
|
+
.filter((r) => r.score > 0),
|
|
252
|
+
].sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
|
|
229
253
|
return {
|
|
230
254
|
query: q,
|
|
231
|
-
results: results.map((r) => ({ name: r.skill.name, score: r.score, summary: r.skill.summary, reasons: r.reasons })),
|
|
255
|
+
results: results.map((r) => ({ name: r.skill.name, score: r.score, bytes: r.skill.bytes, summary: r.skill.summary, reasons: r.reasons })),
|
|
232
256
|
};
|
|
233
257
|
}
|
|
234
258
|
|
|
235
259
|
/** Tier 2: one skill body and the metadata needed to judge it. */
|
|
236
260
|
export async function skillsShowFacts(root, name, args = {}) {
|
|
237
261
|
const registry = await skillsRegistry(args);
|
|
238
|
-
const skill = findSkill(registry, name, 'show');
|
|
262
|
+
const skill = await findSkill(registry, root, name, 'show');
|
|
239
263
|
const { note: workspace, root: workspaceRoot } = await announceWorkspace(args);
|
|
240
264
|
// The repo is a location for a declared source too, so `show` reads a body the
|
|
241
265
|
// caller already has instead of reporting the body missing.
|
|
242
266
|
const body = await sourcePath(skill, workspaceRoot, root);
|
|
243
|
-
|
|
267
|
+
// A local file is canonicalized by realpath, while a declared repo source keeps the
|
|
268
|
+
// caller's spelling: on a symlinked prefix (macOS `/var`) the label must accept the
|
|
269
|
+
// canonical root too, or a repo body is mislabelled `workspace`.
|
|
270
|
+
let realRoot = root;
|
|
271
|
+
try {
|
|
272
|
+
realRoot = await realpath(root);
|
|
273
|
+
} catch {
|
|
274
|
+
/* root does not exist; the label falls back to the un-canonical comparison */
|
|
275
|
+
}
|
|
276
|
+
const bodyFrom =
|
|
277
|
+
body === null ? 'none' : body.startsWith(`${root}/`) || body.startsWith(`${realRoot}/`) ? 'repo' : 'workspace';
|
|
244
278
|
return {
|
|
245
279
|
skill: skillFacts(skill),
|
|
246
280
|
workspace,
|
|
@@ -253,7 +287,7 @@ export async function skillsShowFacts(root, name, args = {}) {
|
|
|
253
287
|
/** The signal chain for one skill in one repository. */
|
|
254
288
|
export async function skillsWhyFacts(root, name, args = {}) {
|
|
255
289
|
const registry = await skillsRegistry(args);
|
|
256
|
-
const skill = findSkill(registry, name, 'why');
|
|
290
|
+
const skill = await findSkill(registry, root, name, 'why');
|
|
257
291
|
const repo = await fingerprint(root);
|
|
258
292
|
const paths = args.paths ? parsePathList(args.paths) : await dirtyPaths(root);
|
|
259
293
|
const tokens = tokenize(args.task ?? '');
|
|
@@ -402,9 +436,10 @@ async function emitAllFacts(facts, flags) {
|
|
|
402
436
|
// --- find ---------------------------------------------------------------------
|
|
403
437
|
|
|
404
438
|
export async function findCommand(args, flags) {
|
|
439
|
+
const root = flags.root ?? process.cwd();
|
|
405
440
|
let facts;
|
|
406
441
|
try {
|
|
407
|
-
facts = await skillsFindFacts(args.join(' '), flags);
|
|
442
|
+
facts = await skillsFindFacts(root, args.join(' '), flags);
|
|
408
443
|
} catch (error) {
|
|
409
444
|
return fail(error.message, error.exitCode ?? EXIT.FAIL, error.remedy);
|
|
410
445
|
}
|
|
@@ -20,7 +20,8 @@ import { resolveVaultDir } from '../bench/mine.mjs';
|
|
|
20
20
|
import { locateSource, sourceHeldLocally } from '../resolve/source.mjs';
|
|
21
21
|
import { nearMatches } from '../resolve/near.mjs';
|
|
22
22
|
import { resolveWorkspace, WORKSPACE_REMEDY } from '../workspace.mjs';
|
|
23
|
-
import { CLI
|
|
23
|
+
import { CLI } from '../name.mjs';
|
|
24
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
24
25
|
|
|
25
26
|
async function readSkillRawText(skill, workspaceRoot, root) {
|
|
26
27
|
const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
|
|
@@ -44,7 +45,7 @@ async function readSkillRawText(skill, workspaceRoot, root) {
|
|
|
44
45
|
|
|
45
46
|
async function loadGoldPathsByTask(root) {
|
|
46
47
|
const map = new Map();
|
|
47
|
-
const tasksDir = join(root,
|
|
48
|
+
const tasksDir = join(root, '.simt', 'bench', 'tasks');
|
|
48
49
|
const vaultRoot = resolveVaultDir(root);
|
|
49
50
|
const entries = (await readdir(tasksDir).catch(() => [])).filter((f) => f.endsWith('.json')).sort();
|
|
50
51
|
for (const entry of entries) {
|
|
@@ -172,26 +173,26 @@ async function runSingleSkillCompile(skill, registry, { root, workspaceRoot, fla
|
|
|
172
173
|
triggerStats,
|
|
173
174
|
});
|
|
174
175
|
|
|
175
|
-
const outDir = join(root,
|
|
176
|
-
const proposalPath = join(root,
|
|
176
|
+
const outDir = join(root, LOCAL_DIR, 'compiled', 'skills', skill.name);
|
|
177
|
+
const proposalPath = join(root, LOCAL_DIR, 'proposals', 'skills', `${skill.name}.json`);
|
|
177
178
|
const writtenFiles = [];
|
|
178
179
|
|
|
179
180
|
if (mode.apply) {
|
|
180
181
|
await mkdir(outDir, { recursive: true });
|
|
181
182
|
await writeFile(join(outDir, 'SKILL.md'), compiled.mainText, 'utf8');
|
|
182
|
-
writtenFiles.push(`${
|
|
183
|
+
writtenFiles.push(`${LOCAL_DIR}/compiled/skills/${skill.name}/SKILL.md`);
|
|
183
184
|
|
|
184
185
|
for (const ref of compiled.references) {
|
|
185
186
|
if (!ref.content) continue;
|
|
186
187
|
const refFull = join(outDir, ref.path);
|
|
187
188
|
await mkdir(dirname(refFull), { recursive: true });
|
|
188
189
|
await writeFile(refFull, ref.content, 'utf8');
|
|
189
|
-
writtenFiles.push(`${
|
|
190
|
+
writtenFiles.push(`${LOCAL_DIR}/compiled/skills/${skill.name}/${ref.path}`);
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
await mkdir(dirname(proposalPath), { recursive: true });
|
|
193
194
|
await writeFile(proposalPath, `${JSON.stringify(proposal, null, 2)}\n`, 'utf8');
|
|
194
|
-
writtenFiles.push(`${
|
|
195
|
+
writtenFiles.push(`${LOCAL_DIR}/proposals/skills/${skill.name}.json`);
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
if (isJson(flags)) {
|
|
@@ -89,6 +89,18 @@ export async function installCommand(args, flags) {
|
|
|
89
89
|
|
|
90
90
|
const found = await locateSource(pick.source, { workspaceRoot: ws.root, root });
|
|
91
91
|
if (!found.path) {
|
|
92
|
+
// A repo-side candidate with the wrong size is a third reason, and it needs
|
|
93
|
+
// its own wording: the file is on the reader's disk, so "this repo does not
|
|
94
|
+
// hold it" would send them looking for a file that is already there. The
|
|
95
|
+
// variant path calls the same failure an impostor (chooseSource).
|
|
96
|
+
const impostor = found.candidates.find((c) => c.how === 'repo');
|
|
97
|
+
if (impostor) {
|
|
98
|
+
return fail(
|
|
99
|
+
`${skill.name}: ${root}/${pick.source.path} exists but is not the declared body (${pick.source.bytes} bytes declared)`,
|
|
100
|
+
EXIT.USAGE,
|
|
101
|
+
`run: ${CLI} graph lint --workspace <path> to see what the registry expects, or delete the local copy`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
92
104
|
// Two reasons land here and they need different wording: no workspace at all,
|
|
93
105
|
// or a workspace that should hold this body and does not. An install that
|
|
94
106
|
// cannot read a body has failed, so this is never a degraded mode - the
|