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
package/src/projection/lock.mjs
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// rewritten.
|
|
9
9
|
|
|
10
10
|
import { createHash } from 'node:crypto';
|
|
11
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
11
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
|
|
14
|
+
import { readLockedVariants } from '../kernel/variants.mjs';
|
|
14
15
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
15
16
|
import { desiredRegion, provenanceDigest, LOCAL_PROFILE_DIR } from './rules.mjs';
|
|
16
17
|
import { readManifest } from './manifest.mjs';
|
|
@@ -29,11 +30,25 @@ const MANIFEST_KEYS = ['mode', 'budget', 'shared', 'surfaces', 'gates'];
|
|
|
29
30
|
* body that was written and not a body for a different set of surfaces
|
|
30
31
|
*/
|
|
31
32
|
export async function computeLock(root, flags = {}) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const variants =
|
|
34
|
+
flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
|
|
35
|
+
const desired = await desiredRegion(root, { ...flags, variants });
|
|
36
|
+
const inputs = await inputDigest(root, variants);
|
|
37
|
+
const outputs = createHash('sha256').update(desired.body).digest('hex');
|
|
38
|
+
const hasVariants =
|
|
39
|
+
variants && typeof variants === 'object' && Object.keys(variants).length > 0;
|
|
40
|
+
return hasVariants ? { inputs, outputs, variants } : { inputs, outputs };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Record locked variant winners in `simt-dev.lock.json` alongside updated digests.
|
|
45
|
+
*/
|
|
46
|
+
export async function writeLockVariants(root, nextVariants, flags = {}) {
|
|
47
|
+
const existing = await readLockedVariants(root);
|
|
48
|
+
const merged = { ...existing, ...(nextVariants ?? {}) };
|
|
49
|
+
const lock = await computeLock(root, { ...flags, variants: merged });
|
|
50
|
+
await writeFile(join(root, LOCK_FILE), `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
|
|
51
|
+
return lock;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
/** Read a lock, or null when the file is not there. A file that does not parse is an
|
|
@@ -52,13 +67,19 @@ export async function readLock(root) {
|
|
|
52
67
|
if (typeof lock?.inputs !== 'string' || typeof lock?.outputs !== 'string') {
|
|
53
68
|
return { lock: null, error: 'must be an object with inputs and outputs' };
|
|
54
69
|
}
|
|
70
|
+
if (
|
|
71
|
+
lock.variants !== undefined &&
|
|
72
|
+
(typeof lock.variants !== 'object' || lock.variants === null || Array.isArray(lock.variants))
|
|
73
|
+
) {
|
|
74
|
+
return { lock: null, error: 'variants must be an object' };
|
|
75
|
+
}
|
|
55
76
|
return { lock, error: null };
|
|
56
77
|
} catch (error) {
|
|
57
78
|
return { lock: null, error: error.message };
|
|
58
79
|
}
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
async function inputDigest(root) {
|
|
82
|
+
async function inputDigest(root, variants = {}) {
|
|
62
83
|
const hash = createHash('sha256');
|
|
63
84
|
hash.update(`provenance:${await provenanceDigest()}\n`);
|
|
64
85
|
const { stack, facts } = await fingerprint(root);
|
|
@@ -89,5 +110,16 @@ async function inputDigest(root) {
|
|
|
89
110
|
} catch (error) {
|
|
90
111
|
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
91
112
|
}
|
|
113
|
+
const variantKeys =
|
|
114
|
+
variants && typeof variants === 'object' ? Object.keys(variants).sort() : [];
|
|
115
|
+
if (variantKeys.length > 0) {
|
|
116
|
+
const winners = {};
|
|
117
|
+
for (const key of variantKeys) {
|
|
118
|
+
const entry = variants[key];
|
|
119
|
+
winners[key] =
|
|
120
|
+
typeof entry === 'string' ? entry : (entry?.winner ?? entry?.variant ?? null);
|
|
121
|
+
}
|
|
122
|
+
hash.update(`variants:${JSON.stringify(winners)}\n`);
|
|
123
|
+
}
|
|
92
124
|
return hash.digest('hex');
|
|
93
125
|
}
|
package/src/projection/rules.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { commandCard, CARD_LIMIT_BYTES } from '../kernel/card.mjs';
|
|
|
35
35
|
import { packToBudget } from '../kernel/budget.mjs';
|
|
36
36
|
import { filterInferable, inferableDefect, inferableProofs } from '../kernel/inferability.mjs';
|
|
37
37
|
import { checkDefect, foldChecks } from '../kernel/checks.mjs';
|
|
38
|
+
import { foldVariants, readLockedVariants, variantsDefect } from '../kernel/variants.mjs';
|
|
38
39
|
import { BASELINES } from './baselines.mjs';
|
|
39
40
|
import { CLI } from '../name.mjs';
|
|
40
41
|
import { installedSkills } from './disclosure.mjs';
|
|
@@ -403,12 +404,14 @@ async function ruleBodies(root, manifest, flags) {
|
|
|
403
404
|
for (const p of renderProblems) problems.push(`${rule.file}: ${p}`);
|
|
404
405
|
if (renderProblems.length > 0) continue;
|
|
405
406
|
|
|
407
|
+
const variantFolded = foldVariants(body, rule.id ?? '', flags?.variants ?? null);
|
|
408
|
+
|
|
406
409
|
// One canonical body, one target per selected surface. The body is identical
|
|
407
410
|
// everywhere; only the frontmatter the editor reads before any rule text
|
|
408
411
|
// differs. Without this loop a repo in a Claude Code session receives
|
|
409
412
|
// AGENTS.md and none of the always-on process rules, because those lived only
|
|
410
413
|
// in a directory only Cursor reads.
|
|
411
|
-
bodies.push({ rule, body });
|
|
414
|
+
bodies.push({ rule, body: variantFolded.body });
|
|
412
415
|
}
|
|
413
416
|
|
|
414
417
|
return { resolved, bodies, problems, notes, skipped };
|
|
@@ -449,12 +452,14 @@ function toolRule(rule) {
|
|
|
449
452
|
|
|
450
453
|
export async function desiredRegion(root, flags) {
|
|
451
454
|
const { stack, facts, manifest } = await fingerprint(root);
|
|
455
|
+
const lockedVariants =
|
|
456
|
+
flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
|
|
452
457
|
const all = await loadRules(root);
|
|
453
458
|
const { selected, fallback, notes } = select(all, stack, facts);
|
|
454
459
|
const { resolved, bodies, problems, notes: ruleNotes } = await ruleBodies(
|
|
455
460
|
root,
|
|
456
461
|
manifest,
|
|
457
|
-
flags ?? {},
|
|
462
|
+
{ ...(flags ?? {}), variants: lockedVariants },
|
|
458
463
|
);
|
|
459
464
|
notes.push(...ruleNotes);
|
|
460
465
|
|
|
@@ -476,7 +481,8 @@ export async function desiredRegion(root, flags) {
|
|
|
476
481
|
// selected surface cannot narrow, the file would be the only home for a rule that
|
|
477
482
|
// reader cannot reach — so it stays, and says why.
|
|
478
483
|
if (narrowing) {
|
|
479
|
-
|
|
484
|
+
const vf = foldVariants(r.body, r.id ?? '', lockedVariants);
|
|
485
|
+
scoped.push(vf.changed ? { ...r, body: vf.body } : r);
|
|
480
486
|
continue;
|
|
481
487
|
}
|
|
482
488
|
inRegion.push(r);
|
|
@@ -521,18 +527,27 @@ export async function desiredRegion(root, flags) {
|
|
|
521
527
|
// which lines the reader already knows.
|
|
522
528
|
const inferable = [];
|
|
523
529
|
const shipped = [];
|
|
530
|
+
const variantsSelected = [];
|
|
524
531
|
let enforced = 0;
|
|
525
532
|
for (const r of packed) {
|
|
526
|
-
const
|
|
533
|
+
const variantFolded = foldVariants(r.body, r.id ?? '', lockedVariants);
|
|
534
|
+
const filtered = filterInferable(variantFolded.body, proofs);
|
|
527
535
|
const folded = foldChecks(filtered.body);
|
|
528
536
|
enforced += folded.enforced.length;
|
|
529
|
-
|
|
537
|
+
variantsSelected.push(...variantFolded.selected);
|
|
538
|
+
if (!variantFolded.changed && !filtered.changed && !folded.changed) {
|
|
530
539
|
shipped.push(r);
|
|
531
540
|
continue;
|
|
532
541
|
}
|
|
533
542
|
for (const x of filtered.removed) inferable.push({ rule: r.id, text: x.text, expr: x.expr });
|
|
534
543
|
shipped.push({ ...r, body: folded.body });
|
|
535
544
|
}
|
|
545
|
+
const lockedNonDefault = variantsSelected.filter((v) => v.locked && v.winner !== v.baseline);
|
|
546
|
+
if (lockedNonDefault.length > 0) {
|
|
547
|
+
notes.push(
|
|
548
|
+
`variants: applied ${lockedNonDefault.length} locked variant(s) (${lockedNonDefault.map((v) => `${v.slotId}=${v.winner}`).join(', ')})`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
536
551
|
if (enforced > 0) {
|
|
537
552
|
notes.push(
|
|
538
553
|
`enforced: ${enforced} line(s) left the region because a checker runs them`,
|
|
@@ -1046,7 +1061,11 @@ export async function loadRules(root = null) {
|
|
|
1046
1061
|
// it travels the field that already reaches `check` and `rules list`. The
|
|
1047
1062
|
// frontmatter defect wins, because a rule that cannot say what it applies to cannot
|
|
1048
1063
|
// be reasoned about at all.
|
|
1049
|
-
error:
|
|
1064
|
+
error:
|
|
1065
|
+
describeDefect(meta) ??
|
|
1066
|
+
inferableDefect(body) ??
|
|
1067
|
+
checkDefect(body) ??
|
|
1068
|
+
variantsDefect(body, meta['simt-id'] ?? ''),
|
|
1050
1069
|
});
|
|
1051
1070
|
}
|
|
1052
1071
|
return out;
|
|
@@ -1081,6 +1100,8 @@ export async function packageRegions(root, flags, rootProfiles = null) {
|
|
|
1081
1100
|
if (packages.length === 0) return { regions: [], notes };
|
|
1082
1101
|
|
|
1083
1102
|
const rules = await loadRules(root);
|
|
1103
|
+
const lockedVariants =
|
|
1104
|
+
flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
|
|
1084
1105
|
const sha = await provenanceDigest();
|
|
1085
1106
|
// The ROOT's proofs, not the package's. The rule set is the root's, so which lines it
|
|
1086
1107
|
// already proves is a fact about the repository rather than about the directory a file
|
|
@@ -1094,9 +1115,12 @@ export async function packageRegions(root, flags, rootProfiles = null) {
|
|
|
1094
1115
|
const delta = selected.filter((r) => !shared.includes(r.id));
|
|
1095
1116
|
if (delta.length === 0) continue;
|
|
1096
1117
|
const shipped = delta.map((r) => {
|
|
1097
|
-
const
|
|
1118
|
+
const variantFolded = foldVariants(r.body, r.id ?? '', lockedVariants);
|
|
1119
|
+
const filtered = filterInferable(variantFolded.body, proofs);
|
|
1098
1120
|
const folded = foldChecks(filtered.body);
|
|
1099
|
-
return
|
|
1121
|
+
return variantFolded.changed || filtered.changed || folded.changed
|
|
1122
|
+
? { ...r, body: folded.body }
|
|
1123
|
+
: r;
|
|
1100
1124
|
});
|
|
1101
1125
|
regions.push({
|
|
1102
1126
|
path: pkg.path,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Where a skill's body lives, and how a path the registry names becomes a file.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from src/commands/skills.mjs when the distiller (plan 0003 R38) became
|
|
4
|
+
// the second real caller: two commands that resolve the same body differently would
|
|
5
|
+
// print two answers to one question, and ADR-002 refuses the third copy rather than
|
|
6
|
+
// the second use.
|
|
7
|
+
//
|
|
8
|
+
// Sources are declared as `repo/path`, so finding one needs the workspace that
|
|
9
|
+
// CONTAINS the repos. That used to be `resolve(here, '..', '..', '..')`, which is
|
|
10
|
+
// right in the checkout and wrong in `node_modules` - so every source resolved to a
|
|
11
|
+
// path that could not exist and `skills` failed with 38 ENOENT lines. Now it comes
|
|
12
|
+
// from configuration, and `null` is a legitimate answer meaning "this installation
|
|
13
|
+
// has no workspace" (ADR-014).
|
|
14
|
+
//
|
|
15
|
+
// A second location exists and is not a fallback: the repo being read is itself a
|
|
16
|
+
// location for a declared source, matched on bytes rather than on hope (ADR-015).
|
|
17
|
+
// The canonical source is still tried first, so a workspace body always wins.
|
|
18
|
+
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
|
|
22
|
+
import { locateSource, sourceHeldLocally } from './source.mjs';
|
|
23
|
+
|
|
24
|
+
export async function sourcePath(skill, workspaceRoot, root) {
|
|
25
|
+
const canonical = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
|
|
26
|
+
|
|
27
|
+
if (canonical) {
|
|
28
|
+
const found = await locateSource(canonical, { workspaceRoot, root });
|
|
29
|
+
if (found.path) return found.path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const held = await sourceHeldLocally(skill, root, { workspaceRoot });
|
|
33
|
+
if (held && !held.ambiguous) return held.path;
|
|
34
|
+
|
|
35
|
+
// A skill with no canonical source, or one whose bytes match nothing, has no
|
|
36
|
+
// body. Say so rather than printing an empty section.
|
|
37
|
+
return canonical && workspaceRoot ? join(workspaceRoot, canonical.repo, canonical.path) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The file a source path points at. A path may be a directory that carries its
|
|
42
|
+
* body as SKILL.md, the Agent Skills layout; SKILL.md wins when both exist.
|
|
43
|
+
*/
|
|
44
|
+
export function bodyFile(full) {
|
|
45
|
+
return existsSync(join(full, 'SKILL.md')) ? join(full, 'SKILL.md') : full;
|
|
46
|
+
}
|