simt-dev 0.6.2 → 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 +83 -1
- package/README.md +46 -17
- package/agents/simt-agent.md +84 -0
- package/bin/simt.mjs +19 -6
- 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-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- 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 +53 -0
- package/package.json +4 -2
- package/rules/core.md +2 -2
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/mdc/test-gate.mdc +2 -0
- package/rules/stacks/android.md +10 -11
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/connectiq.md +2 -2
- package/rules/stacks/container.md +2 -0
- 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 +4 -3
- package/rules/stacks/node.md +2 -2
- package/rules/stacks/python.md +7 -6
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- 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 +238 -15
- package/src/agenthooks/opencode.mjs +72 -0
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/prompt.mjs +6 -2
- package/src/agenthooks/render.mjs +186 -19
- package/src/agenthooks/shell.mjs +523 -0
- package/src/agenthooks/shim.mjs +55 -0
- package/src/agents/render.mjs +277 -0
- package/src/argv.mjs +62 -9
- 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 +133 -17
- package/src/commands/bench.mjs +2 -1
- package/src/commands/ci.mjs +28 -3
- package/src/commands/compare.mjs +8 -7
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +20 -6
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/evidence.mjs +13 -3
- package/src/commands/explain.mjs +52 -0
- package/src/commands/gate.mjs +85 -19
- package/src/commands/graph.mjs +18 -5
- package/src/commands/help.mjs +96 -26
- package/src/commands/hooks.mjs +258 -67
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/impact.mjs +4 -2
- package/src/commands/index.mjs +69 -102
- package/src/commands/init.mjs +239 -37
- package/src/commands/lease.mjs +304 -0
- package/src/commands/lessons.mjs +77 -9
- package/src/commands/map.mjs +33 -18
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +17 -2
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +4 -0
- package/src/commands/rules.mjs +219 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +329 -206
- package/src/commands/skillscompile.mjs +8 -7
- package/src/commands/skillsinstall.mjs +112 -30
- package/src/commands/slice.mjs +712 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +146 -36
- package/src/commands/tests.mjs +5 -2
- package/src/commands/undo.mjs +114 -0
- package/src/commands/upgrade.mjs +103 -9
- 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 +196 -0
- package/src/decisions/dir.mjs +84 -0
- package/src/decisions/index.mjs +109 -11
- package/src/diff.mjs +254 -0
- package/src/distill/distill.mjs +3 -1
- package/src/ecosystem/node-runner.mjs +144 -0
- package/src/explain.mjs +270 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +52 -24
- package/src/hooks/capability.mjs +14 -1
- package/src/hooks/dispatcher.mjs +441 -9
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +23 -3
- package/src/hooks/shim.mjs +12 -7
- package/src/identity.mjs +67 -14
- package/src/ignore.mjs +133 -0
- package/src/index/extract.mjs +192 -11
- package/src/index/persist.mjs +372 -55
- package/src/index/store.mjs +263 -60
- package/src/index/tests.mjs +6 -35
- package/src/journal.mjs +545 -0
- 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 +195 -0
- package/src/ledger/index.mjs +91 -25
- package/src/mcp/resources.mjs +5 -3
- package/src/mcp/server.mjs +86 -13
- package/src/mcp/tools.mjs +340 -264
- package/src/memory/decide.mjs +98 -38
- package/src/memory/lessons.mjs +180 -33
- package/src/name.mjs +9 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +37 -8
- package/src/plan/template.mjs +27 -5
- package/src/plugins.mjs +33 -8
- package/src/prbody.mjs +253 -0
- 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 +82 -21
- 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/shared.mjs +4 -1
- 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 +374 -0
- package/src/state.mjs +76 -0
- package/src/status.mjs +157 -0
- package/src/toolwords.mjs +5 -0
- package/src/tsconfig.mjs +39 -99
- package/templates/plan/tracker.json +1 -1
package/src/projection/rules.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import { filterInferable, inferableDefect, inferableProofs } from '../kernel/inf
|
|
|
37
37
|
import { checkDefect, foldChecks } from '../kernel/checks.mjs';
|
|
38
38
|
import { foldVariants, readLockedVariants, variantsDefect } from '../kernel/variants.mjs';
|
|
39
39
|
import { BASELINES } from './baselines.mjs';
|
|
40
|
-
import { CLI } from '../name.mjs';
|
|
40
|
+
import { CLI, MANIFEST_FILE } from '../name.mjs';
|
|
41
41
|
import { installedSkills } from './disclosure.mjs';
|
|
42
42
|
import { PACK_DIR, resolvePacks } from '../packs.mjs';
|
|
43
43
|
import { discoverPackages } from '../monorepo.mjs';
|
|
@@ -134,6 +134,10 @@ export async function provenanceInputs() {
|
|
|
134
134
|
async function walk(dir, prefix = '') {
|
|
135
135
|
const out = [];
|
|
136
136
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
137
|
+
// A dotfile is machine noise (.DS_Store on macOS), not rule content: the digest
|
|
138
|
+
// must be stable across checkouts, and a fresh worktree does not carry the
|
|
139
|
+
// author's Finder metadata. Rule files are .md/.mdc, never dotfiles.
|
|
140
|
+
if (entry.name.startsWith('.')) continue;
|
|
137
141
|
const rel = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
138
142
|
if (entry.isDirectory()) out.push(...(await walk(join(dir, entry.name), rel)));
|
|
139
143
|
else if (entry.isFile()) out.push(rel);
|
|
@@ -282,8 +286,11 @@ export async function mdcTargets(root, sha, manifest, flags) {
|
|
|
282
286
|
// same `bodies` from the same call, so the region and the files cannot disagree.
|
|
283
287
|
if (foldsRules(resolved) && !resolved.surfaces.some((s) => s.rule)) {
|
|
284
288
|
if (bodies.length > 0) {
|
|
289
|
+
// An empty selection has no row to read the primary from; the document is the
|
|
290
|
+
// one the region lives in, which is the answer in every case this note fires.
|
|
291
|
+
const primary = resolved.surfaces[0]?.primary ?? 'AGENTS.md';
|
|
285
292
|
notes.push(
|
|
286
|
-
`every rule is folded into ${
|
|
293
|
+
`every rule is folded into ${primary}; add a surface with its own rule path for a file per rule`,
|
|
287
294
|
);
|
|
288
295
|
}
|
|
289
296
|
return { targets, problems, notes };
|
|
@@ -411,6 +418,7 @@ async function ruleBodies(root, manifest, flags) {
|
|
|
411
418
|
const { body, problems: renderProblems } = renderMdc(
|
|
412
419
|
rule,
|
|
413
420
|
ruleValues(rule, declared.values[rule.id] ?? {}, resolved.surfaces),
|
|
421
|
+
signals,
|
|
414
422
|
);
|
|
415
423
|
for (const p of renderProblems) problems.push(`${rule.file}: ${p}`);
|
|
416
424
|
if (renderProblems.length > 0) continue;
|
|
@@ -558,6 +566,16 @@ export async function desiredRegion(root, flags) {
|
|
|
558
566
|
for (const x of filtered.removed) inferable.push({ rule: r.id, text: x.text, expr: x.expr });
|
|
559
567
|
shipped.push({ ...r, body: folded.body });
|
|
560
568
|
}
|
|
569
|
+
// A body that points the reader at a profile they cannot reach is a rule-text defect:
|
|
570
|
+
// the region renders, but the reference dangles. Kept out of `problems` because that
|
|
571
|
+
// array is the mdc list `sync` already reports, and one fault must not print twice.
|
|
572
|
+
const crossRefs = [];
|
|
573
|
+
const selectedIds = new Set(selected.map((r) => r.id));
|
|
574
|
+
for (const r of shipped) {
|
|
575
|
+
for (const p of crossRefProblems(r.body, all, selectedIds)) {
|
|
576
|
+
crossRefs.push(`${r.id}: ${p}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
561
579
|
const lockedNonDefault = variantsSelected.filter((v) => v.locked && v.winner !== v.baseline);
|
|
562
580
|
if (lockedNonDefault.length > 0) {
|
|
563
581
|
notes.push(
|
|
@@ -906,6 +924,7 @@ export async function desiredRegion(root, flags) {
|
|
|
906
924
|
surfaces: resolved.ids,
|
|
907
925
|
surfaceSource: resolved.source,
|
|
908
926
|
problems,
|
|
927
|
+
crossRefs,
|
|
909
928
|
fallback,
|
|
910
929
|
notes,
|
|
911
930
|
body: assembled.body,
|
|
@@ -1301,9 +1320,13 @@ export function byPriorityThenId(a, b) {
|
|
|
1301
1320
|
* `Applies to` is already the first line of a stack profile once its H1 is gone,
|
|
1302
1321
|
* so it lands directly under `## Stack: …` instead of under whatever section the
|
|
1303
1322
|
* previous source ended on.
|
|
1323
|
+
*
|
|
1324
|
+
* The rule text names the manifest with a `{manifest}` token and the render
|
|
1325
|
+
* substitutes it, so a rename of the file is a change in one place (MANIFEST_FILE),
|
|
1326
|
+
* not a search across every rule body.
|
|
1304
1327
|
*/
|
|
1305
1328
|
function frameSource(rule, body, kind) {
|
|
1306
|
-
const text = demoteHeadings(body).trim();
|
|
1329
|
+
const text = demoteHeadings(body.replaceAll('{manifest}', MANIFEST_FILE)).trim();
|
|
1307
1330
|
if (!text) return '';
|
|
1308
1331
|
const title = sourceTitle(rule, kind);
|
|
1309
1332
|
if (!title) return text;
|
|
@@ -1317,6 +1340,34 @@ function sourceTitle(rule, kind) {
|
|
|
1317
1340
|
return `Stack: ${rule.id}`;
|
|
1318
1341
|
}
|
|
1319
1342
|
|
|
1343
|
+
/**
|
|
1344
|
+
* A body may point the reader at another profile in two ways: naming a profile
|
|
1345
|
+
* that is selected alongside it, or giving the command to fetch one
|
|
1346
|
+
* (`simt rules show <id>`). A reference that is neither is one the reader of
|
|
1347
|
+
* the projected region cannot follow. Both citation forms are checked: the
|
|
1348
|
+
* backtick-quoted `<id> profile` in prose, and the `simt rules show <id>`
|
|
1349
|
+
* command itself - an id that form names must exist, or the command 404s.
|
|
1350
|
+
*/
|
|
1351
|
+
function crossRefProblems(body, all, selectedIds) {
|
|
1352
|
+
const problems = [];
|
|
1353
|
+
const seen = new Set();
|
|
1354
|
+
for (const m of String(body).matchAll(/`([a-z0-9][a-z0-9-]*)`\s+profile|rules show ([a-z0-9][a-z0-9-]*)/g)) {
|
|
1355
|
+
const id = m[1] ?? m[2];
|
|
1356
|
+
if (seen.has(id)) continue;
|
|
1357
|
+
seen.add(id);
|
|
1358
|
+
if (!all.some((r) => r.id === id)) {
|
|
1359
|
+
problems.push(`cross-reference: body cites the \`${id}\` profile, which is not a declared rule`);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (!selectedIds.has(id) && !new RegExp(`${CLI} rules show ${id}`).test(body)) {
|
|
1363
|
+
problems.push(
|
|
1364
|
+
`cross-reference: body cites the \`${id}\` profile, which is neither co-selected nor reachable via \`${CLI} rules show ${id}\``,
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return problems;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1320
1371
|
/** `##` becomes `###`. A fence is left alone: a heading in an example is an example. */
|
|
1321
1372
|
function demoteHeadings(text) {
|
|
1322
1373
|
let fence = false;
|
|
@@ -307,7 +307,10 @@ export async function sharedState(path, rules, meta) {
|
|
|
307
307
|
* user-level AGENTS.md is the most likely file in a reader's home directory to contain
|
|
308
308
|
* something they wrote themselves. Only the bytes between the markers are ours.
|
|
309
309
|
*/
|
|
310
|
-
|
|
310
|
+
// Exported so `shared install --diff` previews the same bytes the write applies:
|
|
311
|
+
// a diff rendered from a second implementation of this injection would be a
|
|
312
|
+
// second opinion about what "current" means (plan 0003 R50).
|
|
313
|
+
export function injectShared(before, rules, meta) {
|
|
311
314
|
return inject(before, sharedBody(rules), meta, { compose: composeShared });
|
|
312
315
|
}
|
|
313
316
|
|
|
@@ -327,12 +327,20 @@ export const DEFAULT_SURFACES = ['opencode'];
|
|
|
327
327
|
* Cursor reader gets them as `.mdc`. Folding and per-rule files therefore coexist,
|
|
328
328
|
* and `ruleTargets` already skips a surface that has no rule path.
|
|
329
329
|
*
|
|
330
|
+
* An empty selection folds too: with no surface carrying a rule path, the region is
|
|
331
|
+
* the only channel a rule has, and the old answer (no surface folds, so nothing
|
|
332
|
+
* folds) wrote every process rule nowhere while `check` reported current (review
|
|
333
|
+
* 0003 Q27).
|
|
334
|
+
*
|
|
330
335
|
* Asked in exactly two places that must agree: `desiredRegion` folds the bodies in,
|
|
331
336
|
* and `mdcTargets` writes the files. Asking it twice is how ADR-020's fold decision
|
|
332
337
|
* could drift between the writer and the gate, which is the step-6 failure.
|
|
333
338
|
*/
|
|
334
339
|
export function foldsRules(resolved) {
|
|
335
|
-
|
|
340
|
+
if (resolved.surfaces.length === 0) return true;
|
|
341
|
+
// A surface with no rule path has no per-rule channel, so its rules must be in the
|
|
342
|
+
// region; `foldRules` is that fact recorded on the row.
|
|
343
|
+
return resolved.surfaces.some((s) => s.foldRules === true || !s.rule);
|
|
336
344
|
}
|
|
337
345
|
|
|
338
346
|
/**
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The repository root a working directory belongs to, for the hook path.
|
|
2
|
+
//
|
|
3
|
+
// A payload's `cwd` is the directory the harness happens to be in, and a harness
|
|
4
|
+
// happens to be in subdirectories: an agent that was told to work in `src/` edits
|
|
5
|
+
// from there, and every repository-relative comparison that resolves against the
|
|
6
|
+
// payload's cwd — the lease file, `.gitmodules`, the manifest — compares against
|
|
7
|
+
// the wrong root and silently stops matching (review 0003 Q16). The root this
|
|
8
|
+
// module answers is the one git itself would use, so a lease taken at the top
|
|
9
|
+
// level matches an edit made three directories down.
|
|
10
|
+
//
|
|
11
|
+
// The answer is synchronous because the caller is a hook on the hot path: one
|
|
12
|
+
// `git rev-parse` is cheaper than the repository walk the fallback would do in
|
|
13
|
+
// the common case, and the fallback only pays when git cannot answer at all —
|
|
14
|
+
// a directory that is not in a repository, where the walk is short by
|
|
15
|
+
// definition.
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { dirname, join, resolve } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { MANIFEST_FILE, STATE_DIR } from './name.mjs';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The top-level directory of the repository `cwd` is in, or `cwd` itself when it
|
|
25
|
+
* is in none.
|
|
26
|
+
*
|
|
27
|
+
* The fallback looks for the two facts that mark a directory as adopted by this
|
|
28
|
+
* tool — the state directory and the manifest — and walks up, because a hook
|
|
29
|
+
* fired from a subdirectory of an adopted repository must still find it: a
|
|
30
|
+
* repository with no `.git` at all (a worktree's parent, a fresh checkout
|
|
31
|
+
// before `git init` finishes) is the case the git call refuses and the walk
|
|
32
|
+
* answers.
|
|
33
|
+
*/
|
|
34
|
+
export function repoRoot(cwd) {
|
|
35
|
+
const start = resolve(String(cwd ?? ''));
|
|
36
|
+
const result = spawnSync('git', ['-C', start, 'rev-parse', '--show-toplevel'], {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
timeout: 2000,
|
|
39
|
+
});
|
|
40
|
+
if (result.status === 0) {
|
|
41
|
+
const top = String(result.stdout ?? '').trim();
|
|
42
|
+
if (top !== '') return top;
|
|
43
|
+
}
|
|
44
|
+
let dir = start;
|
|
45
|
+
for (;;) {
|
|
46
|
+
if (existsSync(join(dir, STATE_DIR)) || existsSync(join(dir, MANIFEST_FILE))) return dir;
|
|
47
|
+
const parent = dirname(dir);
|
|
48
|
+
if (parent === dir) return start;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether git on this repository compares paths case-insensitively, which is
|
|
55
|
+
* the one filesystem fact a path comparison must honour: on a case-insensitive
|
|
56
|
+
* volume `NB.ipynb` and `nb.ipynb` are the file, and a comparison that spells
|
|
57
|
+
* them differently is a comparison that misses.
|
|
58
|
+
*
|
|
59
|
+
* A git that cannot answer (no repository, no binary) answers `false`, which is
|
|
60
|
+
* the strict spelling: the two case variants are then treated as distinct, the
|
|
61
|
+
// way they are on the filesystem the comparison is being done for.
|
|
62
|
+
*/
|
|
63
|
+
export function coreIgnoreCase(root) {
|
|
64
|
+
const result = spawnSync('git', ['-C', root, 'config', '--get', 'core.ignorecase'], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 1000,
|
|
67
|
+
});
|
|
68
|
+
return result.status === 0 && String(result.stdout ?? '').trim().toLowerCase() === 'true';
|
|
69
|
+
}
|
package/src/resolve/body.mjs
CHANGED
|
@@ -22,6 +22,10 @@ import { existsSync } from 'node:fs';
|
|
|
22
22
|
import { locateSource, sourceHeldLocally } from './source.mjs';
|
|
23
23
|
|
|
24
24
|
export async function sourcePath(skill, workspaceRoot, root) {
|
|
25
|
+
// A node synthesized from the repo's own skill directory already names its file.
|
|
26
|
+
// Asking the workspace for it would hunt for a body the registry never declared.
|
|
27
|
+
if (skill.localFile) return skill.localFile;
|
|
28
|
+
|
|
25
29
|
const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
|
|
26
30
|
|
|
27
31
|
if (canonical) {
|
|
@@ -180,6 +180,11 @@ const FACTS = [
|
|
|
180
180
|
{ name: 'has-mix', kind: 'file', paths: ['mix.exs'] },
|
|
181
181
|
{ name: 'has-deno-config', kind: 'file', paths: ['deno.json', 'deno.jsonc'] },
|
|
182
182
|
{ name: 'has-bun-lock', kind: 'file', paths: ['bun.lock', 'bun.lockb'] },
|
|
183
|
+
// Q31: one fact per Node lockfile, so the runner resolver, the card, the gate
|
|
184
|
+
// seeds and CI all read the same committed evidence instead of assuming npm.
|
|
185
|
+
{ name: 'has-pnpm-lock', kind: 'file', paths: ['pnpm-lock.yaml'] },
|
|
186
|
+
{ name: 'has-yarn-lock', kind: 'file', paths: ['yarn.lock'] },
|
|
187
|
+
{ name: 'has-package-lock', kind: 'file', paths: ['package-lock.json'] },
|
|
183
188
|
// A glob walk skips dot directories, and `.devcontainer` is one. Stat the path.
|
|
184
189
|
{ name: 'has-devcontainer', kind: 'path', paths: ['.devcontainer/devcontainer.json'] },
|
|
185
190
|
];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Repo-local skills as resolver nodes.
|
|
2
|
+
//
|
|
3
|
+
// The registry is what simt-dev ships; the skill directories a repo holds are what
|
|
4
|
+
// that repo owns. After plan 0005 S2 the registry is small and generic, so a skill
|
|
5
|
+
// that lives only in a repo was invisible to `skills find` and `skills brief`, and
|
|
6
|
+
// the NOT SELECTED block points a reader at `skills show` by name - a remedy that
|
|
7
|
+
// 404s for a name the registry does not declare.
|
|
8
|
+
//
|
|
9
|
+
// Presence in the repo's skill directory is the eligibility signal: a skill the repo
|
|
10
|
+
// installed is about the repo, and the stack it belongs to is the stack the repo has.
|
|
11
|
+
// The node's `stack` is therefore the repo's own signals, which makes the stack gate
|
|
12
|
+
// a self-overlap that always passes, and the stack score reads "more this repo's"
|
|
13
|
+
// rather than "compatible with a stranger".
|
|
14
|
+
|
|
15
|
+
import { stat } from 'node:fs/promises';
|
|
16
|
+
|
|
17
|
+
import { BODY_BYTE_BUDGET } from '../graph/compile.mjs';
|
|
18
|
+
import { installedSkills } from '../projection/disclosure.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The repo's own skills, as nodes the resolver can rank.
|
|
22
|
+
*
|
|
23
|
+
* Names the registry declares are excluded: the registry entry is canonical and the
|
|
24
|
+
* local file is its installed copy, so keeping both would rank one skill twice.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} root the repository
|
|
27
|
+
* @param {object} registry the loaded registry
|
|
28
|
+
* @param {string[]} repoStack the repo's signals (stack ∪ facts)
|
|
29
|
+
* @returns {Promise<object[]>} resolver-ready nodes, name-sorted
|
|
30
|
+
*/
|
|
31
|
+
export async function localSkillNodes(root, registry, repoStack) {
|
|
32
|
+
const installed = await installedSkills(root);
|
|
33
|
+
const known = new Set(registry.skills.map((s) => s.name));
|
|
34
|
+
const nodes = [];
|
|
35
|
+
for (const { name, trigger, file } of installed) {
|
|
36
|
+
if (known.has(name)) continue;
|
|
37
|
+
let size;
|
|
38
|
+
try {
|
|
39
|
+
size = (await stat(file)).size;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code !== 'ENOENT') throw error;
|
|
42
|
+
continue; // gone between listing and measuring
|
|
43
|
+
}
|
|
44
|
+
nodes.push({
|
|
45
|
+
name,
|
|
46
|
+
summary: trigger,
|
|
47
|
+
keywords: [],
|
|
48
|
+
capabilities: [],
|
|
49
|
+
stack: [...repoStack],
|
|
50
|
+
paths: [],
|
|
51
|
+
bytes: size,
|
|
52
|
+
deferred: size > BODY_BYTE_BUDGET,
|
|
53
|
+
localFile: file,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
|
57
|
+
return nodes;
|
|
58
|
+
}
|
package/src/review/critic.mjs
CHANGED
|
@@ -32,9 +32,11 @@ import { digestOf, promptHash, proposalFrontmatter } from '../distill/engine.mjs
|
|
|
32
32
|
|
|
33
33
|
const exec = promisify(execFile);
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
import { LOCAL_DIR } from '../state.mjs';
|
|
36
|
+
|
|
37
|
+
// Locked on-disk beneath machine-local state, like the distilled cache (R38) —
|
|
36
38
|
// proposals keyed by content digest, never projected and never committed.
|
|
37
|
-
const REVIEW_DIR = '
|
|
39
|
+
const REVIEW_DIR = join(LOCAL_DIR, 'reviews');
|
|
38
40
|
const ENGINE_REVIEW_LOCAL = 'review-local';
|
|
39
41
|
|
|
40
42
|
/** The diff brief's byte cap; `--budget` overrides it, `0` means unlimited. */
|