@hassangameryt/freeflow 0.8.0 → 0.9.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/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +96 -149
- package/capabilities/cognitive-routing/SKILL.md +21 -129
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +3 -3
- package/capabilities/compaction/references/summary-format.md +26 -15
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +6 -54
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +69 -77
- package/package.json +25 -8
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/cognitive-routing/config.js +3 -4
- package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
- package/pi-extension/dist/cognitive-routing/gate.js +20 -5
- package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
- package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
- package/pi-extension/dist/cognitive-routing/projection.js +23 -6
- package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
- package/pi-extension/dist/cognitive-routing/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
- package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
- package/pi-extension/dist/cognitive-routing/session.js +3 -1
- package/pi-extension/dist/cognitive-routing/sources.js +29 -4
- package/pi-extension/dist/cognitive-routing/state.js +18 -8
- package/pi-extension/dist/cognitive-routing/status.js +4 -3
- package/pi-extension/dist/cognitive-routing/tools.js +29 -15
- package/pi-extension/dist/cognitive-routing/types.js +10 -1
- package/pi-extension/dist/compaction/carry.js +36 -22
- package/pi-extension/dist/compaction/config.js +4 -2
- package/pi-extension/dist/compaction/controller.js +52 -29
- package/pi-extension/dist/compaction/harness.js +19 -3
- package/pi-extension/dist/compaction/results.js +14 -13
- package/pi-extension/dist/compaction/tool.js +6 -6
- package/pi-extension/dist/host/catalog.js +20 -35
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +78 -91
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +1 -1
- package/pi-extension/dist/host/prompts.js +24 -13
- package/pi-extension/dist/host/read-only-session.js +6 -1
- package/pi-extension/dist/host/request-history.js +7 -3
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +17 -2
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
- package/pi-extension/dist/host/settings/settings-view.js +27 -8
- package/pi-extension/dist/host/status.js +19 -5
- package/pi-extension/dist/index.js +638 -72
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
- package/pi-extension/dist/tool-execution/background.js +220 -28
- package/pi-extension/dist/tool-execution/config.js +14 -3
- package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +2 -2
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +24 -14
- package/runtime/prompts/interaction-contract.md +7 -4
- package/runtime/prompts/tool-execution.md +3 -16
- package/runtime/prompts/working-method.md +16 -99
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -104
- package/skills/execute-work/references/code-practices.md +27 -51
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +89 -65
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +68 -17
- package/skills/track-work/SKILL.md +52 -20
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +36 -18
- package/skills/track-work/scripts/lib/document.mjs +17 -13
- package/skills/track-work/scripts/lib/format.mjs +2 -0
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +46 -15
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +232 -13
- package/skills/verify-work/SKILL.md +45 -37
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +25 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +113 -25
- package/skills/write-docs/references/documentation-shapes.md +68 -3
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +96 -6
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +92 -52
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +51 -8
- package/capabilities/cognitive-routing/references/both-mode.md +0 -36
- package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
- package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
- package/plugin.json +0 -33
- package/skills/action-selection/SKILL.md +0 -92
- package/skills/action-selection/references/trajectory-stalls.md +0 -73
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -93
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -94
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -87
- package/skills/design-for-depth/SKILL.md +0 -108
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -105
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
- package/skills/diagnose-failure/SKILL.md +0 -140
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -140
- package/skills/evaluate-skill/SKILL.md +0 -188
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
- package/skills/evaluate-skill/references/review-and-revision.md +0 -94
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
- package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
- package/skills/execute-work/references/execute-work-edges.md +0 -78
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -143
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -67
- package/skills/setup-freeflow/references/activation-contract.md +0 -103
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
- package/skills/verify-work/references/integration-evidence.md +0 -49
- package/skills/verify-work/references/performance-evidence.md +0 -59
- package/skills/workflow/SKILL.md +0 -171
- package/skills/workflow/references/domain-skill-composition.md +0 -45
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -106
- package/skills/write-skill/references/agent-first-instructions.md +0 -133
- package/skills/write-skill/references/progressive-disclosure.md +0 -129
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
- package/skills/write-spec/SKILL.md +0 -85
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -80
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Bounded queues for work the evaluator runs at once: subjects (model calls, `--jobs`) and local CPU work that is
|
|
2
|
+
// not a model call (`--cpu-jobs`), such as command graders and container work. Waiters start in arrival order.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A FIFO limiter: `run(work)` starts `work` once fewer than `limit` are running. `peak` is the most that ever ran
|
|
6
|
+
* at once, recorded so a test or a summary can show the bound held.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} limit
|
|
9
|
+
*/
|
|
10
|
+
export function createLimiter(limit) {
|
|
11
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("a limiter needs a positive integer limit");
|
|
12
|
+
const waiting = [];
|
|
13
|
+
const state = { limit, active: 0, peak: 0 };
|
|
14
|
+
const release = () => {
|
|
15
|
+
state.active -= 1;
|
|
16
|
+
const next = waiting.shift();
|
|
17
|
+
if (next) next();
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
get limit() {
|
|
21
|
+
return state.limit;
|
|
22
|
+
},
|
|
23
|
+
get active() {
|
|
24
|
+
return state.active;
|
|
25
|
+
},
|
|
26
|
+
get peak() {
|
|
27
|
+
return state.peak;
|
|
28
|
+
},
|
|
29
|
+
/** @template T @param {() => Promise<T>} work @returns {Promise<T>} */
|
|
30
|
+
async run(work) {
|
|
31
|
+
if (state.active >= state.limit) await new Promise((resolve) => waiting.push(resolve));
|
|
32
|
+
state.active += 1;
|
|
33
|
+
state.peak = Math.max(state.peak, state.active);
|
|
34
|
+
try {
|
|
35
|
+
return await work();
|
|
36
|
+
} finally {
|
|
37
|
+
release();
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_CPU_JOBS = 2;
|
|
44
|
+
let cpu = createLimiter(DEFAULT_CPU_JOBS);
|
|
45
|
+
|
|
46
|
+
/** Sets how much local CPU work runs at once (`--cpu-jobs`); call before any work is queued. */
|
|
47
|
+
export function setCpuJobs(limit) {
|
|
48
|
+
cpu = createLimiter(limit);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Runs `work` in the shared CPU queue, so graders and containers never exceed `--cpu-jobs` at once. */
|
|
52
|
+
export function withCpuSlot(work) {
|
|
53
|
+
return cpu.run(work);
|
|
54
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { copyFile, mkdir, mkdtemp, readdir, rm, stat, writeFile, cp } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
import { armNames } from "./arms.mjs";
|
|
8
|
+
import { runCommandExpectation } from "./grade-command.mjs";
|
|
9
|
+
import { gradeDeterministic } from "./grade.mjs";
|
|
10
|
+
import { fingerprintDirectory, WORKSPACE, workspaceChanges } from "./sandbox.mjs";
|
|
11
|
+
import { taskCopyGaps } from "./task-copy.mjs";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
// Expectation kinds that grade a workspace or a command, which a reference solution can satisfy without a model.
|
|
16
|
+
// Interaction kinds (reads, responses, tool calls, context) need a transcript, so they are not checkable here.
|
|
17
|
+
const WORKSPACE_KINDS = new Set(["path", "changed-paths", "file-text", "json", "command"]);
|
|
18
|
+
// States that refuse a run unless --skip-reference-check: the reference fails, or the task needs no change.
|
|
19
|
+
const REFUSED = new Set(["unsolvable", "already-solved"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates `group.reference_solution` and returns a problem `{ code, message }`, or null when it is valid or absent.
|
|
23
|
+
* Called from definitions.mjs; a reference must stay in the definition root and must not sit inside the fixture,
|
|
24
|
+
* which the subject sees.
|
|
25
|
+
*/
|
|
26
|
+
export function validateReferenceSolution(value, { root, fixture, arms }) {
|
|
27
|
+
if (value === undefined) return null;
|
|
28
|
+
const invalid = (message) => ({ code: "invalid-definition", message: `group.reference_solution ${message}` });
|
|
29
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
30
|
+
return invalid("must be an object with exactly one of patch or files");
|
|
31
|
+
}
|
|
32
|
+
const sources = Object.keys(value).filter((key) => key !== "arm");
|
|
33
|
+
if (sources.length !== 1 || !["patch", "files"].includes(sources[0])) {
|
|
34
|
+
return invalid("must declare exactly one of patch or files, and may also declare arm");
|
|
35
|
+
}
|
|
36
|
+
if (value.arm !== undefined && !(arms ?? []).includes(value.arm)) {
|
|
37
|
+
return invalid(`arm must name one of group.arms: ${(arms ?? []).join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
const keys = sources;
|
|
40
|
+
const declared = value[keys[0]];
|
|
41
|
+
if (typeof declared !== "string" || declared === "") return invalid(`${keys[0]} must be a path string`);
|
|
42
|
+
const normalized = path.normalize(declared);
|
|
43
|
+
if (
|
|
44
|
+
path.isAbsolute(declared) ||
|
|
45
|
+
normalized === "." ||
|
|
46
|
+
normalized === ".." ||
|
|
47
|
+
normalized.startsWith(`..${path.sep}`)
|
|
48
|
+
) {
|
|
49
|
+
return { code: "unsafe-path", message: `group.reference_solution.${keys[0]} must stay inside the definition root` };
|
|
50
|
+
}
|
|
51
|
+
const absolute = path.resolve(root, declared);
|
|
52
|
+
if (fixture !== null && fixture !== undefined && isInside(path.resolve(root, fixture), absolute)) {
|
|
53
|
+
return invalid(`${keys[0]} is inside the fixture, which the subject sees`);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The definition-root paths a reference reads, which a run counts as inputs (group.files). */
|
|
59
|
+
export function referenceFiles(group, root) {
|
|
60
|
+
const reference = group.reference_solution;
|
|
61
|
+
if (!reference) return [];
|
|
62
|
+
const declared = reference.patch ?? reference.files;
|
|
63
|
+
return [path.resolve(root, declared)];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks one group's reference against its workspace and command expectations, with no model and no subject.
|
|
68
|
+
* Every arm's applicable expectations are graded on the one reference workspace (arms share the fixture), unless
|
|
69
|
+
* reference_solution.arm names one arm. The same graders also run on the unchanged fixture, which they must fail:
|
|
70
|
+
* a task already solved before any change measures nothing. Command graders of an arm with a container environment
|
|
71
|
+
* run in its image. Returns `{ id, state, reason, arms }`: solvable only when every checked arm passes on the
|
|
72
|
+
* reference and fails before it; unsolvable when an arm fails on the reference; already-solved when an arm passes
|
|
73
|
+
* on the unchanged fixture; not-checkable otherwise.
|
|
74
|
+
*/
|
|
75
|
+
export async function checkGroupReference(group, { root, signal } = {}) {
|
|
76
|
+
const base = { id: group.id };
|
|
77
|
+
if (!group.reference_solution) return { ...base, state: "not-checkable", reason: "no reference_solution declared" };
|
|
78
|
+
if (group.type !== "body") return { ...base, state: "not-checkable", reason: "reference checks need a body group" };
|
|
79
|
+
|
|
80
|
+
const arms = group.reference_solution.arm !== undefined ? [group.reference_solution.arm] : armNames(group);
|
|
81
|
+
const byArm = arms
|
|
82
|
+
.map((arm) => ({
|
|
83
|
+
arm,
|
|
84
|
+
container: group.variants?.[arm]?.shell?.container,
|
|
85
|
+
expectations: group.expectations.filter(
|
|
86
|
+
(expectation) => expectation.variant === arm && WORKSPACE_KINDS.has(expectation.kind),
|
|
87
|
+
),
|
|
88
|
+
}))
|
|
89
|
+
.filter((entry) => entry.expectations.length > 0);
|
|
90
|
+
if (byArm.length === 0) {
|
|
91
|
+
return { ...base, state: "not-checkable", reason: "no workspace or command expectation for any checked arm" };
|
|
92
|
+
}
|
|
93
|
+
const work = await mkdtemp(path.join(os.tmpdir(), "skill-eval-reference-"));
|
|
94
|
+
try {
|
|
95
|
+
const unready = await containerReadiness(group, byArm, work);
|
|
96
|
+
if (unready !== null) return { ...base, ...unready };
|
|
97
|
+
const applied = await applyReference(group, { root, workspace: path.join(work, "after", "workspace") });
|
|
98
|
+
if (applied.error) return { ...base, state: "unsolvable", reason: applied.error, arms: [] };
|
|
99
|
+
const after = await gradeWorkspace(group, byArm, { root, signal, directory: path.join(work, "after"), ...applied });
|
|
100
|
+
const fixture = await copyFixture(group, { root, workspace: path.join(work, "before", "workspace") });
|
|
101
|
+
const before = await gradeWorkspace(group, byArm, {
|
|
102
|
+
root,
|
|
103
|
+
signal,
|
|
104
|
+
directory: path.join(work, "before"),
|
|
105
|
+
...fixture,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const armResults = byArm.map(({ arm }, index) => ({
|
|
109
|
+
...armOutcome(arm, after[index]),
|
|
110
|
+
before: beforeOutcome(before[index], after[index]),
|
|
111
|
+
}));
|
|
112
|
+
const summarize = (entries, describe) => entries.map((entry) => `${entry.arm}: ${describe(entry)}`).join("; ");
|
|
113
|
+
const unsolvable = armResults.filter((entry) => entry.state === "unsolvable");
|
|
114
|
+
if (unsolvable.length > 0) {
|
|
115
|
+
return { ...base, state: "unsolvable", reason: summarize(unsolvable, (entry) => entry.reason), arms: armResults };
|
|
116
|
+
}
|
|
117
|
+
const uncheckable = armResults.filter((entry) => entry.state === "not-checkable");
|
|
118
|
+
if (uncheckable.length > 0) {
|
|
119
|
+
return {
|
|
120
|
+
...base,
|
|
121
|
+
state: "not-checkable",
|
|
122
|
+
reason: summarize(uncheckable, (entry) => entry.reason),
|
|
123
|
+
arms: armResults,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const solved = armResults.filter((entry) => entry.before.state === "already-solved");
|
|
127
|
+
if (solved.length > 0) {
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
state: "already-solved",
|
|
131
|
+
reason: summarize(solved, () => "every check already passes on the unchanged fixture"),
|
|
132
|
+
arms: armResults,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { ...base, state: "solvable", reason: null, arms: armResults };
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(work, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* For a container arm, the docker CLI, engine and image must be ready, and the engine must see `workspaceRoot`, where
|
|
143
|
+
* the graded copies live; otherwise the task is not checkable here.
|
|
144
|
+
*/
|
|
145
|
+
async function containerReadiness(group, byArm, workspaceRoot) {
|
|
146
|
+
for (const { arm, container, expectations } of byArm) {
|
|
147
|
+
if (container === undefined || !expectations.some((expectation) => expectation.kind === "command")) continue;
|
|
148
|
+
const { missingContainerPrerequisites } = await import("./container.mjs");
|
|
149
|
+
const { packageManager, prerequisiteRefusal } = await import("./shell.mjs");
|
|
150
|
+
const missing = await missingContainerPrerequisites(container, await packageManager(process.env), workspaceRoot);
|
|
151
|
+
if (missing.length > 0) {
|
|
152
|
+
return {
|
|
153
|
+
state: "not-checkable",
|
|
154
|
+
reason: prerequisiteRefusal(group.id, arm, group.variants[arm].shell, missing),
|
|
155
|
+
missing,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Runs each arm's command graders (in the arm's image when it declares a container) and grades its expectations on
|
|
164
|
+
* one workspace. Comparisons compare arms, so the check drops them and grades each expectation on its own.
|
|
165
|
+
*/
|
|
166
|
+
async function gradeWorkspace(group, byArm, { root, signal, directory, after, changes }) {
|
|
167
|
+
const run = {
|
|
168
|
+
state: "complete",
|
|
169
|
+
workspace: path.join(directory, "workspace"),
|
|
170
|
+
effects: { after, changes },
|
|
171
|
+
isolation: { root: path.join(directory, "isolation") },
|
|
172
|
+
postRun: { commands: {} },
|
|
173
|
+
};
|
|
174
|
+
const results = [];
|
|
175
|
+
for (const { arm, container, expectations } of byArm) {
|
|
176
|
+
for (const expectation of expectations) {
|
|
177
|
+
if (expectation.kind !== "command") continue;
|
|
178
|
+
run.postRun.commands[expectation.id] = await runCommandExpectation({
|
|
179
|
+
expectation,
|
|
180
|
+
run,
|
|
181
|
+
root,
|
|
182
|
+
directory,
|
|
183
|
+
signal,
|
|
184
|
+
container,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const graded = {
|
|
188
|
+
...group,
|
|
189
|
+
expectations: expectations.map(({ comparison: _comparison, ...expectation }) => expectation),
|
|
190
|
+
};
|
|
191
|
+
results.push(await gradeDeterministic(graded, { [arm]: run }, {}));
|
|
192
|
+
}
|
|
193
|
+
return results;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The same graders on the unchanged fixture. They must not all pass; for test graders, each required test that
|
|
198
|
+
* fails before and passes on the reference is fail-to-pass, and one that passes both times is pass-to-pass.
|
|
199
|
+
*/
|
|
200
|
+
function beforeOutcome(before, after) {
|
|
201
|
+
const checks = before.checks.map(({ id, state }) => ({ id, state }));
|
|
202
|
+
const solved =
|
|
203
|
+
before.errors.length === 0 && before.checks.length > 0 && before.checks.every((check) => check.state === "pass");
|
|
204
|
+
const tests = {};
|
|
205
|
+
for (const check of before.checks) {
|
|
206
|
+
const required = check.observed?.tests?.required;
|
|
207
|
+
if (!Array.isArray(required)) continue;
|
|
208
|
+
const afterRequired = after.checks.find((entry) => entry.id === check.id)?.observed?.tests?.required ?? [];
|
|
209
|
+
const passedAfter = new Set(afterRequired.filter((entry) => entry.state === "pass").map((entry) => entry.name));
|
|
210
|
+
tests[check.id] = {
|
|
211
|
+
failToPass: required
|
|
212
|
+
.filter((entry) => entry.state !== "pass" && passedAfter.has(entry.name))
|
|
213
|
+
.map((entry) => entry.name),
|
|
214
|
+
passToPass: required
|
|
215
|
+
.filter((entry) => entry.state === "pass" && passedAfter.has(entry.name))
|
|
216
|
+
.map((entry) => entry.name),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { state: solved ? "already-solved" : "fails-before", checks, tests };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** One arm's outcome: unsolvable on a failed check or grader error, not-checkable when a check could not run. */
|
|
223
|
+
function armOutcome(arm, result) {
|
|
224
|
+
const checks = result.checks;
|
|
225
|
+
const failing = [
|
|
226
|
+
...checks.filter((check) => check.state === "fail").map((check) => `${check.id}: failed`),
|
|
227
|
+
...result.errors.map((error) => `${error.id ?? "system"}: ${error.reason}`),
|
|
228
|
+
];
|
|
229
|
+
if (failing.length > 0) return { arm, state: "unsolvable", reason: failing.join("; "), checks };
|
|
230
|
+
const unavailable = checks.filter((check) => check.state === "unavailable").map((check) => check.id);
|
|
231
|
+
if (unavailable.length > 0)
|
|
232
|
+
return { arm, state: "not-checkable", reason: `unavailable: ${unavailable.join(", ")}`, checks };
|
|
233
|
+
if (checks.length === 0) return { arm, state: "not-checkable", reason: "no check could run", checks };
|
|
234
|
+
return { arm, state: "solvable", reason: null, checks };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Checks every selected group that declares a reference (groups without one are reported as not checkable), and
|
|
239
|
+
* lists for every group what its task copy lacks against the original setting (`copy`, from task-copy.mjs).
|
|
240
|
+
*/
|
|
241
|
+
export async function checkSelection(selection, { root, signal } = {}) {
|
|
242
|
+
const results = [];
|
|
243
|
+
for (const { group } of selection.groups) {
|
|
244
|
+
const result = await checkGroupReference(group, { root, signal });
|
|
245
|
+
results.push({ ...result, copy: await taskCopyGaps(group, { root }) });
|
|
246
|
+
}
|
|
247
|
+
return results;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Before any subject starts: checks each selected group's reference and records the result in its group
|
|
252
|
+
* directory. An unsolvable or already-solved task refuses the run unless skip is set; the refusal names why.
|
|
253
|
+
*/
|
|
254
|
+
export async function enforceReferenceChecks(selection, { root, resultDirectory, skip = false, signal }) {
|
|
255
|
+
for (const { group } of selection.groups) {
|
|
256
|
+
if (!group.reference_solution) continue;
|
|
257
|
+
const result = await checkGroupReference(group, { root, signal });
|
|
258
|
+
const groupDirectory = path.join(resultDirectory, "groups", group.id);
|
|
259
|
+
await mkdir(groupDirectory, { recursive: true });
|
|
260
|
+
await writeFile(
|
|
261
|
+
path.join(groupDirectory, "reference-check.json"),
|
|
262
|
+
`${JSON.stringify({ ...result, skipped: skip && REFUSED.has(result.state) }, null, 2)}\n`,
|
|
263
|
+
);
|
|
264
|
+
if (REFUSED.has(result.state) && !skip) {
|
|
265
|
+
// Named with its file, so a suite with many groups shows which one to fix.
|
|
266
|
+
const task = `${group.id} (${path.relative(root, group.files[0])})`;
|
|
267
|
+
const problem =
|
|
268
|
+
result.state === "unsolvable"
|
|
269
|
+
? `reference solution for ${task} does not solve the task`
|
|
270
|
+
: `task ${task} is already solved before any change, so it cannot show one`;
|
|
271
|
+
throw new Error(
|
|
272
|
+
`${problem}: ${result.reason}; run skill-eval check-tasks for details, or pass --skip-reference-check`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Builds the reference workspace in `workspace`: copies the fixture, then applies the patch or overlays the files.
|
|
280
|
+
* Returns the fingerprint evidence the workspace graders read, or an error string when the reference cannot apply.
|
|
281
|
+
*/
|
|
282
|
+
async function applyReference(group, { root, workspace }) {
|
|
283
|
+
const { after: before } = await copyFixture(group, { root, workspace });
|
|
284
|
+
const reference = group.reference_solution;
|
|
285
|
+
if (reference.patch !== undefined) {
|
|
286
|
+
const patch = path.resolve(root, reference.patch);
|
|
287
|
+
try {
|
|
288
|
+
await execFileAsync("git", ["apply", patch], { cwd: workspace });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
const detail = String(error?.stderr ?? error?.message ?? error).trim();
|
|
291
|
+
return { error: `the reference patch does not apply to the fixture: ${detail}` };
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
const source = path.resolve(root, reference.files);
|
|
295
|
+
if (!(await stat(source).catch(() => null))?.isDirectory()) {
|
|
296
|
+
return { error: `reference files is not a directory: ${reference.files}` };
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
await overlay(source, workspace);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const after = await fingerprintDirectory(workspace, WORKSPACE);
|
|
305
|
+
return { after, changes: workspaceChanges(before, after) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Copies the fixture into `workspace` unchanged; the evidence shows no change. */
|
|
309
|
+
async function copyFixture(group, { root, workspace }) {
|
|
310
|
+
if (group.fixture !== null) {
|
|
311
|
+
await cp(path.resolve(root, group.fixture), workspace, {
|
|
312
|
+
recursive: true,
|
|
313
|
+
dereference: false,
|
|
314
|
+
verbatimSymlinks: true,
|
|
315
|
+
force: false,
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
await mkdir(workspace, { recursive: true });
|
|
319
|
+
}
|
|
320
|
+
const after = await fingerprintDirectory(workspace, WORKSPACE);
|
|
321
|
+
return { after, changes: workspaceChanges(after, after) };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Copies a directory tree over the workspace. Symbolic links and other special entries are refused. */
|
|
325
|
+
async function overlay(source, destination) {
|
|
326
|
+
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
327
|
+
const from = path.join(source, entry.name);
|
|
328
|
+
const to = path.join(destination, entry.name);
|
|
329
|
+
if (entry.isDirectory()) {
|
|
330
|
+
await mkdir(to, { recursive: true });
|
|
331
|
+
await overlay(from, to);
|
|
332
|
+
} else if (entry.isFile()) {
|
|
333
|
+
await copyFile(from, to);
|
|
334
|
+
} else {
|
|
335
|
+
throw new Error(`reference files may contain only regular files and directories: ${entry.name}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isInside(root, target) {
|
|
341
|
+
const relative = path.relative(root, target);
|
|
342
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
343
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Reuse (E-1): a complete, canonical run can stand in for a new trial of the same arm when nothing that could
|
|
2
|
+
// change its behavior changed. Each such run is indexed in `<results>/index.jsonl` under a key over the group's
|
|
3
|
+
// task and inputs, the arm's effective configuration and inputs, the model, the host version and this
|
|
4
|
+
// evaluator's own source, whenever a canonical run completes. `--reuse` counts a matching run instead of running
|
|
5
|
+
// the arm again.
|
|
6
|
+
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { appendFile, cp, readdir, readFile, readlink, rm } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
|
|
14
|
+
import { isSourceExtension } from "./build-source.mjs";
|
|
15
|
+
import { unclearedFlags } from "./scan.mjs";
|
|
16
|
+
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const SCRIPTS = fileURLToPath(new URL("../", import.meta.url));
|
|
19
|
+
export const INDEX_FILE = "index.jsonl";
|
|
20
|
+
// Group fields that describe arms rather than the task; each arm's effective configuration is hashed instead.
|
|
21
|
+
const ARM_FIELDS = new Set([
|
|
22
|
+
"arms",
|
|
23
|
+
"configuration",
|
|
24
|
+
"configurations",
|
|
25
|
+
"variants",
|
|
26
|
+
"model",
|
|
27
|
+
"files",
|
|
28
|
+
"component",
|
|
29
|
+
"reference",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function isPlainObject(value) {
|
|
33
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function canonicalJson(value) {
|
|
37
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
38
|
+
if (isPlainObject(value)) {
|
|
39
|
+
const keys = Object.keys(value)
|
|
40
|
+
.filter((key) => value[key] !== undefined)
|
|
41
|
+
.sort();
|
|
42
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(value ?? null);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sha256(value) {
|
|
48
|
+
return createHash("sha256").update(value).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A content hash of a file or directory tree: relative paths, file bytes and link targets, in sorted order. */
|
|
52
|
+
export async function contentHash(target) {
|
|
53
|
+
const hash = createHash("sha256");
|
|
54
|
+
const visit = async (absolute, relative) => {
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
entries = await readdir(absolute, { withFileTypes: true });
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.code !== "ENOTDIR") {
|
|
60
|
+
hash.update(`missing:${relative}\0`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
hash
|
|
64
|
+
.update(`file:${relative}\0`)
|
|
65
|
+
.update(await readFile(absolute))
|
|
66
|
+
.update("\0");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
hash.update(`dir:${relative}\0`);
|
|
70
|
+
for (const entry of entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))) {
|
|
71
|
+
const child = path.join(absolute, entry.name);
|
|
72
|
+
const name = relative === "" ? entry.name : `${relative}/${entry.name}`;
|
|
73
|
+
if (entry.isSymbolicLink()) hash.update(`link:${name}\0${await readlink(child)}\0`);
|
|
74
|
+
else await visit(child, name);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
await visit(target, "");
|
|
78
|
+
return hash.digest("hex");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let evaluatorVersionPromise = null;
|
|
82
|
+
|
|
83
|
+
/** This evaluator's version: a content hash of its scripts directory, computed once per process. */
|
|
84
|
+
export function evaluatorVersion() {
|
|
85
|
+
evaluatorVersionPromise ??= contentHash(SCRIPTS);
|
|
86
|
+
return evaluatorVersionPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function gitCommit(root, ref) {
|
|
90
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`], {
|
|
91
|
+
cwd: root,
|
|
92
|
+
});
|
|
93
|
+
return stdout.trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The reuse key of one arm of a loaded group. Working-tree inputs are identified by their content, and Git
|
|
98
|
+
* sources (the arm's and Freeflow's) by the commit their ref resolves to, so a change anywhere else in the
|
|
99
|
+
* repository does not invalidate a run.
|
|
100
|
+
*
|
|
101
|
+
* @param {{group: any, arm: string, root: string, hostVersion: string}} input
|
|
102
|
+
*/
|
|
103
|
+
export async function reuseKey({ group, arm, root, hostVersion }) {
|
|
104
|
+
const task = Object.fromEntries(Object.entries(group).filter(([key]) => !ARM_FIELDS.has(key)));
|
|
105
|
+
const groupInputs = [];
|
|
106
|
+
if (typeof group.fixture === "string") groupInputs.push(group.fixture);
|
|
107
|
+
for (const bundle of group.runtime?.extensions ?? []) groupInputs.push(...(bundle.resources ?? []));
|
|
108
|
+
const inputHashes = {};
|
|
109
|
+
for (const input of groupInputs) inputHashes[input] = await contentHash(path.resolve(root, input));
|
|
110
|
+
// The first file is the group definition itself, already hashed by its parsed fields; the rest are files its
|
|
111
|
+
// configuration extends and its command graders read.
|
|
112
|
+
for (const file of (group.files ?? []).slice(1)) {
|
|
113
|
+
inputHashes[path.relative(root, file).split(path.sep).join("/")] = await contentHash(file);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const variant = group.variants?.[arm];
|
|
117
|
+
if (variant === undefined) throw new Error(`group ${group.id} has no arm ${arm}`);
|
|
118
|
+
const armInputs = {};
|
|
119
|
+
if (variant.source?.kind === "working-tree") {
|
|
120
|
+
for (const entry of [...(variant.skills ?? []), ...(variant.context ?? [])]) {
|
|
121
|
+
armInputs[entry] = await contentHash(path.resolve(root, entry));
|
|
122
|
+
}
|
|
123
|
+
} else if (variant.source?.kind === "git") {
|
|
124
|
+
armInputs.source = await gitCommit(root, variant.source.ref);
|
|
125
|
+
}
|
|
126
|
+
if (variant.freeflow?.source?.ref) armInputs.freeflow = await gitCommit(root, variant.freeflow.source.ref);
|
|
127
|
+
// Host extensions built from source: a working-tree directory by content, a Git source by its commit.
|
|
128
|
+
for (const [index, extension] of (variant.hostExtensions ?? []).entries()) {
|
|
129
|
+
if (!isSourceExtension(extension)) continue;
|
|
130
|
+
armInputs[`hostExtensions.${index}`] =
|
|
131
|
+
extension.source === undefined
|
|
132
|
+
? await contentHash(path.resolve(root, extension.path))
|
|
133
|
+
: await gitCommit(root, extension.source.ref);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return sha256(
|
|
137
|
+
canonicalJson({
|
|
138
|
+
task: sha256(canonicalJson(task)),
|
|
139
|
+
groupInputs: inputHashes,
|
|
140
|
+
arm,
|
|
141
|
+
configuration: sha256(canonicalJson(variant)),
|
|
142
|
+
armInputs,
|
|
143
|
+
model: variant.model ?? null,
|
|
144
|
+
hostVersion,
|
|
145
|
+
evaluator: await evaluatorVersion(),
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The index as a map from key to its entries, oldest first; an unreadable line is skipped. */
|
|
151
|
+
export async function readIndex(resultsDirectory) {
|
|
152
|
+
const index = new Map();
|
|
153
|
+
let text = "";
|
|
154
|
+
try {
|
|
155
|
+
text = await readFile(path.join(resultsDirectory, INDEX_FILE), "utf8");
|
|
156
|
+
} catch {
|
|
157
|
+
return index;
|
|
158
|
+
}
|
|
159
|
+
for (const line of text.split("\n")) {
|
|
160
|
+
if (line.trim() === "") continue;
|
|
161
|
+
try {
|
|
162
|
+
const entry = JSON.parse(line);
|
|
163
|
+
if (typeof entry?.key !== "string") continue;
|
|
164
|
+
if (!index.has(entry.key)) index.set(entry.key, []);
|
|
165
|
+
index.get(entry.key).push(entry);
|
|
166
|
+
} catch {
|
|
167
|
+
// A torn or foreign line never blocks the rest of the index.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return index;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Appends index entries, one JSON line each. */
|
|
174
|
+
export async function appendIndex(resultsDirectory, entries) {
|
|
175
|
+
if (entries.length === 0) return;
|
|
176
|
+
await appendFile(
|
|
177
|
+
path.join(resultsDirectory, INDEX_FILE),
|
|
178
|
+
entries.map((entry) => `${JSON.stringify(entry)}\n`).join(""),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readJson(file) {
|
|
183
|
+
try {
|
|
184
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
185
|
+
return isPlainObject(value) ? value : null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The reuse state shared by every invocation of one run or aggregate: whether `--reuse` is on (only then does it
|
|
193
|
+
* claim indexed runs; keys are computed either way, so new runs are indexed), the index as it stood when the run
|
|
194
|
+
* began, each host's version, and the runs already reused, so none is reused twice.
|
|
195
|
+
*
|
|
196
|
+
* @param {{resultsDirectory: string, enabled: boolean, hostVersion: (host: string) => Promise<string | null>}} options
|
|
197
|
+
*/
|
|
198
|
+
export function createReuse({ resultsDirectory, enabled, hostVersion }) {
|
|
199
|
+
let index = null;
|
|
200
|
+
const versions = new Map();
|
|
201
|
+
const used = new Set();
|
|
202
|
+
const reused = [];
|
|
203
|
+
return {
|
|
204
|
+
enabled,
|
|
205
|
+
resultsDirectory,
|
|
206
|
+
reused,
|
|
207
|
+
async hostVersion(host = "pi") {
|
|
208
|
+
if (!versions.has(host)) versions.set(host, await hostVersion(host).catch(() => null));
|
|
209
|
+
return versions.get(host);
|
|
210
|
+
},
|
|
211
|
+
async keyFor(group, arm, root) {
|
|
212
|
+
const host = await this.hostVersion(group.host ?? group.runtime?.host ?? "pi");
|
|
213
|
+
if (host === null) return null;
|
|
214
|
+
return reuseKey({ group, arm, root, hostVersion: host });
|
|
215
|
+
},
|
|
216
|
+
/**
|
|
217
|
+
* Claims an indexed run for this key that this run has not used, whose run is still complete, not held for
|
|
218
|
+
* transcript flags no person has cleared, and whose group grade is still readable; null when there is none. The
|
|
219
|
+
* claim happens before any wait, so parallel groups never claim the same run.
|
|
220
|
+
*/
|
|
221
|
+
async claim(key) {
|
|
222
|
+
if (!enabled || key === null) return null;
|
|
223
|
+
index ??= readIndex(resultsDirectory);
|
|
224
|
+
const entries = (await index).get(key) ?? [];
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const origin = `${entry.result}/${entry.group}/${entry.arm}`;
|
|
227
|
+
if (used.has(origin)) continue;
|
|
228
|
+
used.add(origin);
|
|
229
|
+
const groupDirectory = path.join(resultsDirectory, entry.result, "groups", entry.group);
|
|
230
|
+
const run = await readJson(path.join(groupDirectory, entry.arm, "run.json"));
|
|
231
|
+
const grade = await readJson(path.join(groupDirectory, "deterministic-grade.json"));
|
|
232
|
+
const summary = await readJson(path.join(resultsDirectory, entry.result, "summary.json"));
|
|
233
|
+
if (run?.state !== "complete" || grade === null || grade.state === "grade-error") continue;
|
|
234
|
+
// A held run counts toward nothing until a person clears its flags, so it cannot stand in for a new one.
|
|
235
|
+
if ((await unclearedFlags(run, path.join(groupDirectory, entry.arm))).length > 0) continue;
|
|
236
|
+
if (summary === null || summary.dryRun !== false) continue;
|
|
237
|
+
return { ...entry, directory: path.join(groupDirectory, entry.arm) };
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function remapPaths(value, from, to) {
|
|
245
|
+
if (typeof value === "string") {
|
|
246
|
+
return value === from || value.startsWith(`${from}${path.sep}`) ? `${to}${value.slice(from.length)}` : value;
|
|
247
|
+
}
|
|
248
|
+
if (Array.isArray(value)) return value.map((entry) => remapPaths(entry, from, to));
|
|
249
|
+
if (isPlainObject(value)) {
|
|
250
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, remapPaths(entry, from, to)]));
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Copies a claimed run's directory into the new trial and returns it as an in-memory run (transcripts read back,
|
|
257
|
+
* paths moved to the copy), marked `reused` with where it came from, ready to grade and persist like a new run.
|
|
258
|
+
*/
|
|
259
|
+
export async function adoptRun(claimed, variantDirectory) {
|
|
260
|
+
await rm(variantDirectory, { recursive: true, force: true });
|
|
261
|
+
await cp(claimed.directory, variantDirectory, { recursive: true, verbatimSymlinks: true });
|
|
262
|
+
const stored = await readJson(path.join(variantDirectory, "run.json"));
|
|
263
|
+
if (stored === null) throw new Error(`reused run ${claimed.result}/${claimed.group}/${claimed.arm} is unreadable`);
|
|
264
|
+
const transcript = JSON.parse(await readFile(path.join(variantDirectory, "transcript.json"), "utf8"));
|
|
265
|
+
const run = remapPaths(stored, claimed.directory, variantDirectory);
|
|
266
|
+
run.transcript = Array.isArray(transcript) ? transcript : [];
|
|
267
|
+
if (Array.isArray(run.turns)) {
|
|
268
|
+
run.turns = run.turns.map((turn) =>
|
|
269
|
+
isPlainObject(turn.transcript) && turn.transcript.file === "transcript.json"
|
|
270
|
+
? { ...turn, transcript: run.transcript.slice(turn.transcript.start, turn.transcript.end) }
|
|
271
|
+
: turn,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
delete run.artifacts;
|
|
275
|
+
run.reused = { result: claimed.result, group: claimed.group, arm: claimed.arm, key: claimed.key };
|
|
276
|
+
return run;
|
|
277
|
+
}
|