@deniscodp/skills 0.0.0-stage → 1.0.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +89 -2
  3. package/bin/deniscodp-skills.mjs +4 -0
  4. package/package.json +23 -4
  5. package/scripts/catalog.mjs +68 -0
  6. package/scripts/cli.mjs +264 -0
  7. package/scripts/install.mjs +184 -0
  8. package/scripts/recommended.mjs +102 -0
  9. package/skills/denisco-handoff-implementation/SKILL.md +254 -0
  10. package/skills/denisco-handoff-implementation/references/blocker-checklist.md +71 -0
  11. package/skills/denisco-handoff-implementation/references/delegated-implementation.md +168 -0
  12. package/skills/denisco-handoff-implementation/references/ownership.md +105 -0
  13. package/skills/denisco-handoff-implementation/references/session-plan.md +195 -0
  14. package/skills/denisco-handoff-implementation/scripts/ownership.mjs +153 -0
  15. package/skills/denisco-implementation-coordinator/SKILL.md +237 -0
  16. package/skills/denisco-implementation-coordinator/agents/openai.yaml +2 -0
  17. package/skills/denisco-implementation-coordinator/references/documents.md +175 -0
  18. package/skills/denisco-implementation-coordinator/references/runtimes.md +105 -0
  19. package/skills/denisco-interactive-spec-definition/SKILL.md +126 -0
  20. package/skills/denisco-interactive-spec-definition/references/blocker-checklist.md +71 -0
  21. package/skills/denisco-interactive-spec-definition/references/handoff-template.md +124 -0
  22. package/skills/denisco-project-context/SKILL.md +60 -0
  23. package/skills/denisco-project-context/references/project-documents.md +70 -0
  24. package/skills/denisco-visual-check/SKILL.md +136 -0
  25. package/skills/denisco-visual-check/references/judge-prompt.md +55 -0
  26. package/skills/denisco-visual-check/references/project-config.md +118 -0
  27. package/skills/denisco-workflow-benchmark/SKILL.md +113 -0
  28. package/skills/denisco-workflow-benchmark/agents/openai.yaml +2 -0
  29. package/skills/denisco-workflow-benchmark/references/measurement.md +166 -0
  30. package/skills/denisco-workflow-benchmark/references/workflow-contract.md +371 -0
  31. package/skills/denisco-workflow-benchmark/scripts/bench.mjs +156 -0
  32. package/skills/denisco-workflow-benchmark/scripts/cases.mjs +46 -0
  33. package/skills/denisco-workflow-benchmark/scripts/configuration.mjs +71 -0
  34. package/skills/denisco-workflow-benchmark/scripts/discover.mjs +88 -0
  35. package/skills/denisco-workflow-benchmark/scripts/eval-worker.mjs +58 -0
  36. package/skills/denisco-workflow-benchmark/scripts/io.mjs +82 -0
  37. package/skills/denisco-workflow-benchmark/scripts/prepare.mjs +69 -0
  38. package/skills/denisco-workflow-benchmark/scripts/runs.mjs +235 -0
  39. package/skills/denisco-workflow-benchmark/scripts/state-prompt.mjs +96 -0
  40. package/skills/denisco-workflow-benchmark/scripts/tracking.mjs +91 -0
  41. package/skills/denisco-workflow-benchmark/scripts/usage.mjs +175 -0
  42. package/skills/denisco-workflow-benchmark/scripts/workflow.mjs +334 -0
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { hash, listFiles, mutateRun, now, rejectSymlinkPath, safeId } from './io.mjs';
5
+
6
+ // Capture only a simple metadata.version scalar from the YAML frontmatter. This is not a
7
+ // YAML validator: unfamiliar syntax remains unknown instead of adding a runtime dependency.
8
+ export function declaredVersion(text) {
9
+ const frontmatter = text.replace(/^\uFEFF/, '').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
10
+ const metadata = frontmatter?.match(/^metadata:\s*\r?\n((?:[ \t]+[^\r\n]*\r?\n?)+)/m)?.[1];
11
+ const version = metadata?.match(/^ {2}version: *(?:"(\d+\.\d+\.\d+)"|'(\d+\.\d+\.\d+)'|(\d+\.\d+\.\d+)) *(?:#.*)?$/m);
12
+ return version ? version[1] ?? version[2] ?? version[3] : null;
13
+ }
14
+ export function snapshotConfiguration({ skillsRoot, note, loaded = [] }) {
15
+ if (!note?.trim()) throw new Error('Configuration requires an observation/provenance note');
16
+ if (!Array.isArray(loaded) || new Set(loaded).size !== loaded.length) throw new Error('Loaded skill names must be unique');
17
+ loaded.forEach(safeId);
18
+ const root = path.resolve(skillsRoot);
19
+ rejectSymlinkPath(root);
20
+ const skills = [];
21
+ for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
22
+ if (entry.isSymbolicLink()) throw new Error('Symlink skill snapshot refused');
23
+ const directory = path.join(root, entry.name), skillFile = path.join(directory, 'SKILL.md');
24
+ if (!entry.isDirectory() || !fs.existsSync(skillFile)) continue;
25
+ safeId(entry.name);
26
+ const files = listFiles(directory).map(relative => ({ path: relative, sha256: hash(fs.readFileSync(path.join(directory, relative))) }));
27
+ const version = declaredVersion(fs.readFileSync(skillFile, 'utf8'));
28
+ skills.push({ name: entry.name, path: directory, declaredVersion: version, files, packageHash: hash(JSON.stringify(files)),
29
+ loading: loaded.includes(entry.name) ? 'operator-attested-loaded' : 'installed-only' });
30
+ }
31
+ if (!skills.length) throw new Error('No skills found to snapshot');
32
+ if (loaded.some(name => !skills.some(s => s.name === name))) throw new Error('Attested loaded skill is absent from snapshot');
33
+ const at = now();
34
+ const snapshot = { id: crypto.randomUUID(), capturedAt: at, observedAt: at,
35
+ provenance: note, skillsRoot: root, skills,
36
+ warnings: skills.filter(s => !s.declaredVersion).map(s => `Unknown metadata.version: ${s.name}`) };
37
+ return snapshot;
38
+ }
39
+ export function captureConfiguration(file, options) {
40
+ const snapshot = snapshotConfiguration(options);
41
+ mutateRun(file, run => {
42
+ run.configurationHistory ??= [];
43
+ run.configurationHistory.push(snapshot);
44
+ });
45
+ return snapshot;
46
+ }
47
+
48
+ export function recordEnvironment(file, note) {
49
+ if (!note?.trim()) throw new Error('Environment note required');
50
+ return mutateRun(file, run => {
51
+ run.environmentHistory ??= [];
52
+ // Preserve legacy notes on the first new update, without relabeling their observation time.
53
+ if (run.environmentReview && !run.environmentHistory.length) run.environmentHistory.push({ ...run.environmentReview, legacy: true });
54
+ const event = { note, at: now() };
55
+ run.environmentHistory.push(event);
56
+ run.environmentReview = event;
57
+ });
58
+ }
59
+
60
+ export function attachConfiguration(file, { sessionKey, configurationId, note }) {
61
+ if (!note?.trim()) throw new Error('Configuration attribution requires a provenance note');
62
+ return mutateRun(file, run => {
63
+ const session = run.sessions.find(s => s.key === sessionKey);
64
+ if (!session) throw new Error('Unknown session key');
65
+ if (!run.configurationHistory?.some(c => c.id === configurationId)) throw new Error('Unknown configuration ID');
66
+ if (session.configurationId && session.configurationId !== configurationId) throw new Error('Session already has a different configuration; preserve history');
67
+ if (session.configurationId === configurationId) return;
68
+ session.configurationId = configurationId;
69
+ session.configurationAttribution = { note, at: now() };
70
+ });
71
+ }
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ const canonical = value => process.platform === 'win32' ? path.resolve(value).toLowerCase() : path.resolve(value);
6
+ // Both CLIs relocate their home: a profile directory holds different logs than the default one.
7
+ const defaultRoot = provider => provider === 'codex'
8
+ ? path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'), 'sessions')
9
+ : path.join(process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), '.claude'), 'projects');
10
+ function logFiles(root) {
11
+ const files = [];
12
+ (function visit(directory) {
13
+ if (!fs.existsSync(directory)) return;
14
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
15
+ if (entry.isSymbolicLink()) continue;
16
+ const file = path.join(directory, entry.name);
17
+ if (entry.isDirectory()) visit(file);
18
+ else if (entry.name.endsWith('.jsonl')) files.push(file);
19
+ }
20
+ })(root);
21
+ return files;
22
+ }
23
+
24
+ // Identity by content, never by recency: the marker is in that log because this session wrote it.
25
+ export function whoami({ provider, project, nonce, root }) {
26
+ if (!['codex', 'claude'].includes(provider)) throw new Error('Invalid provider');
27
+ if (typeof nonce !== 'string' || !/^[A-Za-z0-9._:-]{8,100}$/.test(nonce)) throw new Error('Provide a --nonce of 8..100 plain characters');
28
+ root ??= defaultRoot(provider);
29
+ const matches = [];
30
+ for (const file of logFiles(root)) {
31
+ let text;
32
+ try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
33
+ if (!text.includes(nonce)) continue;
34
+ let sessionId = null, agentId = null, cwd = null;
35
+ for (const line of text.split(/\r?\n/)) {
36
+ let r;
37
+ try { r = JSON.parse(line); } catch { continue; }
38
+ const p = provider === 'codex' ? (r.type === 'session_meta' ? r.payload : null) : r;
39
+ const id = provider === 'codex' ? p?.id ?? p?.session_id : p?.sessionId ?? p?.session_id;
40
+ if (id) { sessionId ??= id; cwd ??= p?.cwd ?? null; agentId ??= p?.agentId ?? null; }
41
+ }
42
+ if (sessionId) matches.push({ provider, sessionId, agentId, log: path.resolve(file), cwd });
43
+ }
44
+ if (!matches.length) throw new Error(`No log contains the marker yet. The transcript is written after the call that emitted it: repeat this command in a later tool call. Searched: ${path.resolve(root)}`);
45
+ // --project is a declared constraint, so it narrows the candidates; it never picks among them.
46
+ const selected = project ? matches.filter(match => canonical(match.cwd ?? '') === canonical(project)) : matches;
47
+ if (!selected.length) throw new Error(`Marker found in ${matches.length} log(s), none with cwd ${path.resolve(project)}: ${matches.map(m => m.cwd ?? 'no recorded cwd').join(', ')}. Review before registering`);
48
+ if (selected.length > 1) throw new Error(`Marker found in ${selected.length} logs: ${selected.map(m => m.sessionId).join(', ')}. Use a fresh unique marker; no log is selected automatically.`);
49
+ return { ...selected[0], note: 'Identity established by a marker this session emitted, not by log recency. Register the phase explicitly.' };
50
+ }
51
+
52
+ export function discover({ provider, project, root, limit = 20 }) {
53
+ if (!['codex', 'claude'].includes(provider)) throw new Error('Invalid provider');
54
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200) throw new Error('Limit must be 1..200');
55
+ root ??= defaultRoot(provider);
56
+ const candidates = [];
57
+ function visit(directory) {
58
+ if (!fs.existsSync(directory)) return;
59
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
60
+ if (entry.isSymbolicLink()) continue;
61
+ const file = path.join(directory, entry.name);
62
+ if (entry.isDirectory()) visit(file);
63
+ else if (entry.name.endsWith('.jsonl')) candidates.push({ file, modified: fs.statSync(file).mtimeMs });
64
+ }
65
+ }
66
+ visit(root);
67
+ candidates.sort((a, b) => b.modified - a.modified);
68
+ const found = [];
69
+ for (const { file, modified } of candidates) {
70
+ // Metadata prefix only. Never return message bodies or search by user prompt content.
71
+ const buffer = Buffer.alloc(256 * 1024), fd = fs.openSync(file, 'r');
72
+ let text;
73
+ try { text = buffer.subarray(0, fs.readSync(fd, buffer, 0, buffer.length, 0)).toString('utf8').replace(/^\uFEFF/, ''); }
74
+ finally { fs.closeSync(fd); }
75
+ for (const line of text.split(/\r?\n/)) {
76
+ let r;
77
+ try { r = JSON.parse(line); } catch { continue; }
78
+ const p = provider === 'codex' ? (r.type === 'session_meta' ? r.payload : null) : r;
79
+ const id = provider === 'codex' ? p?.id ?? p?.session_id : p?.sessionId ?? p?.session_id;
80
+ if (id && p?.cwd && canonical(p.cwd) === canonical(project)) {
81
+ found.push({ provider, sessionId: id, agentId: p.agentId ?? null, log: path.resolve(file), modifiedAt: new Date(modified).toISOString(), version: p.cli_version ?? p.version ?? null });
82
+ break;
83
+ }
84
+ }
85
+ if (found.length >= limit) break;
86
+ }
87
+ return { sessions: found, note: 'Metadata candidates only, never auto-selected. Confirm task ownership; logs without matching metadata in the prefix are not listed.' };
88
+ }
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ const [caseId, workspace] = process.argv.slice(2);
6
+ const results = [];
7
+ let orders, dashboard, exporter;
8
+ try {
9
+ [orders, dashboard, exporter] = await Promise.all(['orders', 'dashboard', 'export'].map(name => import(pathToFileURL(path.join(workspace, 'src', `${name}.mjs`)))));
10
+ } catch (error) {
11
+ console.log(JSON.stringify({ fatal: String(error.message).slice(0, 500) }));
12
+ process.exit(1);
13
+ }
14
+ const rows = Object.freeze([
15
+ Object.freeze({ id: 1, status: 'completed' }), Object.freeze({ id: 2, status: 'pending' }),
16
+ Object.freeze({ id: 3, status: 'completed' }), Object.freeze({ id: 4, status: 'cancelled' })
17
+ ]);
18
+ const expected = { total: 4, byStatus: { pending: 1, completed: 2, cancelled: 1 } };
19
+ function check(id, fn) {
20
+ try { fn(); results.push({ id, status: 'pass', observation: 'Fixed assertions passed.' }); }
21
+ catch (e) { results.push({ id, status: 'fail', observation: String(e.message).slice(0, 1200) }); }
22
+ }
23
+ if (caseId === 'order-filter') {
24
+ check('filter', () => {
25
+ for (const status of ['pending', 'completed', 'cancelled']) {
26
+ assert.deepEqual(orders.filterOrders(rows, status), rows.filter(r => r.status === status));
27
+ }
28
+ });
29
+ check('consumers', () => {
30
+ for (const status of ['pending', 'completed', 'cancelled']) {
31
+ const wanted = rows.filter(r => r.status === status);
32
+ assert.deepEqual(dashboard.dashboardOrders(rows, status), wanted);
33
+ assert.deepEqual(exporter.exportOrders(rows, status), wanted);
34
+ }
35
+ });
36
+ } else if (caseId === 'order-summary') {
37
+ check('summary', () => {
38
+ assert.deepEqual(orders.summarizeOrders(rows), expected);
39
+ assert.deepEqual(orders.summarizeOrders([]), { total: 0, byStatus: { pending: 0, completed: 0, cancelled: 0 } });
40
+ assert.deepEqual(orders.summarizeOrders([rows[0]]), { total: 1, byStatus: { pending: 0, completed: 1, cancelled: 0 } });
41
+ });
42
+ check('consumers', () => {
43
+ for (const f of [dashboard.dashboardSummary, exporter.exportSummary]) {
44
+ assert.deepEqual(f(rows), expected);
45
+ assert.deepEqual(f([]), { total: 0, byStatus: { pending: 0, completed: 0, cancelled: 0 } });
46
+ }
47
+ });
48
+ } else throw new Error('Unknown case');
49
+ check('regression', () => {
50
+ for (const f of [orders.filterOrders, dashboard.dashboardOrders, exporter.exportOrders]) {
51
+ assert.deepEqual(f(rows, 'all'), rows);
52
+ assert.deepEqual(f([], 'completed'), []);
53
+ assert.deepEqual(f(rows, 'absent'), []);
54
+ if (caseId === 'order-summary') assert.deepEqual(f(rows, 'completed'), [rows[0], rows[2]]);
55
+ }
56
+ assert.deepEqual(rows.map(r => r.id), [1, 2, 3, 4]);
57
+ });
58
+ console.log(JSON.stringify({ results }));
@@ -0,0 +1,82 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+
5
+ export const hash = data => crypto.createHash('sha256').update(data).digest('hex');
6
+ export const readJSON = file => JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
7
+ export const now = () => new Date().toISOString();
8
+ export function requireValue(value, name) {
9
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`Required: ${name}`);
10
+ return value;
11
+ }
12
+ export function safeId(value) {
13
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/.test(value ?? '')) throw new Error('Invalid ID');
14
+ return value;
15
+ }
16
+ export function inside(root, relative) {
17
+ const resolved = path.resolve(root, relative);
18
+ const rel = path.relative(path.resolve(root), resolved);
19
+ if (rel === '' || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
20
+ throw new Error(`Path must be strictly inside target: ${relative}`);
21
+ }
22
+ return resolved;
23
+ }
24
+ export function rejectSymlinkPath(target) {
25
+ let current = path.resolve(target);
26
+ for (;;) {
27
+ let stat;
28
+ try { stat = fs.lstatSync(current); } catch (e) { if (e.code !== 'ENOENT') throw e; }
29
+ if (stat?.isSymbolicLink()) throw new Error(`Symlink target refused: ${current}`);
30
+ const parent = path.dirname(current);
31
+ if (parent === current) break;
32
+ current = parent;
33
+ }
34
+ }
35
+ export function writeJSON(file, data) {
36
+ rejectSymlinkPath(file);
37
+ fs.mkdirSync(path.dirname(file), { recursive: true });
38
+ const temporary = `${file}.${crypto.randomUUID()}.tmp`;
39
+ fs.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, { flag: 'wx' });
40
+ try { fs.renameSync(temporary, file); } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
41
+ }
42
+ export function newDirectory(target) {
43
+ rejectSymlinkPath(target);
44
+ if (fs.existsSync(target)) throw new Error(`Target already exists: ${target}`);
45
+ fs.mkdirSync(target, { recursive: true });
46
+ }
47
+ export function writeNew(file, content) {
48
+ rejectSymlinkPath(file);
49
+ fs.mkdirSync(path.dirname(file), { recursive: true });
50
+ fs.writeFileSync(file, content, { flag: 'wx' });
51
+ }
52
+ export function listFiles(root, prefix = '') {
53
+ if (!fs.existsSync(root)) return [];
54
+ return fs.readdirSync(root, { withFileTypes: true }).flatMap(entry => {
55
+ if (entry.isSymbolicLink()) throw new Error(`Symlink source refused: ${entry.name}`);
56
+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
57
+ return entry.isDirectory() ? listFiles(path.join(root, entry.name), relative) : [relative];
58
+ }).sort();
59
+ }
60
+ export function treeHash(root) {
61
+ return hash(JSON.stringify(listFiles(root).map(relative => [relative, hash(fs.readFileSync(path.join(root, relative)))])));
62
+ }
63
+ export function loadRun(file) {
64
+ const run = readJSON(file);
65
+ if (run.schemaVersion !== 1 || !Array.isArray(run.sessions) || !Array.isArray(run.criteria)) {
66
+ throw new Error('Unsupported run manifest');
67
+ }
68
+ return run;
69
+ }
70
+ export function mutateRun(file, fn) {
71
+ // One writer per manifest; fail explicitly rather than lose concurrent updates.
72
+ rejectSymlinkPath(file);
73
+ const lock = `${file}.lock`;
74
+ const fd = fs.openSync(lock, 'wx');
75
+ try {
76
+ const run = loadRun(file);
77
+ fn(run);
78
+ run.updatedAt = now();
79
+ writeJSON(file, run);
80
+ return run;
81
+ } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
82
+ }
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { CASES, handoff, smoke } from './cases.mjs';
5
+ import { createRun } from './runs.mjs';
6
+ import { hash, listFiles, readJSON, inside, writeJSON, writeNew, mutateRun } from './io.mjs';
7
+
8
+ // The frozen baseline keeps the original skill names; candidates use the current prefixed names.
9
+ const baselineNames = ['interactive-spec-definition', 'handoff-implementation', 'visual-check'];
10
+ const candidateNames = ['denisco-interactive-spec-definition', 'denisco-handoff-implementation', 'denisco-visual-check'];
11
+ export function prepare({ out, caseId, variant, provider, baseline, sourceRoot }) {
12
+ const definition = CASES[caseId];
13
+ if (!definition) throw new Error('Case must be order-filter or order-summary');
14
+ if (!['baseline', 'candidate'].includes(variant)) throw new Error('Variant must be baseline or candidate');
15
+ if (!['codex', 'claude'].includes(provider)) throw new Error('Choose the provider that will run this case');
16
+ const files = [];
17
+ if (variant === 'baseline') {
18
+ if (!baseline) throw new Error('Explicit --baseline manifest is required');
19
+ const snapshot = readJSON(baseline);
20
+ if (snapshot.schemaVersion !== 1 || snapshot.id !== 'original-v1') throw new Error('Invalid baseline');
21
+ for (const file of snapshot.files) {
22
+ const content = Buffer.from(file.base64, 'base64');
23
+ if (hash(content) !== file.sha256) throw new Error(`Baseline integrity failure: ${file.path}`);
24
+ if (!baselineNames.includes(file.path.split('/')[0])) throw new Error('Unexpected baseline skill');
25
+ inside(out, file.path);
26
+ files.push({ path: file.path, content });
27
+ }
28
+ } else {
29
+ const supplied = sourceRoot ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
30
+ const nested = path.join(supplied, 'skills');
31
+ const root = fs.existsSync(nested) && fs.statSync(nested).isDirectory() && !fs.existsSync(path.join(nested, 'SKILL.md')) ? nested : supplied;
32
+ for (const name of candidateNames) {
33
+ if (!fs.existsSync(path.join(root, name, 'SKILL.md'))) throw new Error(`Missing benchmark skill: ${name}`);
34
+ for (const file of listFiles(path.join(root, name))) {
35
+ files.push({ path: `${name}/${file}`, content: fs.readFileSync(path.join(root, name, file)) });
36
+ }
37
+ }
38
+ }
39
+ const workspace = path.resolve(out, 'workspace');
40
+ const criteria = definition.criteria;
41
+ createRun({ out, task: caseId, project: workspace, variant, criteria });
42
+ const appFiles = { ...definition.files, 'smoke.mjs': smoke, 'HANDOFF.md': handoff(caseId),
43
+ 'package.json': '{"private":true,"type":"module","scripts":{"test":"node smoke.mjs"}}\n',
44
+ 'AGENTS.md': '# Benchmark workspace\n\nImplement the decided HANDOFF.md. Run node smoke.mjs and focused tests.\nOnly modify src files, add focused tests, and write PLAN.md/STATE.md or handoff reports.\nDo not inspect external evaluators/reference solutions, alter the handoff, installed skills or this file.\nThe operator runs fixed acceptance checks separately. No browser, login or external services.\nFollow the supplied handoff implementation skill and preserve its session workflow.\n',
45
+ 'CLAUDE.md': 'Read AGENTS.md and HANDOFF.md. The benchmark contract applies to every session.\n' };
46
+ for (const [relative, content] of Object.entries(appFiles)) writeNew(inside(workspace, relative), content);
47
+ const skillsDir = provider === 'codex' ? '.agents/skills' : '.claude/skills';
48
+ for (const file of files) writeNew(inside(workspace, `${skillsDir}/${file.path}`), file.content);
49
+ const protectedFiles = Object.fromEntries(Object.entries(appFiles).filter(([p]) => !p.startsWith('src/')).map(([p, text]) => [p, hash(text)]));
50
+ for (const file of files) protectedFiles[`${skillsDir}/${file.path}`] = hash(file.content);
51
+ const caseInputHash = hash(JSON.stringify({ caseId, criteria, appFiles }));
52
+ mutateRun(path.join(out, 'run.json'), run => {
53
+ run.caseId = caseId; run.caseInputHash = caseInputHash;
54
+ run.skillSnapshot = Object.fromEntries(files.map(f => [f.path, hash(f.content)]));
55
+ run.protectedFiles = protectedFiles;
56
+ run.preparedProvider = provider;
57
+ });
58
+ const implementationSkill = variant === 'baseline' ? 'handoff-implementation' : 'denisco-handoff-implementation';
59
+ const prompt = `Use ${implementationSkill} with HANDOFF.md. Follow its planning and session-split procedure. The product behavior is decided. Do not inspect anything outside this workspace, especially evaluator or reference solutions. If the workflow blocks, report it instead of silently replacing its procedure. Record actual loaded skill paths, model, effort and external instructions for the operator.\n`;
60
+ writeNew(path.join(out, 'PROMPT.md'), prompt);
61
+ writeNew(path.join(out, 'OPERATOR.md'), `# Operatore\n\nCaso ${caseId}; variante ${variant}; CLI ${provider}.\n\n1. Esegui git init in workspace e verifica con git rev-parse --show-toplevel che sia la radice del progetto. Poi avvia ${provider} lì.\n2. Verifica le skill effettivamente caricate e annota modello, effort, configurazioni globali e memoria. Un conflitto di skill rende la prova contaminata.\n3. Incolla PROMPT.md. Conferma soltanto il piano di sessioni previsto; segui i prompt successivi. Registra interventi e blocchi.\n4. Registra ogni sessione con bench.mjs session, poi collect dopo la chiusura. Includi subagenti attribuibili e conferma la copertura solo dopo revisione.\n5. Esegui bench.mjs evaluate --run <run.json> e report.\n\nIl verificatore si trova nel toolkit, fuori da workspace. Questa separazione è procedurale, non una sandbox di sicurezza. Non usare prove con ispezione del verificatore per conclusioni controllate.\nNon è stata avviata alcuna sessione con modelli.\n`);
62
+ return { run: path.resolve(out, 'run.json'), workspace, prompt: path.resolve(out, 'PROMPT.md'), variant, provider };
63
+ }
64
+ export function checkProtected(run) {
65
+ return Object.entries(run.protectedFiles ?? {}).filter(([relative, expected]) => {
66
+ const file = inside(run.project, relative);
67
+ return !fs.existsSync(file) || hash(fs.readFileSync(file)) !== expected;
68
+ }).map(([relative]) => relative);
69
+ }
@@ -0,0 +1,235 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { hash, treeHash, now, newDirectory, writeJSON, safeId, loadRun, mutateRun, rejectSymlinkPath, inside } from './io.mjs';
6
+ import { importUsage } from './usage.mjs';
7
+ // Baseline runs load the original name; current packages use the prefixed one.
8
+ const visualCheckNames = ['visual-check', 'denisco-visual-check'];
9
+
10
+ export const phases = ['exploration', 'specification', 'planning', 'implementation', 'verification', 'correction'];
11
+ export function createRun({ out, task, project, variant, criteria }) {
12
+ safeId(task); safeId(variant);
13
+ if (!Array.isArray(criteria) || !criteria.length || criteria.some(c => !c.id || !c.expectation) || new Set(criteria.map(c => c.id)).size !== criteria.length) throw new Error('Provide nonempty unique criteria with id and expectation');
14
+ criteria.forEach(c => safeId(c.id));
15
+ if (!criteria.some(c => c.required !== false)) throw new Error('At least one required acceptance criterion is needed');
16
+ newDirectory(out);
17
+ const run = { schemaVersion: 1, id: path.basename(path.resolve(out)), task, project: path.resolve(project), variant,
18
+ createdAt: now(), updatedAt: now(), criteria: criteria.map(c => ({ id: c.id, expectation: String(c.expectation), required: c.required !== false })),
19
+ sessions: [], visualChecks: [], evidence: [], history: [], interventions: [], review: { status: 'pending', note: '' }, environmentReview: null };
20
+ writeJSON(path.join(out, 'run.json'), run);
21
+ return run;
22
+ }
23
+ export function startVisualCheck(file, { id, block, criteria, executor = 'subagent', principalSession, at } = {}) {
24
+ // Omitted executor preserves the legacy CLI contract. New workflow calls select principal explicitly.
25
+ if (!['principal', 'subagent'].includes(executor)) throw new Error('Invalid visual check executor');
26
+ safeId(id); safeId(block); if (!Array.isArray(criteria) || !criteria.length) throw new Error('Visual check criteria are required');
27
+ criteria.forEach(safeId); if (principalSession && typeof principalSession !== 'string') throw new Error('Invalid principal session');
28
+ return mutateRun(file, run => {
29
+ if (run.visualChecks?.some(check => check.id === id)) throw new Error('Visual check ID already used; create a new attempt ID');
30
+ if (criteria.some(criterion => !run.criteria.some(c => c.id === criterion))) throw new Error('Unknown visual check criterion');
31
+ run.visualChecks ??= [];
32
+ run.visualChecks.push({ id, block, criteria, executor, principalSession: principalSession ?? null,
33
+ startedAt: at ?? now(), finishedAt: null, status: 'started', skillLoaded: null, sessionKey: null, results: [], evidence: [] });
34
+ });
35
+ }
36
+
37
+ export function finishVisualCheck(file, { id, sessionKey, skillLoaded, results = [], evidence = [], at } = {}) {
38
+ if (typeof skillLoaded !== 'boolean') throw new Error('Visual check skillLoaded must be true or false');
39
+ if (!Array.isArray(results) || !Array.isArray(evidence)) throw new Error('Visual check results and evidence must be arrays');
40
+ return mutateRun(file, run => {
41
+ const check = run.visualChecks?.find(candidate => candidate.id === id);
42
+ if (!check) throw new Error('Unknown visual check ID');
43
+ if (check.status === 'finished') throw new Error('Visual check already finished');
44
+ const session = sessionKey && run.sessions.find(candidate => candidate.key === sessionKey);
45
+ if (!session) throw new Error('Visual check session is not registered');
46
+ const executor = check.executor ?? 'subagent';
47
+ if (executor === 'subagent') {
48
+ if (!session.parent) throw new Error('Visual check must be executed by a registered child session');
49
+ if (check.principalSession && session.parent !== check.principalSession) throw new Error('Visual check child belongs to a different principal');
50
+ } else if (executor === 'principal') {
51
+ if (session.parent || session.agentId) throw new Error('Principal visual check requires a top-level session');
52
+ if (check.principalSession && session.key !== check.principalSession) throw new Error('Visual check session differs from the assigned principal');
53
+ } else throw new Error('Invalid visual check executor');
54
+ const invocation = session.metrics.skillInvocations?.some(entry => visualCheckNames.includes(entry.skill))
55
+ || session.metrics.commandInvocations?.some(entry => visualCheckNames.includes(entry.command));
56
+ if (skillLoaded && !invocation) throw new Error('Visual check loading is not present in the registered executor log');
57
+ for (const result of results) if (!result?.criterion || !check.criteria.includes(result.criterion) || !['pass', 'fail', 'not-run'].includes(result.status)) throw new Error('Invalid visual check criterion result');
58
+ for (const artifact of evidence) if (typeof artifact !== 'string' || !fs.existsSync(artifact) || !fs.statSync(artifact).isFile()) throw new Error('Visual check evidence must be an existing file');
59
+ check.finishedAt = at ?? now(); check.status = 'finished'; check.skillLoaded = skillLoaded; check.sessionKey = sessionKey;
60
+ check.results = results.map(result => ({ criterion: result.criterion, status: result.status, observation: String(result.observation ?? '') }));
61
+ check.evidence = evidence.map(artifact => ({ path: path.resolve(artifact), sha256: hash(fs.readFileSync(artifact)) }));
62
+ });
63
+ }
64
+ export function registerSession(file, { provider, sessionId, agentId, log, phase, from, to, parent, parentEvidence, coverageConfirmed = false, configurationId, configurationSnapshot }) {
65
+ if (configurationId && configurationSnapshot) throw new Error('Choose an existing configuration or a new snapshot');
66
+ if (!['codex', 'claude'].includes(provider) || !phases.includes(phase)) throw new Error('Invalid provider or phase');
67
+ safeId(sessionId);
68
+ if (agentId) safeId(agentId);
69
+ const key = `${provider}:${sessionId}${agentId ? ':' + agentId : ''}`;
70
+ const parsed = importUsage(log, provider, { sessionId, agentId, from, to });
71
+ return mutateRun(file, run => {
72
+ if (run.sessions.some(s => s.key === key)) throw new Error('Session already registered; collect again to refresh');
73
+ if (parent && !run.sessions.some(s => s.key === parent)) throw new Error('Register parent first, using provider:session-id');
74
+ if (parent && !parentEvidence?.trim()) throw new Error('Parent attribution requires --parent-evidence describing the verified native relationship');
75
+ if (agentId && !parent) throw new Error('An agent log requires an explicitly attributed parent');
76
+ if (configurationId && !run.configurationHistory?.some(c => c.id === configurationId)) throw new Error('Unknown configuration ID');
77
+ if (configurationSnapshot) {
78
+ if (!configurationSnapshot.id || !configurationSnapshot.skills?.length || run.configurationHistory?.some(c => c.id === configurationSnapshot.id)) throw new Error('Invalid or duplicate configuration snapshot');
79
+ run.configurationHistory ??= [];
80
+ run.configurationHistory.push(configurationSnapshot);
81
+ configurationId = configurationSnapshot.id;
82
+ }
83
+ run.sessions.push({ key, provider, sessionId, agentId: agentId ?? null, log: path.resolve(log), phase, from: from ?? null, to: to ?? null,
84
+ parent: parent ?? null, parentEvidence: parentEvidence ?? null, coverageConfirmed, metrics: parsed, registeredAt: now(),
85
+ ...(configurationId ? { configurationId } : {}) });
86
+ });
87
+ }
88
+ export function collect(file, { coverageConfirmed = false } = {}) {
89
+ return mutateRun(file, run => {
90
+ // Build everything before assignment: an invalid log does not partially change the manifest.
91
+ run.sessions = run.sessions.map(s => ({ ...s, coverageConfirmed: coverageConfirmed || s.coverageConfirmed, metrics: importUsage(s.log, s.provider, s), collectedAt: now() }));
92
+ });
93
+ }
94
+ export function recordEvidence(file, { criterion, status, observation, evidence }) {
95
+ if (!['pass', 'fail', 'not-run'].includes(status) || !observation?.trim()) throw new Error('Provide status and actual observation');
96
+ if (status !== 'not-run' && (!evidence || !fs.statSync(evidence).isFile())) throw new Error('A real evidence file is required');
97
+ return mutateRun(file, run => {
98
+ if (!run.criteria.some(c => c.id === criterion)) throw new Error('Unknown criterion');
99
+ run.evidence.push({ criterion, status, observation, path: evidence ? path.resolve(evidence) : null,
100
+ sha256: evidence ? hash(fs.readFileSync(evidence)) : null, at: now(), source: 'operator' });
101
+ });
102
+ }
103
+ export function evaluate(file) {
104
+ const run = loadRun(file);
105
+ if (!run.caseId) throw new Error('Generic tasks use result with actual evidence; no inferred evaluator');
106
+ const worker = fileURLToPath(new URL('./eval-worker.mjs', import.meta.url));
107
+ const implementationHash = treeHash(path.join(run.project, 'src'));
108
+ const workerEnv = Object.fromEntries(['PATH', 'Path', 'SystemRoot', 'SYSTEMROOT', 'TEMP', 'TMP', 'TMPDIR'].filter(k => process.env[k]).map(k => [k, process.env[k]]));
109
+ const result = spawnSync(process.execPath, [worker, run.caseId, run.project], {
110
+ cwd: run.project, encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024,
111
+ env: workerEnv, windowsHide: true
112
+ });
113
+ let output;
114
+ try { output = JSON.parse(result.stdout?.trim().split(/\r?\n/).at(-1) ?? '{}'); } catch { output = {}; }
115
+ const valid = result.status === 0 && Array.isArray(output.results) && run.criteria.every(c => output.results.some(r => r.id === c.id && ['pass', 'fail'].includes(r.status)));
116
+ const results = valid ? output.results : run.criteria.map(c => ({ id: c.id, status: 'fail', observation: result.error?.message ?? output.fatal ?? 'Evaluator failed or timed out.' }));
117
+ const artifact = path.join(path.dirname(file), 'evidence', `${Date.now()}-${process.hrtime.bigint()}.json`);
118
+ writeJSON(artifact, { schemaVersion: 1, caseId: run.caseId, at: now(), evaluatorHash: hash(fs.readFileSync(worker)),
119
+ exitCode: result.status, results, stderr: (result.stderr ?? '').slice(0, 2000) });
120
+ mutateRun(file, current => {
121
+ for (const r of results) current.evidence.push({ criterion: r.id, status: r.status, observation: r.observation,
122
+ path: artifact, sha256: hash(fs.readFileSync(artifact)), implementationHash, at: now(), source: 'fixed-evaluator' });
123
+ });
124
+ return { passed: results.every(r => r.status === 'pass'), results, artifact };
125
+ }
126
+
127
+ // Usage is final only while the imported log still hashes to what the last import recorded.
128
+ export function usageState(session) {
129
+ if (!fs.existsSync(session.log)) return 'unavailable';
130
+ return hash(fs.readFileSync(session.log)) === session.metrics.sourceHash ? 'final' : 'stale';
131
+ }
132
+
133
+ export function summarize(run) {
134
+ const latest = new Map();
135
+ for (const e of run.evidence) latest.set(e.criterion, e);
136
+ const required = run.criteria.filter(c => c.required !== false);
137
+ const current = required.map(c => latest.get(c.id));
138
+ const evidenceChanged = current.some(e => e && ['pass', 'fail'].includes(e.status) && (!e.path || !e.sha256 || !fs.existsSync(e.path) || !fs.statSync(e.path).isFile() || hash(fs.readFileSync(e.path)) !== e.sha256));
139
+ const implementationChanged = run.caseId && current.some(e => e?.implementationHash && e.implementationHash !== treeHash(path.join(run.project, 'src')));
140
+ const protectedChanges = Object.entries(run.protectedFiles ?? {}).filter(([relative, expected]) => {
141
+ const file = inside(run.project, relative);
142
+ return !fs.existsSync(file) || hash(fs.readFileSync(file)) !== expected;
143
+ }).map(([relative]) => relative);
144
+ const lastReopen = run.history.filter(e => e.type === 'reopened').at(-1);
145
+ const fresh = !lastReopen || (Number.isInteger(lastReopen.evidenceCount)
146
+ ? required.every(c => run.evidence.slice(lastReopen.evidenceCount).some(e => e.criterion === c.id))
147
+ : current.every(e => e && e.at > lastReopen.at));
148
+ const quality = protectedChanges.length ? 'contaminated' : evidenceChanged ? 'evidence-changed' : implementationChanged ? 'implementation-changed' : !fresh ? 'reopened' : required.length && current.every(e => e?.status === 'pass') ? 'verified' : current.some(e => e?.status === 'fail') ? 'failed' : current.some(e => e?.status === 'not-run') ? 'blocked' : 'unverified';
149
+ const groups = new Map(), warnings = [], requestKeys = new Set();
150
+ for (const s of run.sessions) {
151
+ for (const key of s.metrics.requestKeys) {
152
+ if (requestKeys.has(key)) warnings.push('Request occurs in multiple registered logs: aggregates may double-count.');
153
+ requestKeys.add(key);
154
+ }
155
+ const groupKey = `${s.provider}/${s.phase}`;
156
+ if (!groups.has(groupKey)) groups.set(groupKey, { provider: s.provider, phase: s.phase, sessions: 0,
157
+ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, total: 0 });
158
+ const group = groups.get(groupKey); group.sessions++;
159
+ for (const k of ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning', 'total']) {
160
+ const value = s.metrics.usage[k]; group[k] = group[k] === null || value === null ? null : group[k] + value;
161
+ }
162
+ const notes = s.metrics.warnings.filter(w => !s.coverageConfirmed || !w.startsWith('Full-history import:'));
163
+ warnings.push(...notes.map(w => `${s.key}: ${w}`));
164
+ if (!s.coverageConfirmed) warnings.push(`${s.key}: scope/subagent completeness not confirmed.`);
165
+ const freshness = usageState(s);
166
+ if (freshness === 'stale') warnings.push(`${s.key}: log changed after the last import; run collect before using these totals.`);
167
+ if (freshness === 'unavailable') warnings.push(`${s.key}: log is unavailable; totals cannot be refreshed.`);
168
+ if (!s.metrics.models.length || !s.metrics.versions.length) warnings.push(`${s.key}: CLI version or model is unknown.`);
169
+ if (run.configurationHistory?.length && !s.configurationId) warnings.push(`${s.key}: skill configuration not attributed; do not infer it from the latest snapshot.`);
170
+ if (s.configurationId && !run.configurationHistory?.some(c => c.id === s.configurationId)) warnings.push(`${s.key}: referenced configuration is missing.`);
171
+ }
172
+ if (!run.sessions.length) warnings.push('No sessions measured.');
173
+ for (const attempt of run.registrationAttempts ?? []) {
174
+ if (attempt.status !== 'registered') warnings.push(`Session registration pending (${attempt.phase}): ${attempt.reason ?? 'Identity unavailable'}. Agent record: ${attempt.ticket}`);
175
+ }
176
+ if (!run.environmentReview) warnings.push('Loaded skills, settings and environment not reviewed.');
177
+ if (protectedChanges.length) warnings.push(`Protected benchmark files changed: ${protectedChanges.join(', ')}`);
178
+ const visualChecks = (run.visualChecks ?? []).map(check => ({ ...check, executor: check.executor ?? 'subagent',
179
+ durationMs: check.finishedAt ? Math.max(0, Date.parse(check.finishedAt) - Date.parse(check.startedAt)) : null }));
180
+ const visualWarnings = visualChecks.flatMap(check => [
181
+ ...(check.status !== 'finished' ? [`${check.id}: visual check is incomplete.`] : []),
182
+ ...(check.status === 'finished' && !check.skillLoaded ? [`${check.id}: denisco-visual-check loading was not confirmed.`] : []),
183
+ ...(check.status === 'finished' && !check.sessionKey ? [`${check.id}: executor session is missing.`] : [])]);
184
+ return { schemaVersion: 1, runId: run.id, task: run.task, variant: run.variant, quality, review: run.review,
185
+ passed: current.filter(e => e?.status === 'pass').length, required: required.length,
186
+ usage: run.sessions.map(s => ({ key: s.key, phase: s.phase, state: usageState(s), collectedAt: s.collectedAt ?? null, total: s.metrics.usage.total })),
187
+ phases: [...groups.values()], sessions: run.sessions.length, interventions: run.interventions.length,
188
+ warnings: [...new Set([...warnings, ...visualWarnings])], coverage: warnings.length || visualWarnings.length ? 'partial-or-unreviewed' : 'reviewed',
189
+ visualChecks,
190
+ configurations: run.sessions.map(s => ({ key: s.key, provider: s.provider, phase: s.phase, models: s.metrics.models,
191
+ versions: s.metrics.versions, efforts: s.metrics.efforts, observedDurationMs: s.metrics.observedDurationMs,
192
+ configurationId: s.configurationId ?? null })),
193
+ configurationHistory: run.configurationHistory ?? [], environmentHistory: run.environmentHistory ?? [],
194
+ environmentReview: run.environmentReview, caseInputHash: run.caseInputHash ?? null };
195
+ }
196
+ const escape = text => String(text).replaceAll('|', '\\|').replaceAll('\n', ' ');
197
+ export function report(file) {
198
+ const run = loadRun(file), summary = summarize(run);
199
+ const visualSection = summary.visualChecks.length
200
+ ? `\n## Verifiche visuali\n\n${summary.visualChecks.map(check => `- ${escape(check.id)}: ${check.status}; esecutore: ${check.executor}; skill caricata: ${check.skillLoaded ?? 'n/d'}; sessione: ${check.sessionKey ?? 'n/d'}; durata ms: ${check.durationMs ?? 'n/d'}`).join('\n')}\n`
201
+ : '';
202
+ const md = `# Run ${escape(run.id)}\n\nTask: ${escape(run.task)}. Variante: ${escape(run.variant)}.\n\nEsito: **${summary.quality}** (${summary.passed}/${summary.required}). Accettazione utente: ${run.review.status}.\n\n| Provider | Fase | Sessioni | Input totale | Cache letta | Cache scritta | Output | Totale |\n|---|---|---:|---:|---:|---:|---:|---:|\n${summary.phases.map(g => `| ${g.provider} | ${g.phase} | ${g.sessions} | ${g.input ?? 'n/d'} | ${g.cacheRead ?? 'n/d'} | ${g.cacheWrite ?? 'n/d'} | ${g.output ?? 'n/d'} | ${g.total ?? 'n/d'} |`).join('\n')}\n\n${visualSection}\nCache e reasoning sono dettagli, non quantità da sommare nuovamente al totale.\nEsplorazione separata; nessun costo monetario stimato. Durate osservate non equivalgono a tempo attivo.\n\n## Copertura\n\n${summary.warnings.length ? summary.warnings.map(w => `- ${escape(w)}`).join('\n') : 'Dati e perimetro revisionati.'}\n\n## Evidenze\n\n${run.criteria.map(c => { const e = run.evidence.filter(e => e.criterion === c.id).at(-1); return `- ${escape(c.id)}: ${e?.status ?? 'not-run'} — ${escape(e?.observation ?? c.expectation)}${e?.path ? ` (${escape(e.path)})` : ''}`; }).join('\n')}\n`;
203
+ const accounting = `
204
+ ## Contabilita dei consumi
205
+
206
+ ${summary.usage.map(u => `- ${escape(u.key)} (${escape(u.phase)}): ${u.state}; ultimo collect: ${u.collectedAt ?? 'mai dopo la registrazione'}; totale ${u.total ?? 'n/d'}`).join('\n')}
207
+
208
+ Finale significa che il log non e' cambiato dopo l'ultimo import. Stale richiede collect prima di
209
+ usare questi totali: la sessione successiva lo esegue con open, altrimenti spetta all'operatore.
210
+ `;
211
+ writeJSON(path.join(path.dirname(file), 'report.json'), summary);
212
+ const markdownPath = path.join(path.dirname(file), 'report.md');
213
+ rejectSymlinkPath(markdownPath);
214
+ const configurations = `\n## Configurazioni delle skill\n\n${summary.configurations.map(c => `- ${escape(c.key)}: ${c.configurationId ?? 'non attribuita'}`).join('\n')}\n\n${summary.configurationHistory.map(c => `- ${escape(c.id)}: osservata ${escape(c.observedAt)}, acquisita ${escape(c.capturedAt)}. ${escape(c.provenance)}\n${c.skills.map(s => ` - ${escape(s.name)}: ${s.declaredVersion ?? 'versione sconosciuta'}; ${escape(s.loading)}; hash ${s.packageHash}`).join('\n')}`).join('\n')}\n\nInstallata non significa caricata. Le attestazioni richiedono riscontri della sessione.\n`;
215
+ fs.writeFileSync(markdownPath, md + accounting + configurations);
216
+ return summary;
217
+ }
218
+ export function compare(files) {
219
+ const rows = files.map(f => summarize(loadRun(f)));
220
+ const notes = ['Descriptive pilot only. Quality precedes consumption; failed and blocked runs are included.'];
221
+ const first = rows[0];
222
+ for (const row of rows.slice(1)) {
223
+ if (row.task !== first.task || !row.caseInputHash || row.caseInputHash !== first.caseInputHash) notes.push('Inputs not matched: no controlled efficiency conclusion.');
224
+ const configuration = r => JSON.stringify([...new Set(r.configurations.map(c => JSON.stringify({ provider: c.provider, models: c.models, versions: c.versions, efforts: c.efforts })))].sort());
225
+ if (configuration(row) !== configuration(first)) notes.push('Provider/model/version/effort differs: comparison is confounded.');
226
+ const packages = r => [...new Set(r.configurationHistory.map(c => JSON.stringify(c.skills.map(s => [s.name, s.packageHash]).sort())))].sort();
227
+ if (JSON.stringify(packages(row)) !== JSON.stringify(packages(first))) notes.push('Recorded skill snapshots differ; inspect configuration attribution before interpreting the comparison.');
228
+ if (row.configurations.some(c => first.configurations.some(other => other.key === c.key))) notes.push('Native session reused across comparison runs: check attribution before drawing conclusions.');
229
+ }
230
+ if (rows.some(r => r.coverage !== 'reviewed')) notes.push('Some consumption or environment coverage is incomplete/unreviewed.');
231
+ return { rows, counts: { runs: rows.length, verified: rows.filter(r => r.quality === 'verified').length,
232
+ failed: rows.filter(r => r.quality === 'failed').length, blocked: rows.filter(r => r.quality === 'blocked').length,
233
+ unverifiedOrInvalidated: rows.filter(r => !['verified', 'failed', 'blocked'].includes(r.quality)).length,
234
+ userAccepted: rows.filter(r => r.review.status === 'accepted').length }, notes: [...new Set(notes)] };
235
+ }