@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,1072 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ARM_NAME, armNames, DEFAULT_REFERENCE } from "./arms.mjs";
|
|
5
|
+
import { decisionProblem, validateDecision } from "./decision.mjs";
|
|
6
|
+
import { isSourceExtension, sourceExtensionInputs, validateSourceExtension } from "./build-source.mjs";
|
|
7
|
+
import { validateCommandExpectations } from "./grade-command.mjs";
|
|
8
|
+
import { expectationProblems } from "./grade.mjs";
|
|
9
|
+
import { containerShell, validateContainerEnvironment } from "./container.mjs";
|
|
10
|
+
import { sealPaths, sealProblem, validateNetwork } from "./seal.mjs";
|
|
11
|
+
import { validateShell, validateShellGroup } from "./shell.mjs";
|
|
12
|
+
import { referenceFiles, validateReferenceSolution } from "./reference.mjs";
|
|
13
|
+
import { rubricFiles, validateJudging } from "./judge.mjs";
|
|
14
|
+
import { participantFiles, validateParticipants } from "./participants.mjs";
|
|
15
|
+
import { claudeCodeGroupProblems, claudeCodeSetupProblems } from "./hosts/claude-code/constraints.mjs";
|
|
16
|
+
import { EVALUATION_HOSTS } from "./hosts/names.mjs";
|
|
17
|
+
|
|
18
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
19
|
+
const EVALUATION_TYPES = new Set(["description", "body"]);
|
|
20
|
+
const PROMPT_MODES = new Set(["isolated", "host"]);
|
|
21
|
+
const HOST_NAMES = [...EVALUATION_HOSTS].join(" or ");
|
|
22
|
+
export const COMMAND_TOOL = "run_command";
|
|
23
|
+
const MAX_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
24
|
+
const RESERVED_ENVIRONMENT_KEYS = new Set([
|
|
25
|
+
"PATH",
|
|
26
|
+
"HOME",
|
|
27
|
+
"PWD",
|
|
28
|
+
"OLDPWD",
|
|
29
|
+
"SHELL",
|
|
30
|
+
"BASH_ENV",
|
|
31
|
+
"NODE_OPTIONS",
|
|
32
|
+
"NODE_PATH",
|
|
33
|
+
"PI_PACKAGE_DIR",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export class DefinitionError extends Error {
|
|
37
|
+
constructor(code, message, definitionPath = null) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "DefinitionError";
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.definitionPath = definitionPath;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fail(code, message, definitionPath) {
|
|
46
|
+
throw new DefinitionError(code, message, definitionPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isPlainObject(value) {
|
|
50
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireObject(value, label, definitionPath) {
|
|
54
|
+
if (!isPlainObject(value)) fail("invalid-definition", `${label} must be an object`, definitionPath);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireExactKeys(value, required, optional, label, definitionPath) {
|
|
59
|
+
const object = requireObject(value, label, definitionPath);
|
|
60
|
+
const allowed = new Set([...required, ...optional]);
|
|
61
|
+
for (const key of required) {
|
|
62
|
+
if (!Object.hasOwn(object, key)) fail("invalid-definition", `${label} is missing ${key}`, definitionPath);
|
|
63
|
+
}
|
|
64
|
+
for (const key of Object.keys(object)) {
|
|
65
|
+
if (!allowed.has(key)) fail("invalid-definition", `${label} contains unsupported field ${key}`, definitionPath);
|
|
66
|
+
}
|
|
67
|
+
return object;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function requireId(value, label, definitionPath) {
|
|
71
|
+
if (typeof value !== "string" || value.length > 64 || !ID.test(value)) {
|
|
72
|
+
fail(
|
|
73
|
+
"invalid-definition",
|
|
74
|
+
`${label} must start with a lowercase letter and use lowercase letters, numbers, or single hyphens up to 64 characters`,
|
|
75
|
+
definitionPath,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireString(value, label, definitionPath) {
|
|
82
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
83
|
+
fail("invalid-definition", `${label} must be a non-empty string`, definitionPath);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requireStringArray(value, label, definitionPath, { paths = false, unique = false } = {}) {
|
|
89
|
+
if (!Array.isArray(value)) fail("invalid-definition", `${label} must be an array`, definitionPath);
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
return value.map((entry, index) => {
|
|
92
|
+
const string = requireString(entry, `${label}[${index}]`, definitionPath);
|
|
93
|
+
if (paths) requireSafeRelativePath(string, `${label}[${index}]`, definitionPath);
|
|
94
|
+
if (unique && seen.has(string)) {
|
|
95
|
+
fail("invalid-definition", `${label} contains duplicate ${string}`, definitionPath);
|
|
96
|
+
}
|
|
97
|
+
seen.add(string);
|
|
98
|
+
return string;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function requireSafeRelativePath(value, label, definitionPath) {
|
|
103
|
+
const string = requireString(value, label, definitionPath);
|
|
104
|
+
const normalized = path.normalize(string);
|
|
105
|
+
if (path.isAbsolute(string) || normalized === "." || normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
|
|
106
|
+
fail("unsafe-path", `${label} must stay inside the definition root`, definitionPath);
|
|
107
|
+
}
|
|
108
|
+
return string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requireSuiteReference(value, label, definitionPath) {
|
|
112
|
+
const string = requireString(value, label, definitionPath);
|
|
113
|
+
if (path.isAbsolute(string) || path.normalize(string) === ".") {
|
|
114
|
+
fail("unsafe-path", `${label} must be relative to the suite file`, definitionPath);
|
|
115
|
+
}
|
|
116
|
+
return string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateInput(value, definitionPath) {
|
|
120
|
+
const input = requireExactKeys(value, [], ["prompt", "turns", "resume_before"], "group.input", definitionPath);
|
|
121
|
+
const hasPrompt = Object.hasOwn(input, "prompt");
|
|
122
|
+
const hasTurns = Object.hasOwn(input, "turns");
|
|
123
|
+
if (hasPrompt === hasTurns) {
|
|
124
|
+
fail("invalid-definition", "group.input must contain exactly one of prompt or turns", definitionPath);
|
|
125
|
+
}
|
|
126
|
+
if (hasPrompt) requireString(input.prompt, "group.input.prompt", definitionPath);
|
|
127
|
+
if (hasTurns) {
|
|
128
|
+
const turns = requireStringArray(input.turns, "group.input.turns", definitionPath);
|
|
129
|
+
if (turns.length === 0) fail("invalid-definition", "group.input.turns must not be empty", definitionPath);
|
|
130
|
+
}
|
|
131
|
+
if (Object.hasOwn(input, "resume_before")) {
|
|
132
|
+
const before = input.resume_before;
|
|
133
|
+
const count = hasTurns ? input.turns.length : 0;
|
|
134
|
+
if (
|
|
135
|
+
!Array.isArray(before) ||
|
|
136
|
+
before.length === 0 ||
|
|
137
|
+
!before.every(
|
|
138
|
+
(turn, i) => Number.isInteger(turn) && turn >= 2 && turn <= count && (i === 0 || turn > before[i - 1]),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
fail(
|
|
142
|
+
"invalid-definition",
|
|
143
|
+
"group.input.resume_before must list turn numbers from 2 to the number of turns, ascending, with group.input.turns",
|
|
144
|
+
definitionPath,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A session resumed between turns must be a body run in a persistent Pi session with one subject. */
|
|
150
|
+
function validateResume(group, internal, definitionPath) {
|
|
151
|
+
if (group.input.resume_before === undefined) return;
|
|
152
|
+
const problem =
|
|
153
|
+
group.type !== "body"
|
|
154
|
+
? "a body group"
|
|
155
|
+
: internal.host !== "pi"
|
|
156
|
+
? "the pi host"
|
|
157
|
+
: internal.runtime?.session !== true
|
|
158
|
+
? "configuration.runtime.session true, so the session has a file to resume"
|
|
159
|
+
: Object.values(internal.variants).some((variant) => variant.participants !== undefined) ||
|
|
160
|
+
group.participants !== undefined
|
|
161
|
+
? "a group without participants"
|
|
162
|
+
: null;
|
|
163
|
+
if (problem !== null) fail("invalid-definition", `group.input.resume_before needs ${problem}`, definitionPath);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateSource(value, label, definitionPath) {
|
|
167
|
+
const source = requireObject(value, label, definitionPath);
|
|
168
|
+
if (source.kind === "working-tree") {
|
|
169
|
+
requireExactKeys(source, ["kind"], [], label, definitionPath);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (source.kind === "git") {
|
|
173
|
+
requireExactKeys(source, ["kind", "ref"], [], label, definitionPath);
|
|
174
|
+
requireString(source.ref, `${label}.ref`, definitionPath);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
fail("invalid-definition", `${label}.kind must be working-tree or git`, definitionPath);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function validateEnvironment(value, label, definitionPath) {
|
|
181
|
+
const environment = requireExactKeys(value, ["source", "skills", "target", "context"], [], label, definitionPath);
|
|
182
|
+
validateSource(environment.source, `${label}.source`, definitionPath);
|
|
183
|
+
const skills = requireStringArray(environment.skills, `${label}.skills`, definitionPath, { paths: true });
|
|
184
|
+
requireStringArray(environment.context, `${label}.context`, definitionPath, { paths: true });
|
|
185
|
+
if (
|
|
186
|
+
environment.target !== null &&
|
|
187
|
+
(!Number.isInteger(environment.target) || environment.target < 0 || environment.target >= skills.length)
|
|
188
|
+
) {
|
|
189
|
+
fail("invalid-definition", `${label}.target must be null or an index into skills`, definitionPath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function validateRuntime(value, definitionPath) {
|
|
194
|
+
const runtime = requireExactKeys(
|
|
195
|
+
value,
|
|
196
|
+
["host", "extensions", "environment", "session"],
|
|
197
|
+
["prompt"],
|
|
198
|
+
"group.runtime",
|
|
199
|
+
definitionPath,
|
|
200
|
+
);
|
|
201
|
+
if (!EVALUATION_HOSTS.has(runtime.host)) {
|
|
202
|
+
fail("invalid-definition", `group.configuration.host.name must be ${HOST_NAMES}`, definitionPath);
|
|
203
|
+
}
|
|
204
|
+
if (Object.hasOwn(runtime, "prompt") && !PROMPT_MODES.has(runtime.prompt)) {
|
|
205
|
+
fail("invalid-definition", "group.runtime.prompt must be isolated or host", definitionPath);
|
|
206
|
+
}
|
|
207
|
+
if (!Array.isArray(runtime.extensions)) {
|
|
208
|
+
fail("invalid-definition", "group.runtime.extensions must be an array", definitionPath);
|
|
209
|
+
}
|
|
210
|
+
const bundleIds = new Set();
|
|
211
|
+
for (const [index, entry] of runtime.extensions.entries()) {
|
|
212
|
+
const bundle = requireExactKeys(
|
|
213
|
+
entry,
|
|
214
|
+
["entry", "resources"],
|
|
215
|
+
[],
|
|
216
|
+
`group.runtime.extensions[${index}]`,
|
|
217
|
+
definitionPath,
|
|
218
|
+
);
|
|
219
|
+
requireSafeRelativePath(bundle.entry, `group.runtime.extensions[${index}].entry`, definitionPath);
|
|
220
|
+
const resources = requireStringArray(
|
|
221
|
+
bundle.resources,
|
|
222
|
+
`group.runtime.extensions[${index}].resources`,
|
|
223
|
+
definitionPath,
|
|
224
|
+
{ paths: true, unique: true },
|
|
225
|
+
);
|
|
226
|
+
if (resources.length === 0) {
|
|
227
|
+
fail("invalid-definition", `group.runtime.extensions[${index}].resources must not be empty`, definitionPath);
|
|
228
|
+
}
|
|
229
|
+
const bundleId = `${bundle.entry}\u0000${resources.join("\u0000")}`;
|
|
230
|
+
if (bundleIds.has(bundleId)) {
|
|
231
|
+
fail("invalid-definition", "group.runtime.extensions contains duplicate bundles", definitionPath);
|
|
232
|
+
}
|
|
233
|
+
bundleIds.add(bundleId);
|
|
234
|
+
}
|
|
235
|
+
if (typeof runtime.session !== "boolean") {
|
|
236
|
+
fail("invalid-definition", "group.runtime.session must be a boolean", definitionPath);
|
|
237
|
+
}
|
|
238
|
+
const environment = requireExactKeys(
|
|
239
|
+
runtime.environment,
|
|
240
|
+
["literal", "inherit"],
|
|
241
|
+
[],
|
|
242
|
+
"group.runtime.environment",
|
|
243
|
+
definitionPath,
|
|
244
|
+
);
|
|
245
|
+
const literal = requireObject(environment.literal, "group.runtime.environment.literal", definitionPath);
|
|
246
|
+
const inherited = requireStringArray(environment.inherit, "group.runtime.environment.inherit", definitionPath, {
|
|
247
|
+
unique: true,
|
|
248
|
+
});
|
|
249
|
+
const literalKeys = new Set(Object.keys(literal).map((key) => key.toUpperCase()));
|
|
250
|
+
for (const [key, value] of Object.entries(literal)) {
|
|
251
|
+
const normalizedKey = key.toUpperCase();
|
|
252
|
+
if (
|
|
253
|
+
!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ||
|
|
254
|
+
isBlockedEnvironmentKey(normalizedKey) ||
|
|
255
|
+
/TOKEN|SECRET|PASSWORD|API[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIAL/i.test(key)
|
|
256
|
+
) {
|
|
257
|
+
fail(
|
|
258
|
+
"invalid-definition",
|
|
259
|
+
`group.runtime.environment.literal contains a reserved or secret-like key: ${key}`,
|
|
260
|
+
definitionPath,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (typeof value !== "string") {
|
|
264
|
+
fail("invalid-definition", `group.runtime.environment.literal.${key} must be a string`, definitionPath);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const key of inherited) {
|
|
268
|
+
const normalizedKey = key.toUpperCase();
|
|
269
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || isBlockedEnvironmentKey(normalizedKey)) {
|
|
270
|
+
fail("invalid-definition", `group.runtime.environment.inherit contains an invalid key: ${key}`, definitionPath);
|
|
271
|
+
}
|
|
272
|
+
if (literalKeys.has(normalizedKey)) {
|
|
273
|
+
fail(
|
|
274
|
+
"invalid-definition",
|
|
275
|
+
`group.runtime.environment key is declared as both literal and inherited: ${key}`,
|
|
276
|
+
definitionPath,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isBlockedEnvironmentKey(normalizedKey) {
|
|
283
|
+
return (
|
|
284
|
+
RESERVED_ENVIRONMENT_KEYS.has(normalizedKey) ||
|
|
285
|
+
normalizedKey.startsWith("SKILL_EVAL_") ||
|
|
286
|
+
normalizedKey.startsWith("PI_") ||
|
|
287
|
+
normalizedKey.startsWith("LD_") ||
|
|
288
|
+
normalizedKey.startsWith("DYLD_")
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Fields each known expectation kind may declare. A field outside this set would be ignored by its grader,
|
|
293
|
+
// so it is rejected before any subject runs. Unknown kinds still surface afterward as grade errors.
|
|
294
|
+
const EXPECTATION_COMMON_FIELDS = ["id", "kind", "variant", "comparison", "turn", "expect"];
|
|
295
|
+
const EXPECTATION_FIELDS = new Map([
|
|
296
|
+
["skill-read", []],
|
|
297
|
+
["resource-read", ["resource", "index", "path"]],
|
|
298
|
+
["path", ["path"]],
|
|
299
|
+
["changed-paths", ["paths"]],
|
|
300
|
+
["file-text", ["path", "value", "line"]],
|
|
301
|
+
["json", ["path", "pointer", "value"]],
|
|
302
|
+
["response-text", ["value"]],
|
|
303
|
+
["context-text", ["value", "surface", "request"]],
|
|
304
|
+
["tool-call", ["tool", "argumentContains", "argumentNotContains"]],
|
|
305
|
+
["command", ["argv", "files", "timeout_seconds", "pass"]],
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
function validateExpectations(value, definitionPath) {
|
|
309
|
+
if (!Array.isArray(value)) fail("invalid-definition", "group.expectations must be an array", definitionPath);
|
|
310
|
+
const ids = new Set();
|
|
311
|
+
for (const [index, entry] of value.entries()) {
|
|
312
|
+
const expectation = requireObject(entry, `group.expectations[${index}]`, definitionPath);
|
|
313
|
+
requireId(expectation.id, `group.expectations[${index}].id`, definitionPath);
|
|
314
|
+
requireString(expectation.kind, `group.expectations[${index}].kind`, definitionPath);
|
|
315
|
+
const kindFields = EXPECTATION_FIELDS.get(expectation.kind);
|
|
316
|
+
if (kindFields) {
|
|
317
|
+
const allowed = new Set([...EXPECTATION_COMMON_FIELDS, ...kindFields]);
|
|
318
|
+
for (const key of Object.keys(expectation)) {
|
|
319
|
+
if (!allowed.has(key)) {
|
|
320
|
+
fail(
|
|
321
|
+
"invalid-definition",
|
|
322
|
+
`group.expectations[${index}] contains unsupported field ${key} for kind ${expectation.kind}`,
|
|
323
|
+
definitionPath,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (ids.has(expectation.id)) {
|
|
329
|
+
fail("invalid-definition", `group.expectations contains duplicate ID ${expectation.id}`, definitionPath);
|
|
330
|
+
}
|
|
331
|
+
ids.add(expectation.id);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Declared commands are fixed by the definition author; the subject may only choose which one runs.
|
|
336
|
+
function validateCommands(group, definitionPath) {
|
|
337
|
+
const toolDeclared = group.tools.includes(COMMAND_TOOL);
|
|
338
|
+
if (!Object.hasOwn(group, "commands")) {
|
|
339
|
+
if (toolDeclared) fail("invalid-definition", `${COMMAND_TOOL} requires declared group.commands`, definitionPath);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (!toolDeclared) fail("invalid-definition", `group.commands requires the ${COMMAND_TOOL} tool`, definitionPath);
|
|
343
|
+
if (group.type !== "body")
|
|
344
|
+
fail("invalid-definition", "group.commands are supported only by body groups", definitionPath);
|
|
345
|
+
if (!Array.isArray(group.commands) || group.commands.length === 0) {
|
|
346
|
+
fail("invalid-definition", "group.commands must be a non-empty array", definitionPath);
|
|
347
|
+
}
|
|
348
|
+
const ids = new Set();
|
|
349
|
+
for (const [index, entry] of group.commands.entries()) {
|
|
350
|
+
const label = `group.commands[${index}]`;
|
|
351
|
+
const command = requireExactKeys(entry, ["id", "argv"], ["timeout_ms"], label, definitionPath);
|
|
352
|
+
requireId(command.id, `${label}.id`, definitionPath);
|
|
353
|
+
if (ids.has(command.id))
|
|
354
|
+
fail("invalid-definition", `group.commands contains duplicate ID ${command.id}`, definitionPath);
|
|
355
|
+
ids.add(command.id);
|
|
356
|
+
const argv = requireStringArray(command.argv, `${label}.argv`, definitionPath);
|
|
357
|
+
if (argv.length === 0) fail("invalid-definition", `${label}.argv must not be empty`, definitionPath);
|
|
358
|
+
if (
|
|
359
|
+
Object.hasOwn(command, "timeout_ms") &&
|
|
360
|
+
(!Number.isInteger(command.timeout_ms) || command.timeout_ms < 1 || command.timeout_ms > MAX_COMMAND_TIMEOUT_MS)
|
|
361
|
+
) {
|
|
362
|
+
fail(
|
|
363
|
+
"invalid-definition",
|
|
364
|
+
`${label}.timeout_ms must be an integer from 1 to ${MAX_COMMAND_TIMEOUT_MS}`,
|
|
365
|
+
definitionPath,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function validateModel(value, label, definitionPath) {
|
|
372
|
+
const model = requireExactKeys(value, ["model"], ["thinking"], label, definitionPath);
|
|
373
|
+
requireString(model.model, `${label}.model`, definitionPath);
|
|
374
|
+
if (Object.hasOwn(model, "thinking")) requireString(model.thinking, `${label}.thinking`, definitionPath);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Schema 2: a group is a task plus a configuration. Each of two or more named arms overlays the shared
|
|
378
|
+
// configuration, and `component` names the only paths where an arm's effective configuration may differ from the
|
|
379
|
+
// reference arm's.
|
|
380
|
+
const SCHEMA_1_MESSAGE =
|
|
381
|
+
"schema 1 definitions are no longer supported; rewrite it as schema 2 (see Evaluate Behavior's references/authoring.md)";
|
|
382
|
+
// `participants` may be shared or differ between arms (one review against a pipeline of reviews); see participants.mjs.
|
|
383
|
+
const CONFIGURATION_FIELDS = [
|
|
384
|
+
"host",
|
|
385
|
+
"model",
|
|
386
|
+
"subject",
|
|
387
|
+
"freeflow",
|
|
388
|
+
"runtime",
|
|
389
|
+
"shell",
|
|
390
|
+
"environment",
|
|
391
|
+
"network",
|
|
392
|
+
"participants",
|
|
393
|
+
];
|
|
394
|
+
// Arms may not differ here: runtime bundles are prepared once per group, and a group runs on one host. Host extensions
|
|
395
|
+
// and settings may differ, like the rest of the configuration, and so may runtime environment literals, which each
|
|
396
|
+
// arm's subject receives as its own.
|
|
397
|
+
const SHARED_ONLY = ["host.name", "host.version", "runtime"];
|
|
398
|
+
const ARM_RUNTIME = ["runtime.environment.literal"];
|
|
399
|
+
const COMPONENT_PATH = /^[a-z][A-Za-z0-9]*(?:\.[A-Za-z0-9_-]+)*$/;
|
|
400
|
+
|
|
401
|
+
function rejectSchema1(value, label, definitionPath) {
|
|
402
|
+
if (isPlainObject(value) && value.schema_version === 1)
|
|
403
|
+
fail("unsupported-schema", `${label}: ${SCHEMA_1_MESSAGE}`, definitionPath);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Objects merge key by key; arrays, scalars and null replace. */
|
|
407
|
+
function merge(base, overlay) {
|
|
408
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) return structuredClone(overlay);
|
|
409
|
+
const result = structuredClone(base);
|
|
410
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
411
|
+
result[key] = Object.hasOwn(result, key) ? merge(result[key], value) : structuredClone(value);
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Leaf paths where two values differ; arrays and scalars are leaves. */
|
|
417
|
+
function differences(left, right, prefix = "") {
|
|
418
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
419
|
+
return [...new Set([...Object.keys(left), ...Object.keys(right)])]
|
|
420
|
+
.sort()
|
|
421
|
+
.flatMap((key) => differences(left[key], right[key], prefix === "" ? key : `${prefix}.${key}`));
|
|
422
|
+
}
|
|
423
|
+
return JSON.stringify(left) === JSON.stringify(right) ? [] : [prefix];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateSubject(value, label, definitionPath) {
|
|
427
|
+
const subject = requireObject(value, label, definitionPath);
|
|
428
|
+
if (subject.kind === "none") {
|
|
429
|
+
requireExactKeys(subject, ["kind"], [], label, definitionPath);
|
|
430
|
+
return { source: { kind: "working-tree" }, skills: [], target: null, context: [] };
|
|
431
|
+
}
|
|
432
|
+
if (subject.kind !== "skill") fail("invalid-definition", `${label}.kind must be skill or none`, definitionPath);
|
|
433
|
+
const { kind: _kind, ...environment } = requireExactKeys(
|
|
434
|
+
subject,
|
|
435
|
+
["kind", "source", "skills", "target", "context"],
|
|
436
|
+
[],
|
|
437
|
+
label,
|
|
438
|
+
definitionPath,
|
|
439
|
+
);
|
|
440
|
+
validateEnvironment(environment, label, definitionPath);
|
|
441
|
+
return environment;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function validateEffectiveConfiguration(value, label, definitionPath) {
|
|
445
|
+
const configuration = requireExactKeys(
|
|
446
|
+
value,
|
|
447
|
+
["host", "subject"],
|
|
448
|
+
["model", "freeflow", "runtime", "shell", "environment", "network", "participants"],
|
|
449
|
+
label,
|
|
450
|
+
definitionPath,
|
|
451
|
+
);
|
|
452
|
+
const host = requireExactKeys(
|
|
453
|
+
configuration.host,
|
|
454
|
+
["name"],
|
|
455
|
+
["version", "extensions", "settings"],
|
|
456
|
+
`${label}.host`,
|
|
457
|
+
definitionPath,
|
|
458
|
+
);
|
|
459
|
+
if (!EVALUATION_HOSTS.has(host.name))
|
|
460
|
+
fail("invalid-definition", `group.configuration.host.name must be ${HOST_NAMES}`, definitionPath);
|
|
461
|
+
if (Object.hasOwn(host, "version")) requireString(host.version, `${label}.host.version`, definitionPath);
|
|
462
|
+
if (Object.hasOwn(host, "extensions"))
|
|
463
|
+
validateHostExtensions(host.extensions, `${label}.host.extensions`, definitionPath);
|
|
464
|
+
if (Object.hasOwn(host, "settings")) {
|
|
465
|
+
const settings = requireObject(host.settings, `${label}.host.settings`, definitionPath);
|
|
466
|
+
// Extensions load only from host.extensions, so the comparison declares every one of them.
|
|
467
|
+
if (Object.hasOwn(settings, "packages")) {
|
|
468
|
+
fail(
|
|
469
|
+
"invalid-definition",
|
|
470
|
+
`${label}.host.settings cannot set packages; declare extensions in host.extensions`,
|
|
471
|
+
definitionPath,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (Object.hasOwn(configuration, "model")) validateModel(configuration.model, `${label}.model`, definitionPath);
|
|
476
|
+
const environment = validateSubject(configuration.subject, `${label}.subject`, definitionPath);
|
|
477
|
+
let runtime;
|
|
478
|
+
if (Object.hasOwn(configuration, "runtime") && configuration.runtime !== null) {
|
|
479
|
+
const declared = requireObject(configuration.runtime, `${label}.runtime`, definitionPath);
|
|
480
|
+
// The host is the configuration's alone; a runtime host would let one group run on two.
|
|
481
|
+
if (Object.hasOwn(declared, "host")) {
|
|
482
|
+
fail(
|
|
483
|
+
"invalid-definition",
|
|
484
|
+
`${label}.runtime cannot set host; the host is configuration.host.name`,
|
|
485
|
+
definitionPath,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
// Undeclared runtime fields take their defaults, so a definition states only what it changes.
|
|
489
|
+
runtime = {
|
|
490
|
+
host: host.name,
|
|
491
|
+
session: false,
|
|
492
|
+
extensions: [],
|
|
493
|
+
environment: { literal: {}, inherit: [] },
|
|
494
|
+
...declared,
|
|
495
|
+
};
|
|
496
|
+
validateRuntime(runtime, definitionPath);
|
|
497
|
+
}
|
|
498
|
+
let freeflow;
|
|
499
|
+
if (Object.hasOwn(configuration, "freeflow") && configuration.freeflow !== null) {
|
|
500
|
+
freeflow = validateFreeflow(configuration.freeflow, `${label}.freeflow`, definitionPath);
|
|
501
|
+
// The isolated prompt replaces the host's system prompt, which would remove Freeflow's own instructions.
|
|
502
|
+
if (runtime?.prompt !== "host") {
|
|
503
|
+
fail(
|
|
504
|
+
"invalid-definition",
|
|
505
|
+
`${label}.freeflow needs configuration.runtime.prompt "host", so Freeflow's own instructions reach the subject`,
|
|
506
|
+
definitionPath,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (Object.hasOwn(configuration, "shell")) {
|
|
511
|
+
validateShell(configuration.shell, `${label}.shell`, (code, message) => fail(code, message, definitionPath));
|
|
512
|
+
}
|
|
513
|
+
if (Object.hasOwn(configuration, "environment")) {
|
|
514
|
+
validateContainerEnvironment(configuration.environment, `${label}.environment`, (code, message) =>
|
|
515
|
+
fail(code, message, definitionPath),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (Object.hasOwn(configuration, "network")) {
|
|
519
|
+
validateNetwork(configuration.network, `${label}.network`, (code, message) => fail(code, message, definitionPath));
|
|
520
|
+
}
|
|
521
|
+
return { configuration, environment, runtime, freeflow };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Pi packages the host loads, each by its exact name and version, resolved from the user's installed packages. */
|
|
525
|
+
function validateHostExtensions(value, label, definitionPath) {
|
|
526
|
+
if (!Array.isArray(value)) fail("invalid-definition", `${label} must be an array`, definitionPath);
|
|
527
|
+
const names = new Set();
|
|
528
|
+
for (const [index, entry] of value.entries()) {
|
|
529
|
+
if (isSourceExtension(entry)) {
|
|
530
|
+
validateSourceExtension(entry, `${label}[${index}]`, (code, message) => fail(code, message, definitionPath));
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const extension = requireExactKeys(entry, ["package", "version"], [], `${label}[${index}]`, definitionPath);
|
|
534
|
+
requireString(extension.package, `${label}[${index}].package`, definitionPath);
|
|
535
|
+
requireString(extension.version, `${label}[${index}].version`, definitionPath);
|
|
536
|
+
if (names.has(extension.package))
|
|
537
|
+
fail("invalid-definition", `${label} names ${extension.package} twice`, definitionPath);
|
|
538
|
+
names.add(extension.package);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Freeflow as part of the configuration: the package built from an exact commit, and the settings written to
|
|
544
|
+
* the workspace's `.freeflow/config.json` (repository) and `.freeflow/local.json` (personal). The settings are
|
|
545
|
+
* checked later with the validator from that same build, so the check matches the version under test.
|
|
546
|
+
*/
|
|
547
|
+
function validateFreeflow(value, label, definitionPath) {
|
|
548
|
+
const freeflow = requireExactKeys(value, ["source"], ["repository", "personal"], label, definitionPath);
|
|
549
|
+
const source = requireExactKeys(freeflow.source, ["kind", "ref"], [], `${label}.source`, definitionPath);
|
|
550
|
+
if (source.kind !== "git") fail("invalid-definition", `${label}.source.kind must be git`, definitionPath);
|
|
551
|
+
requireString(source.ref, `${label}.source.ref`, definitionPath);
|
|
552
|
+
return {
|
|
553
|
+
source: { kind: "git", ref: source.ref },
|
|
554
|
+
repository: requireObject(freeflow.repository ?? {}, `${label}.repository`, definitionPath),
|
|
555
|
+
personal: requireObject(freeflow.personal ?? {}, `${label}.personal`, definitionPath),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function readConfigurationFile(reference, definitionPath, root) {
|
|
560
|
+
requireSuiteReference(reference, "group.configuration.extends", definitionPath);
|
|
561
|
+
const loaded = await readJsonFile(path.resolve(path.dirname(definitionPath), reference), root);
|
|
562
|
+
rejectSchema1(loaded.value, "configuration", loaded.file);
|
|
563
|
+
const shared = requireExactKeys(
|
|
564
|
+
loaded.value,
|
|
565
|
+
["schema_version", "kind", "id", "configuration"],
|
|
566
|
+
[],
|
|
567
|
+
"configuration file",
|
|
568
|
+
loaded.file,
|
|
569
|
+
);
|
|
570
|
+
if (shared.schema_version !== 2) fail("unsupported-schema", "configuration.schema_version must be 2", loaded.file);
|
|
571
|
+
if (shared.kind !== "configuration")
|
|
572
|
+
fail("invalid-definition", "configuration file kind must be configuration", loaded.file);
|
|
573
|
+
requireId(shared.id, "configuration.id", loaded.file);
|
|
574
|
+
const configuration = requireObject(shared.configuration, "configuration.configuration", loaded.file);
|
|
575
|
+
for (const key of Object.keys(configuration)) {
|
|
576
|
+
if (!CONFIGURATION_FIELDS.includes(key)) {
|
|
577
|
+
fail("invalid-definition", `configuration.configuration contains unsupported field ${key}`, loaded.file);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return configuration;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function resolveArms(group, definitionPath, root) {
|
|
584
|
+
const own = requireObject(group.configuration, "group.configuration", definitionPath);
|
|
585
|
+
for (const key of Object.keys(own)) {
|
|
586
|
+
if (key !== "extends" && !CONFIGURATION_FIELDS.includes(key)) {
|
|
587
|
+
fail("invalid-definition", `group.configuration contains unsupported field ${key}`, definitionPath);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const { extends: reference = null, ...fields } = own;
|
|
591
|
+
const base = reference === null ? {} : await readConfigurationFile(reference, definitionPath, root);
|
|
592
|
+
const shared = merge(base, fields);
|
|
593
|
+
|
|
594
|
+
const { names, reference: referenceName } = validateArmNames(group, definitionPath);
|
|
595
|
+
const resolved = {};
|
|
596
|
+
for (const arm of names) {
|
|
597
|
+
const overlay = requireObject(group.arms[arm], `group.arms.${arm}`, definitionPath);
|
|
598
|
+
for (const key of Object.keys(overlay)) {
|
|
599
|
+
if (!CONFIGURATION_FIELDS.includes(key)) {
|
|
600
|
+
fail("invalid-definition", `group.arms.${arm} contains unsupported field ${key}`, definitionPath);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
resolved[arm] = validateEffectiveConfiguration(merge(shared, overlay), `group.arms.${arm}`, definitionPath);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// A one-arm group measures one setup: nothing differs, so `component` is absent or empty.
|
|
607
|
+
if (!Object.hasOwn(group, "component") && names.length > 1) {
|
|
608
|
+
fail("invalid-definition", "group is missing component", definitionPath);
|
|
609
|
+
}
|
|
610
|
+
const component = requireStringArray(group.component ?? [], "group.component", definitionPath, { unique: true });
|
|
611
|
+
if (names.length === 1 && component.length > 0) {
|
|
612
|
+
fail("invalid-definition", "group.component must be absent or empty for a one-arm group", definitionPath);
|
|
613
|
+
}
|
|
614
|
+
for (const [index, entry] of component.entries()) {
|
|
615
|
+
if (!COMPONENT_PATH.test(entry) || !CONFIGURATION_FIELDS.includes(entry.split(".")[0])) {
|
|
616
|
+
fail("invalid-definition", `group.component[${index}] must be a path inside the configuration`, definitionPath);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Every arm is compared with the reference, so arms that each match it on a path also match each other there.
|
|
620
|
+
const declares = (entry, difference) => difference === entry || difference.startsWith(`${entry}.`);
|
|
621
|
+
const where = (arm) => (names.length === 2 ? "the arms differ" : `arm ${arm} differs from ${referenceName}`);
|
|
622
|
+
const differing = new Set();
|
|
623
|
+
for (const arm of names.slice(1)) {
|
|
624
|
+
for (const difference of differences(resolved[referenceName].configuration, resolved[arm].configuration)) {
|
|
625
|
+
if (
|
|
626
|
+
SHARED_ONLY.some((entry) => declares(entry, difference)) &&
|
|
627
|
+
!ARM_RUNTIME.some((entry) => declares(entry, difference))
|
|
628
|
+
) {
|
|
629
|
+
const shared = names.length === 2 ? "both arms" : "every arm";
|
|
630
|
+
fail("unsupported-difference", `${where(arm)} in ${difference}, which ${shared} must share`, definitionPath);
|
|
631
|
+
}
|
|
632
|
+
if (!component.some((entry) => declares(entry, difference))) {
|
|
633
|
+
fail(
|
|
634
|
+
"undeclared-difference",
|
|
635
|
+
`${where(arm)} in ${difference}, which group.component does not declare`,
|
|
636
|
+
definitionPath,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
differing.add(difference);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const entry of component) {
|
|
643
|
+
if (![...differing].some((difference) => declares(entry, difference))) {
|
|
644
|
+
fail(
|
|
645
|
+
"undeclared-difference",
|
|
646
|
+
`group.component declares ${entry}, but ${names.length === 2 ? "the arms do not differ" : `no arm differs from ${referenceName}`} there`,
|
|
647
|
+
definitionPath,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const extendsFile = reference === null ? null : path.resolve(path.dirname(definitionPath), reference);
|
|
652
|
+
return { resolved, names, reference: referenceName, component, extendsFile };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** The declared arm names, reference first and then in declaration order, and the reference arm. */
|
|
656
|
+
function validateArmNames(group, definitionPath) {
|
|
657
|
+
const declared = Object.keys(requireObject(group.arms, "group.arms", definitionPath));
|
|
658
|
+
if (declared.length < 1) fail("invalid-definition", "group.arms must declare at least one arm", definitionPath);
|
|
659
|
+
for (const name of declared) {
|
|
660
|
+
if (!ARM_NAME.test(name)) {
|
|
661
|
+
fail(
|
|
662
|
+
"invalid-definition",
|
|
663
|
+
`group.arms.${name} must be named with a lowercase letter, then up to 31 lowercase letters, numbers or hyphens`,
|
|
664
|
+
definitionPath,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// One arm is its own reference: it is measured, not compared.
|
|
669
|
+
const fallback = declared.length === 1 ? declared[0] : DEFAULT_REFERENCE;
|
|
670
|
+
const reference = Object.hasOwn(group, "reference") ? group.reference : fallback;
|
|
671
|
+
if (typeof reference !== "string" || !declared.includes(reference)) {
|
|
672
|
+
fail(
|
|
673
|
+
"invalid-definition",
|
|
674
|
+
Object.hasOwn(group, "reference")
|
|
675
|
+
? `group.reference must name one of group.arms: ${declared.join(", ")}`
|
|
676
|
+
: `group.arms must include ${DEFAULT_REFERENCE}, or group.reference must name the arm the others are compared with`,
|
|
677
|
+
definitionPath,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return { names: [reference, ...declared.filter((name) => name !== reference)], reference };
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async function validateGroup(value, definitionPath, root) {
|
|
684
|
+
rejectSchema1(value, "group", definitionPath);
|
|
685
|
+
const group = requireExactKeys(
|
|
686
|
+
value,
|
|
687
|
+
[
|
|
688
|
+
"schema_version",
|
|
689
|
+
"kind",
|
|
690
|
+
"id",
|
|
691
|
+
"type",
|
|
692
|
+
"input",
|
|
693
|
+
"fixture",
|
|
694
|
+
"tools",
|
|
695
|
+
"configuration",
|
|
696
|
+
"arms",
|
|
697
|
+
"expectations",
|
|
698
|
+
"review_questions",
|
|
699
|
+
],
|
|
700
|
+
// `component` is required when there are two or more arms (checked with the arms).
|
|
701
|
+
[
|
|
702
|
+
"component",
|
|
703
|
+
"commands",
|
|
704
|
+
"revision",
|
|
705
|
+
"reference",
|
|
706
|
+
"rubric",
|
|
707
|
+
"judges",
|
|
708
|
+
"combine",
|
|
709
|
+
"reference_solution",
|
|
710
|
+
"participants",
|
|
711
|
+
],
|
|
712
|
+
"group",
|
|
713
|
+
definitionPath,
|
|
714
|
+
);
|
|
715
|
+
if (group.schema_version !== 2) fail("unsupported-schema", "group.schema_version must be 2", definitionPath);
|
|
716
|
+
if (group.kind !== "group") fail("invalid-definition", "group.kind must be group", definitionPath);
|
|
717
|
+
requireId(group.id, "group.id", definitionPath);
|
|
718
|
+
if (!EVALUATION_TYPES.has(group.type)) {
|
|
719
|
+
fail("invalid-definition", "group.type must be description or body", definitionPath);
|
|
720
|
+
}
|
|
721
|
+
validateInput(group.input, definitionPath);
|
|
722
|
+
if (group.fixture !== null) requireSafeRelativePath(group.fixture, "group.fixture", definitionPath);
|
|
723
|
+
requireStringArray(group.tools, "group.tools", definitionPath, { unique: true });
|
|
724
|
+
validateCommands(group, definitionPath);
|
|
725
|
+
const { resolved, names, reference, component, extendsFile } = await resolveArms(group, definitionPath, root);
|
|
726
|
+
// Arms are kept reference first, so every reader lists them in the same order.
|
|
727
|
+
const variants = Object.fromEntries(names.map((arm) => [arm, { ...resolved[arm].environment }]));
|
|
728
|
+
for (const arm of names.slice(1)) {
|
|
729
|
+
if (variants[arm].target === null && variants[arm].skills.length > 0) {
|
|
730
|
+
fail(
|
|
731
|
+
"invalid-definition",
|
|
732
|
+
`group.arms.${arm}.subject.target must identify the ${arm} target skill when ${arm} skills are declared`,
|
|
733
|
+
definitionPath,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
validateExpectations(group.expectations, definitionPath);
|
|
738
|
+
if (names.length === 1) {
|
|
739
|
+
const compared = group.expectations.findIndex((expectation) => Object.hasOwn(expectation, "comparison"));
|
|
740
|
+
if (compared >= 0) {
|
|
741
|
+
fail(
|
|
742
|
+
"invalid-definition",
|
|
743
|
+
`group.expectations[${compared}].comparison needs two arms; a one-arm group measures one setup`,
|
|
744
|
+
definitionPath,
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
validateShellGroup({
|
|
749
|
+
group,
|
|
750
|
+
names,
|
|
751
|
+
configurations: Object.fromEntries(names.map((arm) => [arm, resolved[arm].configuration])),
|
|
752
|
+
fail: (code, message) => fail(code, message, definitionPath),
|
|
753
|
+
});
|
|
754
|
+
const referenceProblem = validateReferenceSolution(group.reference_solution, {
|
|
755
|
+
root,
|
|
756
|
+
arms: names,
|
|
757
|
+
fixture: group.fixture === null ? null : path.resolve(root, group.fixture),
|
|
758
|
+
});
|
|
759
|
+
if (referenceProblem !== null) fail(referenceProblem.code, referenceProblem.message, definitionPath);
|
|
760
|
+
const graderFiles = validateCommandExpectations(group, {
|
|
761
|
+
arms: names,
|
|
762
|
+
root,
|
|
763
|
+
fail: (code, message) => fail(code, message, definitionPath),
|
|
764
|
+
});
|
|
765
|
+
requireStringArray(group.review_questions, "group.review_questions", definitionPath);
|
|
766
|
+
Object.assign(group, await validateJudging(group, definitionPath, root));
|
|
767
|
+
const participants = await validateParticipants(group, resolved, definitionPath, root);
|
|
768
|
+
Object.assign(group, participants.group);
|
|
769
|
+
for (const arm of names) if (participants.arms[arm]) variants[arm].participants = participants.arms[arm];
|
|
770
|
+
if (Object.hasOwn(group, "revision")) {
|
|
771
|
+
if (group.revision !== "description-only") {
|
|
772
|
+
fail("invalid-definition", "group.revision must be description-only when declared", definitionPath);
|
|
773
|
+
}
|
|
774
|
+
if (group.type !== "description" || names.some((arm) => variants[arm].target === null)) {
|
|
775
|
+
fail(
|
|
776
|
+
"invalid-definition",
|
|
777
|
+
"a description-only revision needs a description group with a target skill in both arms",
|
|
778
|
+
definitionPath,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
if (names.length !== 2) {
|
|
782
|
+
fail("invalid-definition", "a description-only revision compares exactly two arms", definitionPath);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (group.expectations.length === 0 && group.review_questions.length === 0 && group.rubric === undefined) {
|
|
786
|
+
fail(
|
|
787
|
+
"invalid-definition",
|
|
788
|
+
"group must declare at least one expectation or review question, or a rubric; otherwise a run would observe nothing",
|
|
789
|
+
definitionPath,
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
// What grading would refuse that the definition alone decides is refused here, before anything is spent.
|
|
793
|
+
const [problem] = await expectationProblems({ ...group, variants });
|
|
794
|
+
if (problem !== undefined) {
|
|
795
|
+
const where =
|
|
796
|
+
problem.comparison !== undefined
|
|
797
|
+
? `group.expectations comparison ${problem.comparison}`
|
|
798
|
+
: `group.expectations[${group.expectations.findIndex((entry) => entry.id === problem.expectation)}] (${problem.expectation})`;
|
|
799
|
+
fail("invalid-definition", `${where}: ${problem.reason}`, definitionPath);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// The runner works on the resolved arms: each variant carries its subject and model, and the shared
|
|
803
|
+
// runtime (which may not differ between arms) is the group's.
|
|
804
|
+
for (const arm of names) {
|
|
805
|
+
if (resolved[arm].configuration.model !== undefined) variants[arm].model = resolved[arm].configuration.model;
|
|
806
|
+
if (resolved[arm].freeflow !== undefined) variants[arm].freeflow = resolved[arm].freeflow;
|
|
807
|
+
variants[arm].hostExtensions = resolved[arm].configuration.host.extensions ?? [];
|
|
808
|
+
variants[arm].hostSettings = resolved[arm].configuration.host.settings ?? {};
|
|
809
|
+
const shell = resolved[arm].configuration.shell ?? containerShell(resolved[arm].configuration.environment);
|
|
810
|
+
if (shell !== undefined) variants[arm].shell = shell;
|
|
811
|
+
// A declared network "none" must hold on every path that runs subject-written code (seal.mjs).
|
|
812
|
+
const sealRefusal = sealProblem(arm, sealPaths(group, arm, shell), resolved[arm].configuration.network);
|
|
813
|
+
if (sealRefusal !== null) fail("invalid-definition", sealRefusal, definitionPath);
|
|
814
|
+
// The group runtime is the reference's; an arm whose environment literals differ carries its own.
|
|
815
|
+
const literal = resolved[arm].runtime?.environment.literal;
|
|
816
|
+
if (JSON.stringify(literal) !== JSON.stringify(resolved[reference].runtime?.environment.literal)) {
|
|
817
|
+
variants[arm].runtimeLiteral = literal;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const internal = {
|
|
821
|
+
...group,
|
|
822
|
+
configurations: Object.fromEntries(names.map((arm) => [arm, resolved[arm].configuration])),
|
|
823
|
+
// The files this group was read from, and those its command graders, rubric references, reference solution
|
|
824
|
+
// and path-sourced extensions read, so a run can check they are committed.
|
|
825
|
+
files: [
|
|
826
|
+
definitionPath,
|
|
827
|
+
...(extendsFile === null ? [] : [extendsFile]),
|
|
828
|
+
...graderFiles,
|
|
829
|
+
...referenceFiles(group, root),
|
|
830
|
+
...rubricFiles(group.rubric, root, group.judges),
|
|
831
|
+
...sourceExtensionInputs(
|
|
832
|
+
names.map((arm) => variants[arm].hostExtensions),
|
|
833
|
+
root,
|
|
834
|
+
),
|
|
835
|
+
...participantFiles(Object.values(participants.arms), root),
|
|
836
|
+
],
|
|
837
|
+
component,
|
|
838
|
+
variants,
|
|
839
|
+
};
|
|
840
|
+
const referenceModel = variants[reference].model;
|
|
841
|
+
if (referenceModel && names.every((arm) => JSON.stringify(variants[arm].model) === JSON.stringify(referenceModel))) {
|
|
842
|
+
internal.model = referenceModel;
|
|
843
|
+
}
|
|
844
|
+
if (resolved[reference].runtime !== undefined) internal.runtime = resolved[reference].runtime;
|
|
845
|
+
// The host may not differ between arms (SHARED_ONLY), so the reference's is the group's.
|
|
846
|
+
internal.host = resolved[reference].configuration.host.name;
|
|
847
|
+
if (internal.host === "claude-code") {
|
|
848
|
+
const problems = claudeCodeGroupProblems(group, internal.configurations);
|
|
849
|
+
if (problems.length > 0) fail("invalid-definition", problems[0], definitionPath);
|
|
850
|
+
}
|
|
851
|
+
validateResume(group, internal, definitionPath);
|
|
852
|
+
return internal;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function validateSuite(value, definitionPath) {
|
|
856
|
+
rejectSchema1(value, "suite", definitionPath);
|
|
857
|
+
const suite = requireExactKeys(
|
|
858
|
+
value,
|
|
859
|
+
["schema_version", "kind", "id", "groups"],
|
|
860
|
+
["decision"],
|
|
861
|
+
"suite",
|
|
862
|
+
definitionPath,
|
|
863
|
+
);
|
|
864
|
+
if (suite.schema_version !== 2) fail("unsupported-schema", "suite.schema_version must be 2", definitionPath);
|
|
865
|
+
if (suite.kind !== "suite") fail("invalid-definition", "suite.kind must be suite", definitionPath);
|
|
866
|
+
requireId(suite.id, "suite.id", definitionPath);
|
|
867
|
+
const groups = requireStringArray(suite.groups, "suite.groups", definitionPath, { unique: true });
|
|
868
|
+
for (const [index, reference] of groups.entries()) {
|
|
869
|
+
requireSuiteReference(reference, `suite.groups[${index}]`, definitionPath);
|
|
870
|
+
}
|
|
871
|
+
if (groups.length === 0) fail("invalid-definition", "suite.groups must not be empty", definitionPath);
|
|
872
|
+
if (Object.hasOwn(suite, "decision")) {
|
|
873
|
+
suite.decision = validateDecision(suite.decision, (message) => fail("invalid-definition", message, definitionPath));
|
|
874
|
+
}
|
|
875
|
+
return suite;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function isContained(root, target) {
|
|
879
|
+
const relative = path.relative(root, target);
|
|
880
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function readJsonFile(file, root) {
|
|
884
|
+
const lexicalFile = path.resolve(file);
|
|
885
|
+
if (!isContained(root, lexicalFile)) fail("unsafe-path", `Definition escapes root: ${file}`, lexicalFile);
|
|
886
|
+
|
|
887
|
+
let canonicalRoot;
|
|
888
|
+
let canonicalFile;
|
|
889
|
+
try {
|
|
890
|
+
[canonicalRoot, canonicalFile] = await Promise.all([realpath(root), realpath(lexicalFile)]);
|
|
891
|
+
} catch {
|
|
892
|
+
fail("unreadable-definition", `Cannot resolve definition: ${lexicalFile}`, lexicalFile);
|
|
893
|
+
}
|
|
894
|
+
if (!isContained(canonicalRoot, canonicalFile)) {
|
|
895
|
+
fail("unsafe-path", `Definition symlink escapes root: ${lexicalFile}`, lexicalFile);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
let contents;
|
|
899
|
+
try {
|
|
900
|
+
contents = await readFile(canonicalFile, "utf8");
|
|
901
|
+
} catch {
|
|
902
|
+
fail("unreadable-definition", `Cannot read definition: ${lexicalFile}`, lexicalFile);
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
return { value: JSON.parse(contents), file: lexicalFile };
|
|
906
|
+
} catch {
|
|
907
|
+
fail("invalid-json", `Definition is not valid JSON: ${lexicalFile}`, lexicalFile);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Loads a configuration file (`kind: "configuration"`) on its own, for a process that is not an arm, such as
|
|
913
|
+
* the advisory reviewer: its model, host extensions and host settings, validated as an arm's would be.
|
|
914
|
+
*
|
|
915
|
+
* @returns {Promise<{host: string | null, model: {model: string, thinking?: string} | null, extensions: Array<{package: string, version: string}>, settings: Record<string, unknown>}>}
|
|
916
|
+
*/
|
|
917
|
+
export async function loadConfiguration(file, { root = process.cwd() } = {}) {
|
|
918
|
+
const absolute = path.resolve(root, file);
|
|
919
|
+
const configuration = await readConfigurationFile(path.basename(absolute), absolute, path.resolve(root));
|
|
920
|
+
return hostSetup(configuration, "configuration", absolute);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* A rubric judge's own configuration: inline `host` and `model`, or `{ "extends": "<path>" }` naming a configuration
|
|
925
|
+
* file relative to the group file. Validated as a configuration; `file` is the extended file, a run input.
|
|
926
|
+
*
|
|
927
|
+
* @returns {Promise<{host: string | null, model: {model: string, thinking?: string} | null, extensions: Array<any>, settings: Record<string, unknown>, file: string | null}>}
|
|
928
|
+
*/
|
|
929
|
+
export async function loadJudgeConfiguration(value, label, definitionPath, root) {
|
|
930
|
+
const object = requireObject(value, label, definitionPath);
|
|
931
|
+
if (Object.hasOwn(object, "extends")) {
|
|
932
|
+
requireExactKeys(object, ["extends"], [], label, definitionPath);
|
|
933
|
+
const configuration = await readConfigurationFile(object.extends, definitionPath, root);
|
|
934
|
+
const file = path.resolve(path.dirname(definitionPath), object.extends);
|
|
935
|
+
return { ...hostSetup(configuration, label, file), file };
|
|
936
|
+
}
|
|
937
|
+
requireExactKeys(object, [], ["host", "model"], label, definitionPath);
|
|
938
|
+
return { ...hostSetup(object, label, definitionPath), file: null };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** The host setup a configuration gives a process that is not an arm: its model, host extensions and settings. */
|
|
942
|
+
function hostSetup(configuration, label, absolute) {
|
|
943
|
+
let host = null;
|
|
944
|
+
let extensions = [];
|
|
945
|
+
let settings = {};
|
|
946
|
+
if (Object.hasOwn(configuration, "host")) {
|
|
947
|
+
const declared = requireExactKeys(
|
|
948
|
+
configuration.host,
|
|
949
|
+
["name"],
|
|
950
|
+
["version", "extensions", "settings"],
|
|
951
|
+
`${label}.host`,
|
|
952
|
+
absolute,
|
|
953
|
+
);
|
|
954
|
+
if (!EVALUATION_HOSTS.has(declared.name))
|
|
955
|
+
fail("invalid-definition", `${label}.host.name must be ${HOST_NAMES}`, absolute);
|
|
956
|
+
host = declared.name;
|
|
957
|
+
if (Object.hasOwn(declared, "extensions")) {
|
|
958
|
+
validateHostExtensions(declared.extensions, `${label}.host.extensions`, absolute);
|
|
959
|
+
extensions = declared.extensions;
|
|
960
|
+
}
|
|
961
|
+
if (Object.hasOwn(declared, "settings")) {
|
|
962
|
+
settings = requireObject(declared.settings, `${label}.host.settings`, absolute);
|
|
963
|
+
if (Object.hasOwn(settings, "packages")) {
|
|
964
|
+
fail(
|
|
965
|
+
"invalid-definition",
|
|
966
|
+
`${label}.host.settings cannot set packages; declare extensions in host.extensions`,
|
|
967
|
+
absolute,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
let model = null;
|
|
973
|
+
if (Object.hasOwn(configuration, "model")) {
|
|
974
|
+
validateModel(configuration.model, `${label}.model`, absolute);
|
|
975
|
+
model = configuration.model;
|
|
976
|
+
}
|
|
977
|
+
if (host === "claude-code") {
|
|
978
|
+
const problems = claudeCodeSetupProblems({ label, model, extensions, settings });
|
|
979
|
+
if (problems.length > 0) fail("invalid-definition", problems[0], absolute);
|
|
980
|
+
}
|
|
981
|
+
// `host` is null when the configuration names none; the process then runs on the host it inherits.
|
|
982
|
+
return { host, model, extensions, settings };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export async function loadDefinition(file, { root = process.cwd() } = {}) {
|
|
986
|
+
const definitionRoot = path.resolve(root);
|
|
987
|
+
const loaded = await readJsonFile(file, definitionRoot);
|
|
988
|
+
const object = requireObject(loaded.value, "definition", loaded.file);
|
|
989
|
+
if (object.kind === "group") return validateGroup(object, loaded.file, definitionRoot);
|
|
990
|
+
if (object.kind !== "suite") fail("invalid-definition", "definition.kind must be group or suite", loaded.file);
|
|
991
|
+
|
|
992
|
+
const suite = validateSuite(object, loaded.file);
|
|
993
|
+
const groups = [];
|
|
994
|
+
const ids = new Set();
|
|
995
|
+
for (const reference of suite.groups) {
|
|
996
|
+
const groupFile = path.resolve(path.dirname(loaded.file), reference);
|
|
997
|
+
const group = await loadSuiteGroup(groupFile, definitionRoot);
|
|
998
|
+
const where = `${group.id} (${path.relative(definitionRoot, groupFile)})`;
|
|
999
|
+
if (ids.has(group.id)) fail("invalid-definition", `suite contains duplicate group ID ${where}`, loaded.file);
|
|
1000
|
+
ids.add(group.id);
|
|
1001
|
+
// A decision compares two arms, so a one-arm group in the suite has nothing for it to read.
|
|
1002
|
+
if (suite.decision !== undefined && Object.keys(group.variants).length === 1) {
|
|
1003
|
+
fail("invalid-definition", `suite.decision compares arms, but group ${where} has one arm`, loaded.file);
|
|
1004
|
+
}
|
|
1005
|
+
groups.push(group);
|
|
1006
|
+
}
|
|
1007
|
+
const problem = suite.decision === undefined ? null : decisionProblem(suite.decision, groups);
|
|
1008
|
+
if (problem !== null) {
|
|
1009
|
+
const file = problem.group === null ? "" : ` (${path.relative(definitionRoot, problem.group.files[0])})`;
|
|
1010
|
+
fail("invalid-definition", `suite.decision: ${problem.message}${file}`, loaded.file);
|
|
1011
|
+
}
|
|
1012
|
+
return { ...suite, groups };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* One of a suite's groups. A refusal names the group and its file, so a suite with many groups shows which one
|
|
1017
|
+
* failed: `group <id> (<file>): <reason>`, or the file alone when the group's ID was not read.
|
|
1018
|
+
*/
|
|
1019
|
+
async function loadSuiteGroup(groupFile, root) {
|
|
1020
|
+
let id = null;
|
|
1021
|
+
try {
|
|
1022
|
+
const loaded = await readJsonFile(groupFile, root);
|
|
1023
|
+
if (isPlainObject(loaded.value) && typeof loaded.value.id === "string") id = loaded.value.id;
|
|
1024
|
+
return await validateGroup(loaded.value, loaded.file, root);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
if (!(error instanceof DefinitionError)) throw error;
|
|
1027
|
+
const where = `${path.relative(root, groupFile)}`;
|
|
1028
|
+
const label = id === null ? `group ${where}` : `group ${id} (${where})`;
|
|
1029
|
+
throw new DefinitionError(error.code, `${label}: ${error.message}`, error.definitionPath);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function variantSelection(group, selected) {
|
|
1034
|
+
return Object.fromEntries(
|
|
1035
|
+
armNames(group).map((arm) => [arm, selected === null || selected === arm ? "selected" : "not-selected"]),
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export function selectDefinition(definition, { group = null, variant = null } = {}) {
|
|
1040
|
+
if (variant !== null && (typeof variant !== "string" || !ARM_NAME.test(variant))) {
|
|
1041
|
+
fail("invalid-selector", "variant must be an arm name, such as baseline or candidate", null);
|
|
1042
|
+
}
|
|
1043
|
+
if (definition.kind === "group" && group !== null) {
|
|
1044
|
+
fail("invalid-selector", "--group cannot be used with a direct group definition", null);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const available = definition.kind === "group" ? [definition] : definition.groups;
|
|
1048
|
+
let selected = available;
|
|
1049
|
+
if (group !== null) {
|
|
1050
|
+
const position = typeof group === "number" || /^\d+$/.test(group) ? Number(group) : null;
|
|
1051
|
+
selected =
|
|
1052
|
+
position === null ? available.filter((entry) => entry.id === group) : [available[position - 1]].filter(Boolean);
|
|
1053
|
+
if (selected.length === 0) fail("unknown-group", `Unknown group selector: ${group}`, null);
|
|
1054
|
+
}
|
|
1055
|
+
// A selected arm applies across the selected groups, so each of them must have it.
|
|
1056
|
+
for (const entry of selected) {
|
|
1057
|
+
if (variant !== null && !armNames(entry).includes(variant)) {
|
|
1058
|
+
fail("invalid-selector", `variant ${variant} is not an arm of group ${entry.id}`, null);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "selection",
|
|
1064
|
+
definitionKind: definition.kind,
|
|
1065
|
+
id: definition.id,
|
|
1066
|
+
groups: selected.map((entry) => ({
|
|
1067
|
+
position: available.indexOf(entry) + 1,
|
|
1068
|
+
group: entry,
|
|
1069
|
+
variants: variantSelection(entry, variant),
|
|
1070
|
+
})),
|
|
1071
|
+
};
|
|
1072
|
+
}
|