@hassangameryt/freeflow 0.7.3 → 0.8.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/NOTICE +14 -0
- package/README.md +26 -31
- package/capabilities/cognitive-routing/SKILL.md +106 -161
- package/capabilities/cognitive-routing/references/both-mode.md +36 -0
- package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
- package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
- package/capabilities/compaction/SKILL.md +49 -0
- package/capabilities/compaction/references/summary-format.md +239 -0
- package/command-surface.json +4 -0
- package/gemini-extension.json +1 -1
- package/hooks/shared/runtime-context.mjs +28 -30
- package/licenses/Apache-2.0.txt +55 -0
- package/package.json +22 -9
- package/pi-extension/README.md +41 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
- package/pi-extension/dist/cognitive-routing/economics.js +45 -0
- package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
- package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
- package/pi-extension/dist/cognitive-routing/gate.js +241 -0
- package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
- package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
- package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
- package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
- package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
- package/pi-extension/dist/cognitive-routing/session.js +221 -0
- package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
- package/pi-extension/dist/cognitive-routing/status.js +80 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
- package/pi-extension/dist/compaction/carry.js +191 -0
- package/pi-extension/dist/compaction/config.js +22 -0
- package/pi-extension/dist/compaction/controller.js +471 -0
- package/pi-extension/dist/compaction/harness.js +77 -0
- package/pi-extension/dist/compaction/results.js +158 -0
- package/pi-extension/dist/compaction/thresholds.js +35 -0
- package/pi-extension/dist/compaction/tool.js +57 -0
- package/pi-extension/dist/host/branch.js +13 -0
- package/pi-extension/dist/host/canonical.js +12 -0
- package/pi-extension/dist/host/catalog.js +77 -0
- package/pi-extension/dist/host/config.js +374 -0
- package/pi-extension/dist/host/projection-tags.js +60 -0
- package/pi-extension/dist/host/prompts.js +82 -0
- package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
- package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
- package/pi-extension/dist/host/runtime-state.js +189 -0
- package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
- package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
- package/pi-extension/dist/host/staging.js +28 -0
- package/pi-extension/dist/host/status.js +101 -0
- package/pi-extension/dist/index.js +423 -173
- package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
- package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
- package/pi-extension/dist/provider-support/cache/health.js +73 -0
- package/pi-extension/dist/provider-support/cache/index.js +58 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
- package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
- package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
- package/pi-extension/dist/provider-support/effort.js +13 -0
- package/pi-extension/dist/provider-support/index.js +44 -3
- package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
- package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
- package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
- package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
- package/pi-extension/dist/provider-support/routes.js +17 -0
- package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
- package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
- package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
- package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
- package/pi-extension/dist/tool-execution/background.js +238 -0
- package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
- package/pi-extension/dist/tool-execution/config.js +22 -0
- package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
- package/pi-extension/dist/tool-execution/file-state.js +138 -0
- package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
- package/pi-extension/dist/tool-execution/prompt.js +54 -0
- package/pi-extension/dist/tool-execution/tools.js +16 -0
- package/pi-extension/freeflow/README.md +9 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +22 -75
- package/runtime/prompts/interaction-contract.md +15 -8
- package/runtime/prompts/tool-execution.md +38 -0
- package/runtime/prompts/working-method.md +108 -0
- package/skills/action-selection/SKILL.md +39 -47
- package/skills/action-selection/references/trajectory-stalls.md +2 -0
- package/skills/bypass/SKILL.md +2 -2
- package/skills/commit-work/SKILL.md +31 -83
- package/skills/commit-work/references/git-state-edges.md +1 -1
- package/skills/commit-work/references/simple-push.md +2 -0
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +31 -50
- package/skills/design-for-depth/SKILL.md +44 -53
- package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
- package/skills/design-for-depth/references/implementation-decisions.md +1 -1
- package/skills/design-for-depth/references/interface-design-loop.md +5 -36
- package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
- package/skills/diagnose-failure/SKILL.md +39 -24
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
- package/skills/discuss/SKILL.md +35 -11
- package/skills/discuss/references/discussion-continuity.md +7 -27
- package/skills/evaluate-skill/SKILL.md +49 -80
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
- package/skills/evaluate-skill/references/evaluation-design.md +14 -14
- package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
- package/skills/evaluate-skill/references/review-and-revision.md +3 -1
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
- package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
- package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
- package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
- package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
- package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
- package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
- package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
- package/skills/execute-work/SKILL.md +41 -9
- package/skills/execute-work/references/code-practices.md +2 -0
- package/skills/execute-work/references/execute-work-edges.md +11 -1
- package/skills/handoff/SKILL.md +1 -1
- package/skills/handoff/references/handoff-templates.md +1 -1
- package/skills/review-artifact/SKILL.md +42 -41
- package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +38 -32
- package/skills/review-work/references/adjudicate-work-review.md +1 -1
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/review-work/references/security-risk-lens.md +2 -4
- package/skills/setup-freeflow/SKILL.md +3 -1
- package/skills/setup-freeflow/references/activation-contract.md +7 -13
- package/skills/setup-freeflow/references/host-setup.md +3 -3
- package/skills/track-work/SKILL.md +113 -161
- package/skills/track-work/references/working-record-format.md +189 -350
- package/skills/track-work/scripts/lib/document.mjs +62 -6
- package/skills/track-work/scripts/lib/format.mjs +16 -1
- package/skills/track-work/scripts/lib/store.mjs +9 -8
- package/skills/track-work/scripts/lib/transitions.mjs +79 -4
- package/skills/track-work/scripts/working-record.mjs +34 -12
- package/skills/verify-work/SKILL.md +29 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
- package/skills/verify-work/references/integration-evidence.md +2 -0
- package/skills/verify-work/references/performance-evidence.md +3 -1
- package/skills/verify-work/references/test-design.md +3 -1
- package/skills/workflow/SKILL.md +65 -21
- package/skills/workflow/references/domain-skill-composition.md +5 -3
- package/skills/write-docs/SKILL.md +83 -0
- package/skills/write-docs/references/documentation-shapes.md +119 -0
- package/skills/write-docs/scripts/docs-check.mjs +139 -0
- package/skills/write-plan/SKILL.md +40 -40
- package/skills/write-plan/references/plan-shapes.md +1 -1
- package/skills/write-skill/SKILL.md +107 -147
- package/skills/write-skill/references/activation-boundaries.md +2 -0
- package/skills/write-skill/references/agent-first-instructions.md +22 -10
- package/skills/write-skill/references/progressive-disclosure.md +4 -2
- package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
- package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
- package/skills/write-skill/scripts/skill-author.mjs +18 -2
- package/skills/write-spec/SKILL.md +34 -41
- package/skills/write-spec/references/artifact-standards.md +1 -1
- package/skills/write-spec/references/decision-records.md +2 -0
- package/skills/write-spec/references/spec-shapes.md +1 -1
- package/capabilities/context-virtualization/SKILL.md +0 -103
- package/capabilities/conversation-history/SKILL.md +0 -53
- package/hooks/tests/check-host-adapters.test.mjs +0 -82
- package/hooks/tests/check-runtime-context-hook.sh +0 -212
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
- package/pi-extension/dist/context-virtualization/commands.js +0 -89
- package/pi-extension/dist/context-virtualization/projector.js +0 -42
- package/pi-extension/dist/context-virtualization/runtime.js +0 -378
- package/pi-extension/dist/context-virtualization/state.js +0 -57
- package/pi-extension/dist/context-virtualization/types.js +0 -10
- package/pi-extension/dist/conversation-history/passages.js +0 -66
- package/pi-extension/dist/conversation-history/ranking.js +0 -105
- package/pi-extension/dist/conversation-history/runtime.js +0 -226
- package/pi-extension/dist/conversation-history/search.js +0 -56
- package/pi-extension/dist/conversation-history/types.js +0 -1
- package/pi-extension/dist/freeflow-context/resolver.js +0 -120
- package/pi-extension/dist/freeflow-context/runtime.js +0 -80
- package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
- package/pi-extension/dist/freeflow-context/tool.js +0 -314
- package/pi-extension/dist/freeflow-context/types.js +0 -12
- package/pi-extension/dist/freeflow-context/visibility.js +0 -28
- package/pi-extension/dist/integrations/mcp-config.js +0 -29
- package/pi-extension/dist/runtime/runtime-context.js +0 -754
- package/pi-extension/dist/runtime/runtime-identity.js +0 -5
- package/pi-extension/dist/session-sources/provenance.js +0 -32
- package/pi-extension/dist/ui/text-component.js +0 -57
- package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
- package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
- package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
- package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
- package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
- package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
- package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
- package/pi-extension/src/context-virtualization/commands.ts +0 -111
- package/pi-extension/src/context-virtualization/projector.ts +0 -62
- package/pi-extension/src/context-virtualization/runtime.ts +0 -428
- package/pi-extension/src/context-virtualization/state.ts +0 -79
- package/pi-extension/src/context-virtualization/types.ts +0 -92
- package/pi-extension/src/conversation-history/passages.ts +0 -81
- package/pi-extension/src/conversation-history/ranking.ts +0 -155
- package/pi-extension/src/conversation-history/runtime.ts +0 -250
- package/pi-extension/src/conversation-history/search.ts +0 -79
- package/pi-extension/src/conversation-history/types.ts +0 -31
- package/pi-extension/src/freeflow-context/resolver.ts +0 -139
- package/pi-extension/src/freeflow-context/runtime.ts +0 -100
- package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
- package/pi-extension/src/freeflow-context/tool.ts +0 -366
- package/pi-extension/src/freeflow-context/types.ts +0 -70
- package/pi-extension/src/freeflow-context/visibility.ts +0 -53
- package/pi-extension/src/index.ts +0 -390
- package/pi-extension/src/integrations/mcp-config.ts +0 -32
- package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
- package/pi-extension/src/provider-support/astra/history.ts +0 -145
- package/pi-extension/src/provider-support/astra/index.ts +0 -12
- package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
- package/pi-extension/src/provider-support/index.ts +0 -5
- package/pi-extension/src/runtime/request-history.ts +0 -243
- package/pi-extension/src/runtime/runtime-context.ts +0 -860
- package/pi-extension/src/runtime/runtime-identity.ts +0 -8
- package/pi-extension/src/session-sources/events.ts +0 -230
- package/pi-extension/src/session-sources/provenance.ts +0 -39
- package/pi-extension/src/session-sources/read-only-session.ts +0 -170
- package/pi-extension/src/session-sources/sources.ts +0 -334
- package/pi-extension/src/settings/settings-tui.ts +0 -501
- package/pi-extension/src/settings/settings-ui.ts +0 -1652
- package/pi-extension/src/ui/text-component.ts +0 -66
- package/pi-extension/tests/cache-reuse/history.test.js +0 -117
- package/pi-extension/tests/cache-reuse/native.test.js +0 -91
- package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
- package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
- package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
- package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
- package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
- package/pi-extension/tests/conversation-history/search.test.js +0 -97
- package/pi-extension/tests/fixtures/pi-host.js +0 -4
- package/pi-extension/tests/fixtures/routing-native.js +0 -191
- package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
- package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
- package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
- package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
- package/pi-extension/tests/provider-support/astra.test.js +0 -306
- package/pi-extension/tests/provider-support/cli.test.js +0 -92
- package/pi-extension/tests/provider-support/native.test.js +0 -183
- package/pi-extension/tests/settings/settings-tui.test.js +0 -243
- package/pi-extension/tsconfig.json +0 -16
- package/runtime/prompts/context-virtualization.md +0 -3
- package/runtime/prompts/conversation-history.md +0 -3
- package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { sha256, writeJson, writeText } from "./evidence.mjs";
|
|
6
|
+
import { baseProcessEnvironment, runPiProcess } from "./pi.mjs";
|
|
7
|
+
import { resolveResult } from "./view.mjs";
|
|
8
|
+
|
|
9
|
+
const ANSWERS = new Set(["yes", "no", "unclear"]);
|
|
10
|
+
const RESPONSE_LIMIT = 8000;
|
|
11
|
+
const ARGUMENT_LIMIT = 300;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Answers each group's review questions with a separate model that sees two unlabeled runs.
|
|
15
|
+
* The result is advisory: it is written beside, never into, canonical run and grade evidence.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} target
|
|
18
|
+
* @param {{group: string | null}} selectors
|
|
19
|
+
* @param {{root?: string, signal?: AbortSignal, model: string, thinking?: string | null}} options
|
|
20
|
+
*/
|
|
21
|
+
export async function reviewResult(target, selectors, { root = process.cwd(), signal, model, thinking = null }) {
|
|
22
|
+
const resultDirectory = await resolveResult(target, root);
|
|
23
|
+
const summary = JSON.parse(await readFile(path.join(resultDirectory, "summary.json"), "utf8"));
|
|
24
|
+
const groups = selectGroups(summary, selectors.group);
|
|
25
|
+
const outcomes = [];
|
|
26
|
+
for (const group of groups) {
|
|
27
|
+
if (signal?.aborted) break;
|
|
28
|
+
outcomes.push(
|
|
29
|
+
await reviewGroup(path.join(resultDirectory, "groups", group.id), group.id, { model, thinking, signal }),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return { resultDirectory, groups: outcomes };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function selectGroups(summary, selector) {
|
|
36
|
+
const groups = Array.isArray(summary.groups) ? summary.groups : [];
|
|
37
|
+
if (selector === null) return groups;
|
|
38
|
+
if (summary.definitionKind === "group") throw new Error("--group cannot be used with a direct group result");
|
|
39
|
+
const position = /^\d+$/.test(selector) ? Number(selector) : null;
|
|
40
|
+
const selected = groups.filter((group) => (position === null ? group.id === selector : group.position === position));
|
|
41
|
+
if (selected.length === 0) throw new Error(`Unknown group selector: ${selector}`);
|
|
42
|
+
return selected;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function reviewGroup(groupDirectory, id, { model, thinking, signal }) {
|
|
46
|
+
const definition = JSON.parse(await readFile(path.join(groupDirectory, "definition.json"), "utf8"));
|
|
47
|
+
const questions = Array.isArray(definition.review_questions) ? definition.review_questions : [];
|
|
48
|
+
if (questions.length === 0) return { id, state: "skipped", reason: "no review questions" };
|
|
49
|
+
const runs = {};
|
|
50
|
+
for (const variant of ["baseline", "candidate"]) {
|
|
51
|
+
runs[variant] = JSON.parse(await readFile(path.join(groupDirectory, variant, "run.json"), "utf8"));
|
|
52
|
+
if (runs[variant].state !== "complete")
|
|
53
|
+
return { id, state: "skipped", reason: `${variant} run is ${runs[variant].state}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const labels = randomInt(2) === 0 ? { A: "baseline", B: "candidate" } : { A: "candidate", B: "baseline" };
|
|
57
|
+
const prompt = reviewPrompt(definition, questions, { A: runs[labels.A], B: runs[labels.B] });
|
|
58
|
+
const reviewDirectory = path.join(groupDirectory, "review");
|
|
59
|
+
await rm(reviewDirectory, { recursive: true, force: true });
|
|
60
|
+
await mkdir(reviewDirectory, { recursive: true });
|
|
61
|
+
await writeText(path.join(reviewDirectory, "prompt.md"), prompt);
|
|
62
|
+
|
|
63
|
+
const args = [
|
|
64
|
+
"--mode",
|
|
65
|
+
"json",
|
|
66
|
+
"-p",
|
|
67
|
+
"--no-session",
|
|
68
|
+
"--no-extensions",
|
|
69
|
+
"--no-skills",
|
|
70
|
+
"--no-prompt-templates",
|
|
71
|
+
"--no-themes",
|
|
72
|
+
"--no-context-files",
|
|
73
|
+
"--no-approve",
|
|
74
|
+
"--no-tools",
|
|
75
|
+
"--model",
|
|
76
|
+
model,
|
|
77
|
+
];
|
|
78
|
+
if (thinking !== null) args.push("--thinking", thinking);
|
|
79
|
+
args.push(prompt);
|
|
80
|
+
const observation = await runPiProcess({
|
|
81
|
+
command: "pi",
|
|
82
|
+
args,
|
|
83
|
+
cwd: reviewDirectory,
|
|
84
|
+
eventsFile: path.join(reviewDirectory, "events.jsonl"),
|
|
85
|
+
stderrFile: path.join(reviewDirectory, "stderr.log"),
|
|
86
|
+
signal,
|
|
87
|
+
environment: { ...baseProcessEnvironment(), PWD: reviewDirectory, PI_SKIP_VERSION_CHECK: "1", PI_TELEMETRY: "0" },
|
|
88
|
+
});
|
|
89
|
+
await writeText(path.join(reviewDirectory, "final.md"), `${observation.response}\n`);
|
|
90
|
+
|
|
91
|
+
const errors = [];
|
|
92
|
+
if (observation.exitCode !== 0 || !observation.settled || observation.assistantError !== null) {
|
|
93
|
+
errors.push(
|
|
94
|
+
`reviewer did not complete: exit=${observation.exitCode} settled=${observation.settled} error=${observation.assistantError}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const answers = errors.length === 0 ? parseAnswers(observation.response, questions, labels, errors) : [];
|
|
98
|
+
const state = errors.length === 0 ? "complete" : "review-error";
|
|
99
|
+
await writeJson(path.join(groupDirectory, "semantic-grade.json"), {
|
|
100
|
+
schema_version: 1,
|
|
101
|
+
kind: "advisory-review",
|
|
102
|
+
advisory: true,
|
|
103
|
+
state,
|
|
104
|
+
model,
|
|
105
|
+
thinking,
|
|
106
|
+
labels,
|
|
107
|
+
prompt: { file: "review/prompt.md", sha256: sha256(prompt) },
|
|
108
|
+
answers,
|
|
109
|
+
errors,
|
|
110
|
+
usage: observation.usage,
|
|
111
|
+
artifacts: { events: "review/events.jsonl", final: "review/final.md", stderr: "review/stderr.log" },
|
|
112
|
+
completedAt: new Date().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
return { id, state, artifact: "semantic-grade.json" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reviewPrompt(definition, questions, runs) {
|
|
118
|
+
const task =
|
|
119
|
+
typeof definition.input?.prompt === "string" ? [definition.input.prompt] : (definition.input?.turns ?? []);
|
|
120
|
+
const lines = [
|
|
121
|
+
"You are reviewing two anonymous runs, Run A and Run B, of the same agent task.",
|
|
122
|
+
"Judge only the evidence below. Answer every question for both runs with yes, no, or unclear.",
|
|
123
|
+
"",
|
|
124
|
+
"Task turns:",
|
|
125
|
+
...task.map((turn, index) => `${index + 1}. ${turn}`),
|
|
126
|
+
"",
|
|
127
|
+
"Questions:",
|
|
128
|
+
...questions.map((question, index) => `${index + 1}. ${question}`),
|
|
129
|
+
];
|
|
130
|
+
for (const label of ["A", "B"]) lines.push("", `## Run ${label}`, ...runEvidence(runs[label]));
|
|
131
|
+
lines.push(
|
|
132
|
+
"",
|
|
133
|
+
"Reply with only JSON in this shape:",
|
|
134
|
+
'{"answers":[{"question":1,"A":"yes|no|unclear","B":"yes|no|unclear","rationale":"one sentence"}]}',
|
|
135
|
+
);
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runEvidence(run) {
|
|
140
|
+
const turns = Array.isArray(run.turns)
|
|
141
|
+
? run.turns
|
|
142
|
+
: [{ turn: 1, response: run.response, toolActivity: run.toolActivity }];
|
|
143
|
+
const lines = [];
|
|
144
|
+
for (const turn of turns) {
|
|
145
|
+
lines.push(`Turn ${turn.turn ?? 1} response:`, truncate(String(turn.response ?? ""), RESPONSE_LIMIT));
|
|
146
|
+
for (const call of turn.toolActivity ?? []) {
|
|
147
|
+
const status = call.completed ? (call.isError ? "failed" : "succeeded") : "incomplete";
|
|
148
|
+
lines.push(`- tool ${call.toolName} ${status}: ${truncate(JSON.stringify(call.args ?? null), ARGUMENT_LIMIT)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const changes = run.effects?.changes;
|
|
152
|
+
if (changes) {
|
|
153
|
+
for (const kind of ["created", "modified", "deleted"]) {
|
|
154
|
+
lines.push(`Files ${kind}: ${(changes[kind] ?? []).join(", ") || "none"}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function truncate(value, limit) {
|
|
161
|
+
return value.length <= limit ? value : `${value.slice(0, limit)}… [truncated ${value.length - limit} characters]`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseAnswers(response, questions, labels, errors) {
|
|
165
|
+
const text = response
|
|
166
|
+
.trim()
|
|
167
|
+
.replace(/^```(?:json)?\s*/i, "")
|
|
168
|
+
.replace(/\s*```$/, "");
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(text);
|
|
172
|
+
} catch {
|
|
173
|
+
errors.push("reviewer response is not JSON");
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
const answers = Array.isArray(parsed?.answers) ? parsed.answers : null;
|
|
177
|
+
if (answers === null || answers.length !== questions.length) {
|
|
178
|
+
errors.push(`reviewer must answer exactly ${questions.length} question(s)`);
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
const mapped = [];
|
|
182
|
+
for (const [index, question] of questions.entries()) {
|
|
183
|
+
const answer = answers.find((entry) => entry?.question === index + 1);
|
|
184
|
+
if (!answer || !ANSWERS.has(answer.A) || !ANSWERS.has(answer.B) || typeof answer.rationale !== "string") {
|
|
185
|
+
errors.push(`reviewer answer for question ${index + 1} is missing or invalid`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
mapped.push({
|
|
189
|
+
question,
|
|
190
|
+
[labels.A]: answer.A,
|
|
191
|
+
[labels.B]: answer.B,
|
|
192
|
+
rationale: answer.rationale,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return errors.length === 0 ? mapped : [];
|
|
196
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, realpath } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import { loadDefinition, selectDefinition } from "./definitions.mjs";
|
|
4
|
+
import { COMMAND_TOOL, loadDefinition, selectDefinition } from "./definitions.mjs";
|
|
5
5
|
import { createInvocationId, fileIdentity, writeJson, writeText } from "./evidence.mjs";
|
|
6
6
|
import { gradeDeterministic } from "./grade.mjs";
|
|
7
7
|
import { runBody, runDescription, VariantSetupError } from "./pi.mjs";
|
|
@@ -12,14 +12,24 @@ const DESCRIPTION_TOOLS = new Set(["read"]);
|
|
|
12
12
|
const BODY_TOOLS = new Set(["read", "write", "edit"]);
|
|
13
13
|
// Native tools must be separated from extension tools before CLI tool selection is built.
|
|
14
14
|
const NATIVE_TOOLS = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
|
|
15
|
-
const DEFAULT_RUNTIME = {
|
|
15
|
+
const DEFAULT_RUNTIME = {
|
|
16
|
+
host: "pi",
|
|
17
|
+
session: false,
|
|
18
|
+
prompt: "isolated",
|
|
19
|
+
extensions: [],
|
|
20
|
+
environment: { literal: {}, inherit: [] },
|
|
21
|
+
};
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* @param {string} definitionFile
|
|
19
25
|
* @param {any} selectors
|
|
20
|
-
* @param {{root?: string, signal?: AbortSignal}} options
|
|
26
|
+
* @param {{root?: string, signal?: AbortSignal, budget?: {maxCost: number, spent: number, exhausted: boolean} | null}} options
|
|
21
27
|
*/
|
|
22
|
-
export async function runEvaluation(
|
|
28
|
+
export async function runEvaluation(
|
|
29
|
+
definitionFile,
|
|
30
|
+
selectors = {},
|
|
31
|
+
{ root = process.cwd(), signal, budget = null } = {},
|
|
32
|
+
) {
|
|
23
33
|
const definitionPath = await realpath(path.resolve(root, definitionFile));
|
|
24
34
|
const definition = await loadDefinition(definitionPath, { root });
|
|
25
35
|
const selection = selectDefinition(definition, selectors);
|
|
@@ -45,9 +55,27 @@ export async function runEvaluation(definitionFile, selectors = {}, { root = pro
|
|
|
45
55
|
startedAt: new Date().toISOString(),
|
|
46
56
|
});
|
|
47
57
|
|
|
58
|
+
// Budget exhaustion cancels only queued subjects; a subject already running completes and is charged.
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
const onParentAbort = () => controller.abort();
|
|
61
|
+
if (signal?.aborted) controller.abort();
|
|
62
|
+
else signal?.addEventListener("abort", onParentAbort, { once: true });
|
|
63
|
+
const charge = (run) => {
|
|
64
|
+
if (budget === null) return;
|
|
65
|
+
budget.spent += run.usage?.cost?.total ?? 0;
|
|
66
|
+
if (budget.spent >= budget.maxCost) {
|
|
67
|
+
budget.exhausted = true;
|
|
68
|
+
controller.abort();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
48
72
|
const groups = [];
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
try {
|
|
74
|
+
for (const selected of selection.groups) {
|
|
75
|
+
groups.push(await runGroup({ selected, root, resultDirectory, signal: controller.signal, charge }));
|
|
76
|
+
}
|
|
77
|
+
} finally {
|
|
78
|
+
signal?.removeEventListener("abort", onParentAbort);
|
|
51
79
|
}
|
|
52
80
|
const state = batchState(groups);
|
|
53
81
|
const summary = {
|
|
@@ -58,6 +86,7 @@ export async function runEvaluation(definitionFile, selectors = {}, { root = pro
|
|
|
58
86
|
groups,
|
|
59
87
|
completedAt: new Date().toISOString(),
|
|
60
88
|
};
|
|
89
|
+
if (budget !== null) summary.budget = budgetEvidence(budget);
|
|
61
90
|
await writeJson(path.join(resultDirectory, "summary.json"), summary);
|
|
62
91
|
return { id: invocationId, path: resultDirectory, state, summary };
|
|
63
92
|
}
|
|
@@ -76,7 +105,10 @@ function assertSupportedSelection(selection) {
|
|
|
76
105
|
if (unsupportedNativeTools.length > 0) {
|
|
77
106
|
throw new Error(`${group.type} execution does not support native tools: ${unsupportedNativeTools.join(", ")}`);
|
|
78
107
|
}
|
|
79
|
-
|
|
108
|
+
// The evaluator supplies run_command itself when the definition declares commands.
|
|
109
|
+
const customTools = group.tools.filter(
|
|
110
|
+
(tool) => !NATIVE_TOOLS.has(tool) && !supportedTools.has(tool) && tool !== COMMAND_TOOL,
|
|
111
|
+
);
|
|
80
112
|
if (group.type === "description" && customTools.length > 0) {
|
|
81
113
|
throw new Error(`${group.type} execution does not support custom tools: ${customTools.join(", ")}`);
|
|
82
114
|
}
|
|
@@ -88,7 +120,11 @@ function assertSupportedSelection(selection) {
|
|
|
88
120
|
}
|
|
89
121
|
}
|
|
90
122
|
|
|
91
|
-
|
|
123
|
+
export function budgetEvidence(budget) {
|
|
124
|
+
return { maxCost: budget.maxCost, spent: Number(budget.spent.toFixed(10)), exhausted: budget.exhausted };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function runGroup({ selected, root, resultDirectory, signal, charge }) {
|
|
92
128
|
const { group } = selected;
|
|
93
129
|
const groupDirectory = path.join(resultDirectory, "groups", group.id);
|
|
94
130
|
await mkdir(groupDirectory, { recursive: true });
|
|
@@ -121,6 +157,7 @@ async function runGroup({ selected, root, resultDirectory, signal }) {
|
|
|
121
157
|
runs[variant] = cancelledRun(group, variant);
|
|
122
158
|
} else if (resourceError === null) {
|
|
123
159
|
runs[variant] = await executeVariant({ group, variant, root, variantDirectory, fixture, runtime, signal });
|
|
160
|
+
charge(runs[variant]);
|
|
124
161
|
} else {
|
|
125
162
|
runs[variant] = invalidRun(group, variant, resourceError);
|
|
126
163
|
}
|
|
@@ -110,6 +110,7 @@ export async function prepareRuntime({ runtime, root, groupDirectory }) {
|
|
|
110
110
|
return {
|
|
111
111
|
host: runtime.host,
|
|
112
112
|
session: runtime.session,
|
|
113
|
+
prompt: runtime.prompt ?? "isolated",
|
|
113
114
|
environment: { ...runtime.environment },
|
|
114
115
|
extensions: bundles,
|
|
115
116
|
};
|
|
@@ -145,6 +146,7 @@ export async function materializeRuntime({ runtime, variantDirectory }) {
|
|
|
145
146
|
return {
|
|
146
147
|
host: runtime.host,
|
|
147
148
|
session: runtime.session,
|
|
149
|
+
prompt: runtime.prompt ?? "isolated",
|
|
148
150
|
environment: { ...runtime.environment },
|
|
149
151
|
extensions: bundles,
|
|
150
152
|
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { createInvocationId, writeJson } from "./evidence.mjs";
|
|
5
|
+
import { budgetEvidence, runEvaluation } from "./runner.mjs";
|
|
6
|
+
|
|
7
|
+
const CHECK_STATES = ["pass", "fail", "unavailable"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs the selection as independent complete invocations and records an aggregate beside them.
|
|
11
|
+
* Each trial remains an ordinary result; the aggregate only counts their deterministic grades.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} definitionFile
|
|
14
|
+
* @param {any} selectors
|
|
15
|
+
* @param {{root?: string, signal?: AbortSignal, trials: number, maxCost: number | null, onTrial?: (result: any) => void}} options
|
|
16
|
+
*/
|
|
17
|
+
export async function runTrials(definitionFile, selectors, { root = process.cwd(), signal, trials, maxCost, onTrial }) {
|
|
18
|
+
const budget = maxCost === null ? null : { maxCost, spent: 0, exhausted: false };
|
|
19
|
+
const results = [];
|
|
20
|
+
for (let trial = 0; trial < trials; trial += 1) {
|
|
21
|
+
if (signal?.aborted || budget?.exhausted) break;
|
|
22
|
+
const result = await runEvaluation(definitionFile, selectors, { root, signal, budget });
|
|
23
|
+
results.push(result);
|
|
24
|
+
onTrial?.(result);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const id = createInvocationId();
|
|
28
|
+
const directory = path.join(root, ".skill-eval", "runs", id);
|
|
29
|
+
await mkdir(directory, { recursive: true });
|
|
30
|
+
const aggregate = {
|
|
31
|
+
schema_version: 1,
|
|
32
|
+
kind: "aggregate",
|
|
33
|
+
id,
|
|
34
|
+
state: aggregateState(results, trials),
|
|
35
|
+
requestedTrials: trials,
|
|
36
|
+
trials: results.map((result) => ({ id: result.id, state: result.state })),
|
|
37
|
+
groups: await aggregateGroups(results),
|
|
38
|
+
completedAt: new Date().toISOString(),
|
|
39
|
+
};
|
|
40
|
+
if (budget !== null) aggregate.budget = budgetEvidence(budget);
|
|
41
|
+
await writeJson(path.join(directory, "aggregate.json"), aggregate);
|
|
42
|
+
return { id, path: directory, state: aggregate.state, aggregate };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function aggregateState(results, requested) {
|
|
46
|
+
if (results.length < requested || results.some((result) => result.state === "cancelled")) return "cancelled";
|
|
47
|
+
if (results.every((result) => result.state === "complete")) return "complete";
|
|
48
|
+
return "partially-complete";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function aggregateGroups(results) {
|
|
52
|
+
/** @type {Map<string, any>} */
|
|
53
|
+
const groups = new Map();
|
|
54
|
+
for (const result of results) {
|
|
55
|
+
for (const group of result.summary.groups) {
|
|
56
|
+
const entry = groups.get(group.id) ?? { id: group.id, trials: 0, comparisons: new Map(), checks: new Map() };
|
|
57
|
+
groups.set(group.id, entry);
|
|
58
|
+
const gradeFile = path.join(
|
|
59
|
+
result.path,
|
|
60
|
+
"groups",
|
|
61
|
+
group.id,
|
|
62
|
+
group.artifacts?.grade ?? "deterministic-grade.json",
|
|
63
|
+
);
|
|
64
|
+
const grade = JSON.parse(await readFile(gradeFile, "utf8"));
|
|
65
|
+
entry.trials += 1;
|
|
66
|
+
for (const check of grade.checks ?? []) {
|
|
67
|
+
const counts = entry.checks.get(check.id) ?? {
|
|
68
|
+
id: check.id,
|
|
69
|
+
variant: check.variant,
|
|
70
|
+
kind: check.kind,
|
|
71
|
+
...zeroCounts(),
|
|
72
|
+
};
|
|
73
|
+
entry.checks.set(check.id, counts);
|
|
74
|
+
if (CHECK_STATES.includes(check.state)) counts[check.state] += 1;
|
|
75
|
+
}
|
|
76
|
+
for (const comparison of grade.comparisons ?? []) {
|
|
77
|
+
const counts = entry.comparisons.get(comparison.id) ?? {
|
|
78
|
+
id: comparison.id,
|
|
79
|
+
kind: comparison.kind,
|
|
80
|
+
baseline: zeroCounts(),
|
|
81
|
+
candidate: zeroCounts(),
|
|
82
|
+
transitions: {},
|
|
83
|
+
};
|
|
84
|
+
entry.comparisons.set(comparison.id, counts);
|
|
85
|
+
for (const variant of ["baseline", "candidate"]) {
|
|
86
|
+
const state = comparison[variant]?.state;
|
|
87
|
+
if (CHECK_STATES.includes(state)) counts[variant][state] += 1;
|
|
88
|
+
}
|
|
89
|
+
counts.transitions[comparison.transition] = (counts.transitions[comparison.transition] ?? 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return [...groups.values()].map((entry) => ({
|
|
94
|
+
id: entry.id,
|
|
95
|
+
trials: entry.trials,
|
|
96
|
+
comparisons: [...entry.comparisons.values()],
|
|
97
|
+
checks: [...entry.checks.values()],
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function zeroCounts() {
|
|
102
|
+
return { pass: 0, fail: 0, unavailable: 0 };
|
|
103
|
+
}
|
|
@@ -22,6 +22,13 @@ class ViewError extends Error {
|
|
|
22
22
|
|
|
23
23
|
export async function renderResult(target, selectors = DEFAULT_SELECTORS, { root = process.cwd() } = {}) {
|
|
24
24
|
const resultDirectory = await resolveResult(target, root);
|
|
25
|
+
const aggregateFile = path.join(resultDirectory, "aggregate.json");
|
|
26
|
+
if (await stat(aggregateFile).catch(() => null)) {
|
|
27
|
+
if (selectors.group !== null || selectors.variant !== null) {
|
|
28
|
+
throw new ViewError("selectors are not supported for an aggregate; view an individual trial instead");
|
|
29
|
+
}
|
|
30
|
+
return renderAggregate(resultDirectory, await readJson(aggregateFile, "aggregate"));
|
|
31
|
+
}
|
|
25
32
|
const summary = await readJson(path.join(resultDirectory, "summary.json"), "result summary");
|
|
26
33
|
let groups = Array.isArray(summary.groups) ? summary.groups : [];
|
|
27
34
|
if (summary.definitionKind === "group" && selectors.group !== null) {
|
|
@@ -56,6 +63,7 @@ async function renderGroup(resultDirectory, group, selectedVariant) {
|
|
|
56
63
|
|
|
57
64
|
appendSharedEvidence(lines, definition);
|
|
58
65
|
appendGradeEvidence(lines, grade, group, definition, selectedVariant);
|
|
66
|
+
await appendAdvisoryReview(lines, groupDirectory, selectedVariant);
|
|
59
67
|
for (const variant of variants) {
|
|
60
68
|
await appendVariantEvidence(lines, resultDirectory, groupDirectory, group.id, variant);
|
|
61
69
|
}
|
|
@@ -135,6 +143,64 @@ function appendGradeEvidence(lines, grade, group, definition, selectedVariant) {
|
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
// Advisory answers stay visually separate from deterministic grades; they are not canonical evidence.
|
|
147
|
+
async function appendAdvisoryReview(lines, groupDirectory, selectedVariant) {
|
|
148
|
+
const file = path.join(groupDirectory, "semantic-grade.json");
|
|
149
|
+
if (!(await stat(file).catch(() => null))) return;
|
|
150
|
+
const review = await readJson(file, "advisory review");
|
|
151
|
+
lines.push(`Advisory review [${review.state}] model=${review.model} (not canonical evidence)`);
|
|
152
|
+
const variants = selectedVariant === null ? VARIANTS : [selectedVariant];
|
|
153
|
+
for (const [index, answer] of (review.answers ?? []).entries()) {
|
|
154
|
+
lines.push(
|
|
155
|
+
compactRow("review", [
|
|
156
|
+
`Q${index + 1}`,
|
|
157
|
+
...variants.map((variant) => `${variant}=${answer[variant]}`),
|
|
158
|
+
answer.rationale,
|
|
159
|
+
]),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
for (const error of review.errors ?? []) lines.push(compactRow("error", ["review", error]));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderAggregate(resultDirectory, aggregate) {
|
|
166
|
+
const lines = [
|
|
167
|
+
`Aggregate ${aggregate.id} [${aggregate.state}] trials=${aggregate.trials.length}/${aggregate.requestedTrials}`,
|
|
168
|
+
`Path: ${escapeCell(resultDirectory)}`,
|
|
169
|
+
];
|
|
170
|
+
if (aggregate.budget) {
|
|
171
|
+
lines.push(
|
|
172
|
+
compactRow("budget", [
|
|
173
|
+
`max-cost=${formatNumber(aggregate.budget.maxCost)}`,
|
|
174
|
+
`spent=${formatNumber(aggregate.budget.spent)}`,
|
|
175
|
+
`exhausted=${aggregate.budget.exhausted}`,
|
|
176
|
+
]),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
for (const trial of aggregate.trials) lines.push(compactRow("trial", [trial.id, trial.state]));
|
|
180
|
+
for (const group of aggregate.groups) {
|
|
181
|
+
lines.push("", `Group ${group.id} trials=${group.trials}`);
|
|
182
|
+
for (const comparison of group.comparisons) {
|
|
183
|
+
lines.push(
|
|
184
|
+
compactRow("comparison", [
|
|
185
|
+
comparison.id,
|
|
186
|
+
comparison.kind,
|
|
187
|
+
`baseline=${passRate(comparison.baseline)}`,
|
|
188
|
+
`candidate=${passRate(comparison.candidate)}`,
|
|
189
|
+
...Object.entries(comparison.transitions).map(([transition, count]) => `${transition}=${count}`),
|
|
190
|
+
]),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
for (const check of group.checks) {
|
|
194
|
+
lines.push(compactRow("check", [check.id, check.variant, check.kind, `pass=${passRate(check)}`]));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return `${lines.join("\n")}\n`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function passRate(counts) {
|
|
201
|
+
return `${counts.pass}/${counts.pass + counts.fail + counts.unavailable}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
138
204
|
function selectedGradeErrors(errors, definition, selectedVariant) {
|
|
139
205
|
if (!Array.isArray(errors)) return [];
|
|
140
206
|
if (selectedVariant === null) return errors;
|
|
@@ -185,7 +251,7 @@ async function appendVariantEvidence(lines, resultDirectory, groupDirectory, gro
|
|
|
185
251
|
);
|
|
186
252
|
}
|
|
187
253
|
|
|
188
|
-
async function resolveResult(target, root) {
|
|
254
|
+
export async function resolveResult(target, root) {
|
|
189
255
|
const explicit = path.resolve(root, target);
|
|
190
256
|
const stored = path.join(root, ".skill-eval", "runs", target);
|
|
191
257
|
const isPath = path.isAbsolute(target) || target.includes("/") || target.includes("\\") || target.startsWith(".");
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { COMMAND_TOOL } from "./lib/definitions.mjs";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 120 * 1000;
|
|
6
|
+
const OUTPUT_LIMIT_BYTES = 32 * 1024;
|
|
7
|
+
const TERMINATION_GRACE_MS = 2000;
|
|
8
|
+
|
|
9
|
+
function parseCommands(value) {
|
|
10
|
+
const parsed = JSON.parse(value ?? "[]");
|
|
11
|
+
if (!Array.isArray(parsed) || parsed.length === 0) throw new Error("SKILL_EVAL_COMMANDS must declare commands");
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function commandEnvironment() {
|
|
16
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("SKILL_EVAL_")));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function captureStream(stream) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
const capture = { bytes: 0, kept: 0, truncated: false };
|
|
22
|
+
stream.on("data", (chunk) => {
|
|
23
|
+
capture.bytes += chunk.length;
|
|
24
|
+
const room = OUTPUT_LIMIT_BYTES - capture.kept;
|
|
25
|
+
if (room <= 0) {
|
|
26
|
+
capture.truncated = true;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const kept = chunk.length > room ? chunk.subarray(0, room) : chunk;
|
|
30
|
+
if (kept.length < chunk.length) capture.truncated = true;
|
|
31
|
+
chunks.push(kept);
|
|
32
|
+
capture.kept += kept.length;
|
|
33
|
+
});
|
|
34
|
+
capture.text = () => Buffer.concat(chunks).toString("utf8");
|
|
35
|
+
return capture;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function signalGroup(child, signal) {
|
|
39
|
+
try {
|
|
40
|
+
if (process.platform === "win32") child.kill(signal);
|
|
41
|
+
else process.kill(-child.pid, signal);
|
|
42
|
+
} catch {
|
|
43
|
+
// The process group already exited.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Runs one author-declared argv in the workspace. A nonzero exit or timeout is ordinary evidence, not a tool error.
|
|
48
|
+
function runCommand(command, cwd, signal) {
|
|
49
|
+
const timeoutMs = command.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(command.argv[0], command.argv.slice(1), {
|
|
52
|
+
cwd,
|
|
53
|
+
env: commandEnvironment(),
|
|
54
|
+
shell: false,
|
|
55
|
+
detached: process.platform !== "win32",
|
|
56
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
const stdout = captureStream(child.stdout);
|
|
59
|
+
const stderr = captureStream(child.stderr);
|
|
60
|
+
let timedOut = false;
|
|
61
|
+
let forced;
|
|
62
|
+
const stop = () => {
|
|
63
|
+
signalGroup(child, "SIGTERM");
|
|
64
|
+
forced = setTimeout(() => signalGroup(child, "SIGKILL"), TERMINATION_GRACE_MS);
|
|
65
|
+
};
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
timedOut = true;
|
|
68
|
+
stop();
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
const onAbort = () => stop();
|
|
71
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
72
|
+
child.once("error", (error) => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
clearTimeout(forced);
|
|
75
|
+
signal?.removeEventListener("abort", onAbort);
|
|
76
|
+
reject(error);
|
|
77
|
+
});
|
|
78
|
+
child.once("close", (exitCode, exitSignal) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
clearTimeout(forced);
|
|
81
|
+
signal?.removeEventListener("abort", onAbort);
|
|
82
|
+
resolve({ exitCode, signal: exitSignal, timedOut, timeoutMs, stdout, stderr });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderOutput(label, capture) {
|
|
88
|
+
const note = capture.truncated ? ` (truncated: showing ${capture.kept} of ${capture.bytes} bytes)` : "";
|
|
89
|
+
return `--- ${label}${note} ---\n${capture.text()}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default function registerCommandTool(pi) {
|
|
93
|
+
const commands = parseCommands(process.env.SKILL_EVAL_COMMANDS);
|
|
94
|
+
const workspace = process.env.SKILL_EVAL_WRITABLE_ROOT;
|
|
95
|
+
if (!workspace) throw new Error("SKILL_EVAL_WRITABLE_ROOT is required");
|
|
96
|
+
const byId = new Map(commands.map((command) => [command.id, command]));
|
|
97
|
+
|
|
98
|
+
pi.registerTool({
|
|
99
|
+
name: COMMAND_TOOL,
|
|
100
|
+
label: "Run command",
|
|
101
|
+
description: `Run one declared command in the workspace. Available commands: ${commands
|
|
102
|
+
.map((command) => `${command.id} (${command.argv.join(" ")})`)
|
|
103
|
+
.join("; ")}.`,
|
|
104
|
+
parameters: {
|
|
105
|
+
type: "object",
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
required: ["command"],
|
|
108
|
+
properties: {
|
|
109
|
+
command: { type: "string", enum: commands.map((command) => command.id), description: "Declared command ID" },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
executionMode: "sequential",
|
|
113
|
+
async execute(_toolCallId, input, signal) {
|
|
114
|
+
const command = byId.get(input?.command);
|
|
115
|
+
if (!command) throw new Error(`Command is not declared for this evaluation: ${String(input?.command)}`);
|
|
116
|
+
const result = await runCommand(command, workspace, signal);
|
|
117
|
+
const status = result.timedOut
|
|
118
|
+
? `timed out after ${result.timeoutMs} ms`
|
|
119
|
+
: `exit code: ${result.exitCode ?? `signal ${result.signal}`}`;
|
|
120
|
+
return {
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: [
|
|
125
|
+
`$ ${command.argv.join(" ")}`,
|
|
126
|
+
status,
|
|
127
|
+
renderOutput("stdout", result.stdout),
|
|
128
|
+
renderOutput("stderr", result.stderr),
|
|
129
|
+
].join("\n"),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
details: {
|
|
133
|
+
id: command.id,
|
|
134
|
+
argv: command.argv,
|
|
135
|
+
exitCode: result.exitCode,
|
|
136
|
+
signal: result.signal,
|
|
137
|
+
timedOut: result.timedOut,
|
|
138
|
+
stdout: { bytes: result.stdout.bytes, truncated: result.stdout.truncated },
|
|
139
|
+
stderr: { bytes: result.stderr.bytes, truncated: result.stderr.truncated },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|