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/bench/run.mjs
CHANGED
|
@@ -6,6 +6,9 @@ import { dirname, join, resolve, sep } from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
|
|
8
8
|
import { loadRegistry } from '../graph/load.mjs';
|
|
9
|
+
import { extractOpenedSkills, matchSkillsForPaths } from '../graph/compile.mjs';
|
|
10
|
+
import { matchGlob } from '../index/govern.mjs';
|
|
11
|
+
import { formatLessonParagraph, listPromotedLessons } from '../memory/lessons.mjs';
|
|
9
12
|
import { CONFIG_DIR, MANIFEST_FILE } from '../name.mjs';
|
|
10
13
|
import { readManifest } from '../projection/manifest.mjs';
|
|
11
14
|
import { plan } from '../projection/project.mjs';
|
|
@@ -14,7 +17,9 @@ import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
|
14
17
|
import { rankSkills, tokenize } from '../resolve/resolve.mjs';
|
|
15
18
|
import { gradeWorkspace } from './grade.mjs';
|
|
16
19
|
import { resolveVaultDir } from './mine.mjs';
|
|
20
|
+
import { loadTasks } from './tasks.mjs';
|
|
17
21
|
import { ablateSectionInWorkspace, parseAblationArmName } from './ablate.mjs';
|
|
22
|
+
import { applyVariantInWorkspace, parseVariantArmName } from './variants.mjs';
|
|
18
23
|
|
|
19
24
|
const STATE_DIR = `.${CONFIG_DIR}`;
|
|
20
25
|
|
|
@@ -288,6 +293,15 @@ export async function parseArmSpec(rawSpec, cwd = process.cwd()) {
|
|
|
288
293
|
env: {},
|
|
289
294
|
};
|
|
290
295
|
}
|
|
296
|
+
const variantSpec = parseVariantArmName(spec);
|
|
297
|
+
if (variantSpec) {
|
|
298
|
+
return {
|
|
299
|
+
...variantSpec,
|
|
300
|
+
write: {},
|
|
301
|
+
remove: [],
|
|
302
|
+
env: {},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
291
305
|
if (!BUILTIN_ARMS.has(spec)) {
|
|
292
306
|
throw new Error(
|
|
293
307
|
`unknown arm "${spec}"; built-in arms are: ${[...BUILTIN_ARMS].join(', ')} (or pass --arm name=path/to/arm.json)`,
|
|
@@ -400,7 +414,7 @@ async function stripBareArm(workDir) {
|
|
|
400
414
|
}
|
|
401
415
|
}
|
|
402
416
|
|
|
403
|
-
for (const rel of ['.githooks', '.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json']) {
|
|
417
|
+
for (const rel of ['.githooks', '.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json', 'docs/lessons']) {
|
|
404
418
|
await rm(join(workDir, rel), { recursive: true, force: true }).catch(() => {});
|
|
405
419
|
}
|
|
406
420
|
|
|
@@ -457,20 +471,32 @@ async function applyProjectionMode(workDir, mode) {
|
|
|
457
471
|
|
|
458
472
|
async function writeTaskBrief(workDir, task) {
|
|
459
473
|
const prompt = String(task?.prompt ?? '');
|
|
460
|
-
const
|
|
474
|
+
const taskTokens = tokenize(prompt);
|
|
475
|
+
const taskTokenSet = new Set(taskTokens);
|
|
476
|
+
const [registry, fp, lessons] = await Promise.all([
|
|
461
477
|
loadRegistry().catch(() => ({ skills: [] })),
|
|
462
478
|
fingerprint(workDir).catch(() => ({ stacks: [], facts: new Set() })),
|
|
479
|
+
listPromotedLessons(workDir).catch(() => []),
|
|
463
480
|
]);
|
|
464
481
|
const skillsList = Array.isArray(registry.skills)
|
|
465
482
|
? registry.skills
|
|
466
483
|
: Object.values(registry.skills ?? {});
|
|
467
484
|
const { selected = [] } = rankSkills(skillsList, {
|
|
468
|
-
taskTokens
|
|
485
|
+
taskTokens,
|
|
469
486
|
paths: [],
|
|
470
487
|
repoStack: fp.stack ?? [],
|
|
471
488
|
});
|
|
472
489
|
const ranked = selected.slice(0, 5);
|
|
473
490
|
|
|
491
|
+
const taskPaths = Array.isArray(task?.paths) ? task.paths : [];
|
|
492
|
+
const matchingLessons = lessons.filter((lesson) => {
|
|
493
|
+
const lessonGlobs = Array.isArray(lesson.paths) ? lesson.paths : [];
|
|
494
|
+
if (taskPaths.some((p) => lessonGlobs.some((glob) => matchGlob(glob, p)))) return true;
|
|
495
|
+
if (lessonGlobs.some((glob) => prompt.includes(glob.replace(/\/\*\*$/, '')))) return true;
|
|
496
|
+
const kw = Array.isArray(lesson.keywords) ? lesson.keywords : [];
|
|
497
|
+
return kw.some((k) => taskTokenSet.has(String(k).toLowerCase()));
|
|
498
|
+
});
|
|
499
|
+
|
|
474
500
|
const lines = [
|
|
475
501
|
`# Task Brief: ${task?.id ?? 'task'}`,
|
|
476
502
|
'',
|
|
@@ -480,6 +506,13 @@ async function writeTaskBrief(workDir, task) {
|
|
|
480
506
|
...(ranked.length > 0
|
|
481
507
|
? ranked.map((r) => `- ${r.skill.name} (score ${r.score}): ${r.skill.description ?? ''}`)
|
|
482
508
|
: ['- (none matched)']),
|
|
509
|
+
...(matchingLessons.length > 0
|
|
510
|
+
? [
|
|
511
|
+
'',
|
|
512
|
+
'## Lessons',
|
|
513
|
+
...matchingLessons.map((l) => `- ${formatLessonParagraph(l)}`),
|
|
514
|
+
]
|
|
515
|
+
: []),
|
|
483
516
|
'',
|
|
484
517
|
];
|
|
485
518
|
const briefPath = join(workDir, STATE_DIR, 'brief.md');
|
|
@@ -497,9 +530,12 @@ async function writeTaskBrief(workDir, task) {
|
|
|
497
530
|
*/
|
|
498
531
|
export async function materialiseArm(workDir, armSpec, task = {}) {
|
|
499
532
|
const ablate = armSpec.ablate ?? parseAblationArmName(armSpec.name)?.ablate ?? null;
|
|
533
|
+
const variant = armSpec.variant ?? parseVariantArmName(armSpec.name)?.variant ?? null;
|
|
500
534
|
const base = armSpec.base ?? (BUILTIN_ARMS.has(armSpec.name) ? armSpec.name : null);
|
|
501
535
|
if (ablate) {
|
|
502
536
|
await ablateSectionInWorkspace(workDir, ablate);
|
|
537
|
+
} else if (variant) {
|
|
538
|
+
await applyVariantInWorkspace(workDir, variant);
|
|
503
539
|
} else if (base === 'bare') {
|
|
504
540
|
await stripBareArm(workDir);
|
|
505
541
|
} else if (base === 'current') {
|
|
@@ -559,6 +595,31 @@ export async function prepareIsolatedWorkspace(root, baseSha, armSpec, task = {}
|
|
|
559
595
|
throw new Error(`tar unpack failed for ${baseSha}: ${tarRes.stderr.toString('utf8').trim()}`);
|
|
560
596
|
}
|
|
561
597
|
|
|
598
|
+
const rootLessonsDir = join(root, 'docs', 'lessons');
|
|
599
|
+
const rootLessonFiles = (await readdir(rootLessonsDir).catch(() => [])).filter((f) => f.endsWith('.md'));
|
|
600
|
+
if (rootLessonFiles.length > 0) {
|
|
601
|
+
const destLessonsDir = join(workDir, 'docs', 'lessons');
|
|
602
|
+
await mkdir(destLessonsDir, { recursive: true });
|
|
603
|
+
for (const file of rootLessonFiles) {
|
|
604
|
+
const target = join(destLessonsDir, file);
|
|
605
|
+
if (!existsSync(target)) {
|
|
606
|
+
const text = await readFile(join(rootLessonsDir, file), 'utf8');
|
|
607
|
+
await writeFile(target, text, 'utf8');
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const rootProfilesDir = join(root, STATE_DIR, 'profiles');
|
|
613
|
+
const rootProfileFiles = (await readdir(rootProfilesDir).catch(() => [])).filter((f) => f.endsWith('.md'));
|
|
614
|
+
if (rootProfileFiles.length > 0) {
|
|
615
|
+
const destProfilesDir = join(workDir, STATE_DIR, 'profiles');
|
|
616
|
+
await mkdir(destProfilesDir, { recursive: true });
|
|
617
|
+
for (const file of rootProfileFiles) {
|
|
618
|
+
const text = await readFile(join(rootProfilesDir, file), 'utf8');
|
|
619
|
+
await writeFile(join(destProfilesDir, file), text, 'utf8');
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
562
623
|
const { env } = await materialiseArm(workDir, armSpec, task);
|
|
563
624
|
|
|
564
625
|
await runCmd('git', ['init', '-q'], { cwd: workDir });
|
|
@@ -598,66 +659,28 @@ function buildAdapterArgs(adapter, { prompt, turns, model, analyticsLog }) {
|
|
|
598
659
|
}
|
|
599
660
|
|
|
600
661
|
async function executeAdapter(adapter, workDir, task, { vaultRoot, model, maxTurns, env, timeoutMs, slotDir }) {
|
|
601
|
-
if (adapter.id === 'noop') {
|
|
602
|
-
return parseAdapterUsage(adapter, '');
|
|
603
|
-
}
|
|
604
|
-
|
|
662
|
+
if (adapter.id === 'noop') return { ...parseAdapterUsage(adapter, ''), transcript: '' };
|
|
605
663
|
if (adapter.id === 'gold') {
|
|
606
|
-
const
|
|
607
|
-
const goldPatch = await readFile(goldPatchPath);
|
|
664
|
+
const goldPatch = await readFile(join(vaultRoot, task.id, 'gold.patch'));
|
|
608
665
|
if (goldPatch.length > 0) {
|
|
609
|
-
const applyRes = await runCmd('git', ['apply', '-'], {
|
|
610
|
-
cwd: workDir,
|
|
611
|
-
input: goldPatch,
|
|
612
|
-
timeoutMs,
|
|
613
|
-
});
|
|
666
|
+
const applyRes = await runCmd('git', ['apply', '-'], { cwd: workDir, input: goldPatch, timeoutMs });
|
|
614
667
|
if (applyRes.code !== 0) {
|
|
615
|
-
await runCmd('git', ['apply', '--3way', '-'], {
|
|
616
|
-
cwd: workDir,
|
|
617
|
-
input: goldPatch,
|
|
618
|
-
timeoutMs,
|
|
619
|
-
});
|
|
668
|
+
await runCmd('git', ['apply', '--3way', '-'], { cwd: workDir, input: goldPatch, timeoutMs });
|
|
620
669
|
}
|
|
621
670
|
}
|
|
622
|
-
return parseAdapterUsage(adapter, '');
|
|
671
|
+
return { ...parseAdapterUsage(adapter, ''), transcript: '' };
|
|
623
672
|
}
|
|
624
673
|
|
|
625
674
|
const analyticsLog = join(slotDir, 'analytics.jsonl');
|
|
626
|
-
const args = buildAdapterArgs(adapter, {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
const outcome = await runCmd(adapter.bin, args, {
|
|
634
|
-
cwd: workDir,
|
|
635
|
-
env,
|
|
636
|
-
timeoutMs,
|
|
637
|
-
});
|
|
638
|
-
const analyticsText =
|
|
639
|
-
adapter.format === 'analytics-log'
|
|
640
|
-
? await readFile(analyticsLog, 'utf8').catch(() => '')
|
|
641
|
-
: '';
|
|
642
|
-
|
|
643
|
-
return parseAdapterUsage(adapter, outcome.stdout.toString('utf8'), analyticsText);
|
|
675
|
+
const args = buildAdapterArgs(adapter, { prompt: String(task.prompt ?? ''), turns: maxTurns, model, analyticsLog });
|
|
676
|
+
const outcome = await runCmd(adapter.bin, args, { cwd: workDir, env, timeoutMs });
|
|
677
|
+
const stdoutText = outcome.stdout.toString('utf8');
|
|
678
|
+
const analyticsText = adapter.format === 'analytics-log' ? await readFile(analyticsLog, 'utf8').catch(() => '') : '';
|
|
679
|
+
return { ...parseAdapterUsage(adapter, stdoutText, analyticsText), transcript: `${stdoutText}\n${analyticsText}` };
|
|
644
680
|
}
|
|
645
681
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const entries = (await readdir(tasksDir).catch(() => []))
|
|
649
|
-
.filter((f) => f.endsWith('.json'))
|
|
650
|
-
.sort();
|
|
651
|
-
|
|
652
|
-
const filterSet = new Set(filterIds);
|
|
653
|
-
const tasks = [];
|
|
654
|
-
for (const entry of entries) {
|
|
655
|
-
const data = JSON.parse(await readFile(join(tasksDir, entry), 'utf8'));
|
|
656
|
-
if (filterSet.size > 0 && !filterSet.has(data.id)) continue;
|
|
657
|
-
tasks.push(data);
|
|
658
|
-
}
|
|
659
|
-
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
660
|
-
}
|
|
682
|
+
// loadTasks lives in tasks.mjs (the advice engine became a second reader, so the loader
|
|
683
|
+
// was extracted rather than copied — ADR-002).
|
|
661
684
|
|
|
662
685
|
function runIdFor(harness, armName, rep) {
|
|
663
686
|
return `${harness}-${armName}-r${rep}`;
|
|
@@ -776,6 +799,9 @@ export async function runBench(
|
|
|
776
799
|
}
|
|
777
800
|
}
|
|
778
801
|
|
|
802
|
+
const regRes = await loadRegistry().catch(() => ({ registry: { skills: [] } }));
|
|
803
|
+
const registrySkills = regRes?.registry?.skills ?? [];
|
|
804
|
+
const knownSkillNames = new Set(registrySkills.map((s) => s.name));
|
|
779
805
|
const parentDir = await mkdtemp(join(tmpdir(), 'bench-batch-'));
|
|
780
806
|
try {
|
|
781
807
|
const records = await mapPool(plannedRuns, concurrency, async (item) => {
|
|
@@ -787,7 +813,7 @@ export async function runBench(
|
|
|
787
813
|
{ parentDir },
|
|
788
814
|
);
|
|
789
815
|
try {
|
|
790
|
-
const { usage, costUsd } = await executeAdapter(adapter, workDir, item.task, {
|
|
816
|
+
const { usage, costUsd, transcript } = await executeAdapter(adapter, workDir, item.task, {
|
|
791
817
|
vaultRoot,
|
|
792
818
|
model: resolvedModel,
|
|
793
819
|
maxTurns,
|
|
@@ -801,6 +827,13 @@ export async function runBench(
|
|
|
801
827
|
initialHead,
|
|
802
828
|
timeoutMs,
|
|
803
829
|
});
|
|
830
|
+
const expectedSkills = matchSkillsForPaths(registrySkills, grade.goldFiles ?? []);
|
|
831
|
+
const openedSkills =
|
|
832
|
+
adapter.id === 'gold'
|
|
833
|
+
? expectedSkills
|
|
834
|
+
: adapter.id === 'noop'
|
|
835
|
+
? []
|
|
836
|
+
: extractOpenedSkills(transcript, knownSkillNames);
|
|
804
837
|
|
|
805
838
|
const record = {
|
|
806
839
|
schemaVersion: 1,
|
|
@@ -814,6 +847,7 @@ export async function runBench(
|
|
|
814
847
|
usage,
|
|
815
848
|
costUsd,
|
|
816
849
|
diff: grade.diff,
|
|
850
|
+
skills: { expected: expectedSkills, opened: openedSkills },
|
|
817
851
|
probe: null,
|
|
818
852
|
};
|
|
819
853
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Mined task descriptors, read back from the tool state's `bench/tasks/` directory (plan 0003 R01).
|
|
2
|
+
//
|
|
3
|
+
// Extracted from bench/run.mjs on its second real reader (ADR-002): the R40 advice engine
|
|
4
|
+
// needs the two facts a run record does not carry — the prompt, which classifies the task,
|
|
5
|
+
// and the gold stat, which says how many files the change touched. A second private loader
|
|
6
|
+
// beside the first would drift the first time one learned a filter the other lacked.
|
|
7
|
+
//
|
|
8
|
+
// Deterministic by construction: the directory is read sorted, the filter is exact, and no
|
|
9
|
+
// part of this touches a model, a network or a clock.
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { CONFIG_DIR } from '../name.mjs';
|
|
13
|
+
|
|
14
|
+
const STATE_DIR = `.${CONFIG_DIR}`;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load mined benchmark task descriptors.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} root the repository
|
|
20
|
+
* @param {string[]} [filterIds] exact task ids to keep; empty means load them all
|
|
21
|
+
* @returns descriptor objects sorted by id (id, base, sourceCommit, prompt, testCmd,
|
|
22
|
+
* f2p, p2p, gold {files, linesAdded, linesRemoved}, vault)
|
|
23
|
+
*/
|
|
24
|
+
export async function loadTasks(root, filterIds = []) {
|
|
25
|
+
const tasksDir = join(resolve(root), STATE_DIR, 'bench', 'tasks');
|
|
26
|
+
const entries = (await readdir(tasksDir).catch(() => []))
|
|
27
|
+
.filter((f) => f.endsWith('.json'))
|
|
28
|
+
.sort();
|
|
29
|
+
|
|
30
|
+
const filterSet = new Set(filterIds);
|
|
31
|
+
const tasks = [];
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const data = JSON.parse(await readFile(join(tasksDir, entry), 'utf8'));
|
|
34
|
+
if (filterSet.size > 0 && !filterSet.has(data.id)) continue;
|
|
35
|
+
tasks.push(data);
|
|
36
|
+
}
|
|
37
|
+
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
38
|
+
}
|