@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,81 @@
|
|
|
1
|
+
// Declared skills on Claude Code. Its own skill loader would also find bundled and user skills, so the evaluator
|
|
2
|
+
// keeps it off and delivers declared skills the way Pi's evaluated subjects see them: a listing of each skill's
|
|
3
|
+
// name, description and SKILL.md location in the system prompt, and, for a body run's target, Pi's exact skill
|
|
4
|
+
// command expansion as the first prompt.
|
|
5
|
+
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
|
|
9
|
+
import { escapeXml } from "../../../guard-rules.mjs";
|
|
10
|
+
|
|
11
|
+
const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
12
|
+
|
|
13
|
+
function scalar(value) {
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
if (trimmed.startsWith('"')) return JSON.parse(trimmed);
|
|
16
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1).replaceAll("''", "'");
|
|
17
|
+
return trimmed;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A declared skill's SKILL.md: its frontmatter `name`, `description` and `disable-model-invocation`, and its body.
|
|
22
|
+
* Only one-line scalars are read; a skill whose name or description this cannot read is refused, never guessed.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} directory the materialized skill directory
|
|
25
|
+
*/
|
|
26
|
+
export async function readSkill(directory) {
|
|
27
|
+
const filePath = path.join(directory, "SKILL.md");
|
|
28
|
+
const contents = await readFile(filePath, "utf8");
|
|
29
|
+
const match = FRONTMATTER.exec(contents);
|
|
30
|
+
if (match === null) throw new Error(`skill has no frontmatter: ${filePath}`);
|
|
31
|
+
const fields = {};
|
|
32
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
33
|
+
const field = /^([A-Za-z][\w-]*):(.*)$/.exec(line);
|
|
34
|
+
if (field !== null) fields[field[1]] = field[2];
|
|
35
|
+
}
|
|
36
|
+
const read = (key) => {
|
|
37
|
+
if (fields[key] === undefined) return undefined;
|
|
38
|
+
try {
|
|
39
|
+
return scalar(fields[key]);
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const name = read("name");
|
|
45
|
+
const description = read("description");
|
|
46
|
+
if (!name || !description || /^[>|]/.test(name) || /^[>|]/.test(description)) {
|
|
47
|
+
throw new Error(`skill needs a one-line name and description the Claude Code adapter can read: ${filePath}`);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
description,
|
|
52
|
+
filePath,
|
|
53
|
+
baseDir: directory,
|
|
54
|
+
disableModelInvocation: read("disable-model-invocation") === "true",
|
|
55
|
+
body: contents.slice(match[0].length).trim(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The `<available_skills>` listing for the system prompt, as Pi writes it; empty when no skill may be invoked. */
|
|
60
|
+
export function skillListing(skills) {
|
|
61
|
+
const listed = skills.filter((skill) => !skill.disableModelInvocation);
|
|
62
|
+
if (listed.length === 0) return "";
|
|
63
|
+
const lines = ["<available_skills>"];
|
|
64
|
+
for (const skill of listed) {
|
|
65
|
+
lines.push(
|
|
66
|
+
" <skill>",
|
|
67
|
+
` <name>${escapeXml(skill.name)}</name>`,
|
|
68
|
+
` <description>${escapeXml(skill.description)}</description>`,
|
|
69
|
+
` <location>${escapeXml(skill.filePath)}</location>`,
|
|
70
|
+
" </skill>",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
lines.push("</available_skills>");
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The prompt Pi delivers for `/skill:<name> <prompt>`, so an explicit invocation reads the same on both hosts. */
|
|
78
|
+
export function skillCommandPrompt(skill, prompt) {
|
|
79
|
+
const block = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${skill.body}\n</skill>`;
|
|
80
|
+
return prompt ? `${block}\n\n${prompt}` : block;
|
|
81
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Evaluation hosts. A host is the agent program that runs a subject's model and tools: Pi or Claude Code. The evaluator's
|
|
2
|
+
// core is host-neutral; each host's adapter implements the same interface:
|
|
3
|
+
//
|
|
4
|
+
// - name, label, command; version(environment): the installed host's version.
|
|
5
|
+
// - resolveExtensions(declared, { deferSources }): declared host extensions, found among the installed ones.
|
|
6
|
+
// - refreshLogins(providers): make every login a batch uses valid before it starts.
|
|
7
|
+
// - effectiveSettings(declared): the evaluator's host defaults with declared settings winning.
|
|
8
|
+
// - prepareIsolation(directory, model, { settings }) -> { environment, evidence, loginDirectory }: a private home,
|
|
9
|
+
// temp and host configuration for one process, with only the declared provider's login, and the directory that
|
|
10
|
+
// login lives in (the shell probes plant a marker there); removeCredentials(directory) after.
|
|
11
|
+
// - prepareLaunch(context) -> { args, environment }: one subject's arguments and the host's own variables.
|
|
12
|
+
// - runOneShot(options), runSession(options): run a subject and return the normalized observation (see subject.mjs).
|
|
13
|
+
// - modelCall(directory, prompt, options): one tool-less call for a judge, reviewer or simulated user.
|
|
14
|
+
// - probeShell(options): run the shell tier's confinement probes through the host (see shell.mjs).
|
|
15
|
+
//
|
|
16
|
+
// Names a definition may use are in names.mjs, which definitions.mjs reads without loading any adapter.
|
|
17
|
+
|
|
18
|
+
import { claudeCodeHost } from "./claude-code/index.mjs";
|
|
19
|
+
import { piHost } from "./pi/index.mjs";
|
|
20
|
+
|
|
21
|
+
const ADAPTERS = new Map([
|
|
22
|
+
[piHost.name, piHost],
|
|
23
|
+
[claudeCodeHost.name, claudeCodeHost],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** The adapter for a host name; an unknown name is an error, never a fallback to Pi. */
|
|
27
|
+
export function hostAdapter(name) {
|
|
28
|
+
const adapter = ADAPTERS.get(name);
|
|
29
|
+
if (adapter === undefined) throw new Error(`unknown evaluation host: ${name}`);
|
|
30
|
+
return adapter;
|
|
31
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Pi's part of the shell tier's confinement probes (shell.mjs): the arm's own Pi, with its extensions and the
|
|
2
|
+
// evaluator's guard, runs every probe as a bash call from a local scripted model, and reports what each returned.
|
|
3
|
+
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
import { baseProcessEnvironment } from "../../process.mjs";
|
|
9
|
+
import { containerRouteExtensionPath, guardExtensionPath, runPiProcess } from "./process.mjs";
|
|
10
|
+
|
|
11
|
+
const PROBE_PROVIDER = "skill-eval-probe";
|
|
12
|
+
const PROBE_MODEL = "confinement";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs the probes through Pi in JSON mode. `guard` is the guard's environment for the probe workspace; `isolation`
|
|
16
|
+
* is the probe's prepared isolation, whose agent directory gets the scripted model.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Promise<{observation: any, launchError: string | null, results: Map<string, {ran: boolean, isError: boolean, output: string}>}>}
|
|
19
|
+
*/
|
|
20
|
+
export async function probeShell({ probes, workspace, isolation, extensions, guard, eventsFile, stderrFile, signal }) {
|
|
21
|
+
const model = await startModelStub(probes);
|
|
22
|
+
try {
|
|
23
|
+
const agent = path.join(isolation.evidence.root, "agent");
|
|
24
|
+
await writeFile(
|
|
25
|
+
path.join(agent, "models.json"),
|
|
26
|
+
`${JSON.stringify({ providers: { [PROBE_PROVIDER]: { baseUrl: model.baseUrl, api: "openai-completions", apiKey: "probe", models: [{ id: PROBE_MODEL }] } } }, null, 2)}\n`,
|
|
27
|
+
);
|
|
28
|
+
const environment = {
|
|
29
|
+
...baseProcessEnvironment(),
|
|
30
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
31
|
+
PI_TELEMETRY: "0",
|
|
32
|
+
...guard,
|
|
33
|
+
...isolation.environment,
|
|
34
|
+
};
|
|
35
|
+
const args = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--extension", guardExtensionPath()];
|
|
36
|
+
for (const extension of extensions) args.push("--extension", extension.path);
|
|
37
|
+
// A container shell's commands reach the container through the route extension, last, as in a subject.
|
|
38
|
+
if (guard.SKILL_EVAL_CONTAINER) args.push("--extension", containerRouteExtensionPath());
|
|
39
|
+
args.push(
|
|
40
|
+
"--no-skills",
|
|
41
|
+
"--no-prompt-templates",
|
|
42
|
+
"--no-themes",
|
|
43
|
+
"--no-context-files",
|
|
44
|
+
"--no-approve",
|
|
45
|
+
"--tools",
|
|
46
|
+
"bash",
|
|
47
|
+
"--model",
|
|
48
|
+
`${PROBE_PROVIDER}/${PROBE_MODEL}`,
|
|
49
|
+
"Run each confinement probe with bash, exactly as given.",
|
|
50
|
+
);
|
|
51
|
+
let observation = null;
|
|
52
|
+
let launchError = null;
|
|
53
|
+
try {
|
|
54
|
+
observation = await runPiProcess({
|
|
55
|
+
command: "pi",
|
|
56
|
+
args,
|
|
57
|
+
cwd: workspace,
|
|
58
|
+
eventsFile,
|
|
59
|
+
stderrFile,
|
|
60
|
+
signal,
|
|
61
|
+
environment,
|
|
62
|
+
});
|
|
63
|
+
} catch (error) {
|
|
64
|
+
launchError = error instanceof Error ? error.message : String(error);
|
|
65
|
+
}
|
|
66
|
+
// Whatever Pi reported before a launch error still counts.
|
|
67
|
+
return { observation, launchError, results: await toolResults(eventsFile) };
|
|
68
|
+
} finally {
|
|
69
|
+
await model.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Each probe's tool result from Pi's JSON events, by tool call ID. */
|
|
74
|
+
async function toolResults(eventsFile) {
|
|
75
|
+
const results = new Map();
|
|
76
|
+
const contents = await readFile(eventsFile, "utf8").catch(() => "");
|
|
77
|
+
for (const line of contents.split("\n")) {
|
|
78
|
+
if (line.trim() === "") continue;
|
|
79
|
+
let event;
|
|
80
|
+
try {
|
|
81
|
+
event = JSON.parse(line);
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (event.type !== "tool_execution_end" || typeof event.toolCallId !== "string") continue;
|
|
86
|
+
const content = Array.isArray(event.result?.content) ? event.result.content : [];
|
|
87
|
+
results.set(event.toolCallId, {
|
|
88
|
+
ran: true,
|
|
89
|
+
isError: event.isError === true,
|
|
90
|
+
output: content
|
|
91
|
+
.filter((part) => part?.type === "text")
|
|
92
|
+
.map((part) => part.text)
|
|
93
|
+
.join(""),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A local OpenAI-compatible chat-completions endpoint that answers the first request with every probe as a bash
|
|
101
|
+
* tool call, and any request carrying tool results with a short final answer. No model is called.
|
|
102
|
+
*/
|
|
103
|
+
function startModelStub(probes) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const server = http.createServer((request, response) => {
|
|
106
|
+
let body = "";
|
|
107
|
+
request.setEncoding("utf8");
|
|
108
|
+
request.on("data", (chunk) => {
|
|
109
|
+
body += chunk;
|
|
110
|
+
});
|
|
111
|
+
request.on("end", () => {
|
|
112
|
+
let parsed = {};
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(body || "{}");
|
|
115
|
+
} catch {
|
|
116
|
+
// Answered as a first request.
|
|
117
|
+
}
|
|
118
|
+
const followUp = Array.isArray(parsed.messages) && parsed.messages.some((message) => message?.role === "tool");
|
|
119
|
+
const message = followUp
|
|
120
|
+
? { role: "assistant", content: "Probes finished." }
|
|
121
|
+
: {
|
|
122
|
+
role: "assistant",
|
|
123
|
+
content: null,
|
|
124
|
+
tool_calls: probes.map((probe, index) => ({
|
|
125
|
+
index,
|
|
126
|
+
id: probe.id,
|
|
127
|
+
type: "function",
|
|
128
|
+
function: { name: "bash", arguments: JSON.stringify({ command: probe.command }) },
|
|
129
|
+
})),
|
|
130
|
+
};
|
|
131
|
+
const finish = followUp ? "stop" : "tool_calls";
|
|
132
|
+
const base = { id: "probe", created: 0, model: parsed.model ?? PROBE_MODEL };
|
|
133
|
+
const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
134
|
+
if (parsed.stream !== true) {
|
|
135
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
136
|
+
response.end(
|
|
137
|
+
JSON.stringify({
|
|
138
|
+
...base,
|
|
139
|
+
object: "chat.completion",
|
|
140
|
+
choices: [
|
|
141
|
+
{
|
|
142
|
+
index: 0,
|
|
143
|
+
message: { ...message, tool_calls: message.tool_calls?.map(({ index: _index, ...call }) => call) },
|
|
144
|
+
finish_reason: finish,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
usage,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
|
|
153
|
+
const send = (chunk) =>
|
|
154
|
+
response.write(`data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", ...chunk })}\n\n`);
|
|
155
|
+
send({ choices: [{ index: 0, delta: message, finish_reason: null }] });
|
|
156
|
+
send({ choices: [{ index: 0, delta: {}, finish_reason: finish }] });
|
|
157
|
+
send({ choices: [], usage });
|
|
158
|
+
response.end("data: [DONE]\n\n");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
server.once("error", reject);
|
|
162
|
+
server.listen(0, "127.0.0.1", () => {
|
|
163
|
+
resolve({
|
|
164
|
+
baseUrl: `http://127.0.0.1:${server.address().port}/v1`,
|
|
165
|
+
close: () =>
|
|
166
|
+
new Promise((done) => {
|
|
167
|
+
server.closeAllConnections?.();
|
|
168
|
+
server.close(() => done());
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Pi-specific knowledge the evaluator needs outside a subject process: where the user's Pi agent directory
|
|
2
|
+
// is, how installed Pi packages are found, and how a stored OAuth login is kept fresh. Kept in one module so a
|
|
3
|
+
// later host adapter has one place to grow from (D-007).
|
|
4
|
+
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { mkdir, readFile, rm, stat } from "node:fs/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
|
|
13
|
+
const run = promisify(execFile);
|
|
14
|
+
const REFRESH_HELPER = fileURLToPath(new URL("../../../pi-login-refresh.mjs", import.meta.url));
|
|
15
|
+
const LOCK_STALE_MS = 2 * 60 * 1000;
|
|
16
|
+
const LOCK_WAIT_MS = 90 * 1000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pi settings the evaluator uses unless a configuration's host.settings declares otherwise: automatic retry and
|
|
20
|
+
* compaction stay off, so a provider error or a compaction never happens silently inside a measured run.
|
|
21
|
+
*/
|
|
22
|
+
export const EVALUATOR_PI_SETTINGS = Object.freeze({ retry: { enabled: false }, compaction: { enabled: false } });
|
|
23
|
+
|
|
24
|
+
/** The settings a subject actually runs with: the evaluator's defaults, with every declared setting winning. */
|
|
25
|
+
export function effectivePiSettings(declared = {}) {
|
|
26
|
+
const merge = (base, overlay) => {
|
|
27
|
+
const result = structuredClone(base);
|
|
28
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
29
|
+
const isObject = (entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry);
|
|
30
|
+
result[key] = isObject(result[key]) && isObject(value) ? merge(result[key], value) : structuredClone(value);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
return merge(EVALUATOR_PI_SETTINGS, declared);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The user's Pi agent directory: an explicit override for the evaluator, then Pi's own variable, then its default. */
|
|
38
|
+
export function piAgentDirectory() {
|
|
39
|
+
return (
|
|
40
|
+
process.env.SKILL_EVAL_CREDENTIALS_DIR ||
|
|
41
|
+
process.env.PI_CODING_AGENT_DIR ||
|
|
42
|
+
path.join(process.env.HOME || os.homedir(), ".pi", "agent")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function readJsonObject(file) {
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
49
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Finds each declared package extension among the user's installed Pi packages (`<agent dir>/npm/node_modules`),
|
|
57
|
+
* by exact name and version. A missing package or another version refuses the run. An extension built from source
|
|
58
|
+
* (`source` or `path`, see build-source.mjs) is built per group into its resources, so with `deferSources` it stays
|
|
59
|
+
* in its declared position as `{ pending: <declared entry> }`; without it, it is refused.
|
|
60
|
+
*
|
|
61
|
+
* @param {Array<any>} declared
|
|
62
|
+
* @param {{deferSources?: boolean}} [options]
|
|
63
|
+
*/
|
|
64
|
+
export async function resolveHostExtensions(declared, { deferSources = false } = {}) {
|
|
65
|
+
const root = path.join(piAgentDirectory(), "npm", "node_modules");
|
|
66
|
+
const resolved = [];
|
|
67
|
+
for (const extension of declared) {
|
|
68
|
+
if (!Object.hasOwn(extension, "package")) {
|
|
69
|
+
if (!deferSources) throw new Error("host.extensions built from source are supported only in arm configurations");
|
|
70
|
+
resolved.push({ pending: extension });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const directory = path.join(root, ...extension.package.split("/"));
|
|
74
|
+
const manifest = await readJsonObject(path.join(directory, "package.json"));
|
|
75
|
+
if (manifest === null) {
|
|
76
|
+
throw new Error(`host.extensions: ${extension.package} is not installed in Pi (looked in ${root})`);
|
|
77
|
+
}
|
|
78
|
+
if (manifest.version !== extension.version) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`host.extensions: ${extension.package} is ${manifest.version} in Pi, but the definition declares ${extension.version}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
resolved.push({ path: directory, package: `${extension.package}@${extension.version}` });
|
|
84
|
+
}
|
|
85
|
+
return resolved;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Minutes left on a provider's stored OAuth login, or null when the login is not OAuth or not stored. */
|
|
89
|
+
export async function loginMinutesLeft(provider) {
|
|
90
|
+
const login = (await readJsonObject(path.join(piAgentDirectory(), "auth.json")))?.[provider];
|
|
91
|
+
if (!login || login.type !== "oauth" || typeof login.expires !== "number") return null;
|
|
92
|
+
return Math.floor((login.expires - Date.now()) / 60000);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Makes a provider's stored OAuth login valid for at least `target` minutes when it has fewer than `threshold`
|
|
97
|
+
* left, by asking the installed Pi to refresh it in the user's own agent directory. One evaluator process
|
|
98
|
+
* refreshes at a time; the others wait and then reread. API-key and absent logins are left alone.
|
|
99
|
+
*
|
|
100
|
+
* @returns {Promise<number | null>} minutes left afterwards, or null when the login is not OAuth
|
|
101
|
+
*/
|
|
102
|
+
export async function ensureLogin(provider, { threshold, target }) {
|
|
103
|
+
const before = await loginMinutesLeft(provider);
|
|
104
|
+
if (before === null || before >= threshold) return before;
|
|
105
|
+
const authPath = path.join(piAgentDirectory(), "auth.json");
|
|
106
|
+
await withLock(authPath, async () => {
|
|
107
|
+
const current = await loginMinutesLeft(provider);
|
|
108
|
+
if (current !== null && current >= threshold) return;
|
|
109
|
+
const helper = process.env.SKILL_EVAL_LOGIN_REFRESH || REFRESH_HELPER;
|
|
110
|
+
try {
|
|
111
|
+
await run(process.execPath, [helper, authPath, provider, String(target)], { timeout: 60000 });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const detail = error?.stderr?.trim() || (error instanceof Error ? error.message : String(error));
|
|
114
|
+
throw new Error(`refreshing the ${provider} login failed (log in again in Pi): ${detail}`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const after = await loginMinutesLeft(provider);
|
|
118
|
+
if (after === null || after < threshold) {
|
|
119
|
+
throw new Error(`the ${provider} login has ${after ?? 0} minutes left after a refresh; log in again in Pi`);
|
|
120
|
+
}
|
|
121
|
+
return after;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function withLock(authPath, work) {
|
|
125
|
+
const lock = path.join(
|
|
126
|
+
os.tmpdir(),
|
|
127
|
+
`skill-eval-login-${createHash("sha256").update(authPath).digest("hex").slice(0, 16)}.lock`,
|
|
128
|
+
);
|
|
129
|
+
await mkdir(path.dirname(lock), { recursive: true });
|
|
130
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
131
|
+
for (;;) {
|
|
132
|
+
try {
|
|
133
|
+
await mkdir(lock);
|
|
134
|
+
break;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error?.code !== "EEXIST") throw error;
|
|
137
|
+
const info = await stat(lock).catch(() => null);
|
|
138
|
+
if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS) await rm(lock, { recursive: true, force: true });
|
|
139
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for another login refresh (${lock})`);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
return await work();
|
|
145
|
+
} finally {
|
|
146
|
+
await rm(lock, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The Pi host adapter: everything the evaluator does that depends on Pi, behind the interface in ../index.mjs.
|
|
2
|
+
|
|
3
|
+
import { installedHostVersion } from "../../identity.mjs";
|
|
4
|
+
import { baseProcessEnvironment } from "../../process.mjs";
|
|
5
|
+
import { probeShell } from "./confinement.mjs";
|
|
6
|
+
import { effectivePiSettings, ensureLogin, resolveHostExtensions } from "./host.mjs";
|
|
7
|
+
import { prepareIsolation, removeCopiedCredentials, TARGET_MINUTES } from "./isolation.mjs";
|
|
8
|
+
import { launchModel } from "./model-call.mjs";
|
|
9
|
+
import { piArguments, runPiProcess } from "./process.mjs";
|
|
10
|
+
import { runRpcBodySession } from "./rpc.mjs";
|
|
11
|
+
|
|
12
|
+
export const piHost = Object.freeze({
|
|
13
|
+
name: "pi",
|
|
14
|
+
label: "Pi",
|
|
15
|
+
command: "pi",
|
|
16
|
+
|
|
17
|
+
/** The installed Pi's version, as `pi --version` prints it. */
|
|
18
|
+
version: (environment = baseProcessEnvironment()) => installedHostVersion("pi", environment),
|
|
19
|
+
|
|
20
|
+
resolveExtensions: resolveHostExtensions,
|
|
21
|
+
|
|
22
|
+
/** Makes each provider's stored OAuth login valid for the target window, once, before a batch. */
|
|
23
|
+
async refreshLogins(providers) {
|
|
24
|
+
for (const provider of providers)
|
|
25
|
+
await ensureLogin(provider, { threshold: TARGET_MINUTES, target: TARGET_MINUTES });
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
effectiveSettings: effectivePiSettings,
|
|
29
|
+
prepareIsolation,
|
|
30
|
+
removeCredentials: removeCopiedCredentials,
|
|
31
|
+
|
|
32
|
+
/** Pi's arguments and its own environment switches for one subject. */
|
|
33
|
+
async prepareLaunch({
|
|
34
|
+
group,
|
|
35
|
+
variant,
|
|
36
|
+
environment,
|
|
37
|
+
runtime,
|
|
38
|
+
mode,
|
|
39
|
+
observeContext,
|
|
40
|
+
sessionDirectory,
|
|
41
|
+
model,
|
|
42
|
+
freeflowEntry,
|
|
43
|
+
hostExtensions,
|
|
44
|
+
}) {
|
|
45
|
+
return {
|
|
46
|
+
args: piArguments(
|
|
47
|
+
group,
|
|
48
|
+
environment,
|
|
49
|
+
runtime,
|
|
50
|
+
mode,
|
|
51
|
+
observeContext,
|
|
52
|
+
sessionDirectory,
|
|
53
|
+
model,
|
|
54
|
+
freeflowEntry,
|
|
55
|
+
hostExtensions,
|
|
56
|
+
group.variants[variant]?.shell?.container !== undefined,
|
|
57
|
+
),
|
|
58
|
+
environment: { PI_SKIP_VERSION_CHECK: "1", PI_TELEMETRY: "0" },
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/** One prompt in JSON mode; the prompt is the last argument. */
|
|
63
|
+
runOneShot: runPiProcess,
|
|
64
|
+
|
|
65
|
+
/** A persistent RPC session over the declared prompts, then any `nextPrompt` replies. */
|
|
66
|
+
runSession: runRpcBodySession,
|
|
67
|
+
|
|
68
|
+
modelCall: launchModel,
|
|
69
|
+
probeShell,
|
|
70
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { ensureLogin, piAgentDirectory, readJsonObject } from "./host.mjs";
|
|
5
|
+
|
|
6
|
+
// A copied OAuth login must outlast the process using it: a refresh inside the copy would rotate the token
|
|
7
|
+
// there and leave the user's own login holding a revoked one. So the login is refreshed centrally, in the
|
|
8
|
+
// user's agent directory, whenever it has less than SUBJECT_MINUTES left, to at least TARGET_MINUTES.
|
|
9
|
+
export const SUBJECT_MINUTES = 30;
|
|
10
|
+
export const TARGET_MINUTES = 120;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a private home, temp directory and Pi agent directory for one host process, and the environment
|
|
14
|
+
* that points the process at them. Only the declared provider's login is copied in, never another's, after
|
|
15
|
+
* a central refresh if it was close to expiring; the declared Pi settings become the agent directory's
|
|
16
|
+
* settings.json. The parent's HOME, TMPDIR, XDG directories and Pi settings are not visible.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} directory where the isolation root is created
|
|
19
|
+
* @param {string | null} model the declared `provider/model`, or null
|
|
20
|
+
* @param {{settings?: Record<string, unknown>}} [options]
|
|
21
|
+
* @returns {Promise<{environment: Record<string, string>, evidence: any, loginDirectory: string}>}
|
|
22
|
+
*/
|
|
23
|
+
export async function prepareIsolation(directory, model, { settings = {} } = {}) {
|
|
24
|
+
const root = path.join(directory, "isolation");
|
|
25
|
+
const home = path.join(root, "home");
|
|
26
|
+
const agent = path.join(root, "agent");
|
|
27
|
+
const tmp = path.join(root, "tmp");
|
|
28
|
+
for (const entry of [home, agent, tmp]) await mkdir(entry, { recursive: true });
|
|
29
|
+
if (Object.keys(settings).length > 0) {
|
|
30
|
+
await writeFile(path.join(agent, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const provider = typeof model === "string" && model.includes("/") ? model.split("/")[0] : null;
|
|
34
|
+
const source = piAgentDirectory();
|
|
35
|
+
const credentials = [];
|
|
36
|
+
if (provider !== null) {
|
|
37
|
+
// The caller reports a refresh failure as an invalid run, before any host process starts.
|
|
38
|
+
await ensureLogin(provider, { threshold: SUBJECT_MINUTES, target: TARGET_MINUTES });
|
|
39
|
+
const login = (await readJsonObject(path.join(source, "auth.json")))?.[provider];
|
|
40
|
+
if (login && typeof login === "object") {
|
|
41
|
+
await writeFile(path.join(agent, "auth.json"), `${JSON.stringify({ [provider]: login })}\n`, { mode: 0o600 });
|
|
42
|
+
credentials.push({ file: "auth.json", provider, type: typeof login.type === "string" ? login.type : null });
|
|
43
|
+
}
|
|
44
|
+
const models = await readJsonObject(path.join(source, "models.json"));
|
|
45
|
+
const custom = models?.providers?.[provider];
|
|
46
|
+
if (custom && typeof custom === "object") {
|
|
47
|
+
await writeFile(path.join(agent, "models.json"), `${JSON.stringify({ providers: { [provider]: custom } })}\n`, {
|
|
48
|
+
mode: 0o600,
|
|
49
|
+
});
|
|
50
|
+
credentials.push({ file: "models.json", provider, type: "provider" });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
environment: {
|
|
56
|
+
HOME: home,
|
|
57
|
+
TMPDIR: tmp,
|
|
58
|
+
TMP: tmp,
|
|
59
|
+
TEMP: tmp,
|
|
60
|
+
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
61
|
+
XDG_DATA_HOME: path.join(home, ".local", "share"),
|
|
62
|
+
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
63
|
+
PI_CODING_AGENT_DIR: agent,
|
|
64
|
+
},
|
|
65
|
+
evidence: { root, provider, credentials, settings },
|
|
66
|
+
// Where the host keeps its login: the confinement probe plants a marker here that the shell must not read.
|
|
67
|
+
loginDirectory: agent,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deletes the copied login and model entries once the process has exited, so no copy of a credential
|
|
73
|
+
* stays in the run's evidence. Safe to call when nothing was copied.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} directory the directory passed to prepareIsolation
|
|
76
|
+
*/
|
|
77
|
+
export async function removeCopiedCredentials(directory) {
|
|
78
|
+
for (const file of ["auth.json", "models.json"]) {
|
|
79
|
+
await rm(path.join(directory, "isolation", "agent", file), { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// One model call through Pi that is not a subject: a judge, a reviewer, or a simulated user. It runs without tools,
|
|
2
|
+
// skills, context files or session, in its own isolated home and agent directory, with the provider's login copied in
|
|
3
|
+
// and removed afterward.
|
|
4
|
+
|
|
5
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
import { writeText } from "../../evidence.mjs";
|
|
9
|
+
import { baseProcessEnvironment } from "../../process.mjs";
|
|
10
|
+
import { effectivePiSettings } from "./host.mjs";
|
|
11
|
+
import { prepareIsolation, removeCopiedCredentials } from "./isolation.mjs";
|
|
12
|
+
import { runPiProcess } from "./process.mjs";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Starts one model call on a prompt in its own directory, which keeps the prompt, events, final response and stderr.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} directory replaced on every call
|
|
18
|
+
* @param {string} prompt
|
|
19
|
+
* @param {{model: string, thinking: string | null, extensions: Array<{path: string}>, settings: object, signal?: AbortSignal, role?: string}} options
|
|
20
|
+
* @returns {Promise<{response: string | null, errors: string[], usage: any, isolation: any}>}
|
|
21
|
+
*/
|
|
22
|
+
export async function launchModel(directory, prompt, options) {
|
|
23
|
+
const { model, thinking, extensions, settings, signal, role = "reviewer" } = options;
|
|
24
|
+
await rm(directory, { recursive: true, force: true });
|
|
25
|
+
await mkdir(directory, { recursive: true });
|
|
26
|
+
await writeText(path.join(directory, "prompt.md"), prompt);
|
|
27
|
+
|
|
28
|
+
const args = [
|
|
29
|
+
"--mode",
|
|
30
|
+
"json",
|
|
31
|
+
"-p",
|
|
32
|
+
"--no-session",
|
|
33
|
+
"--no-extensions",
|
|
34
|
+
...extensions.flatMap((extension) => ["--extension", extension.path]),
|
|
35
|
+
"--no-skills",
|
|
36
|
+
"--no-prompt-templates",
|
|
37
|
+
"--no-themes",
|
|
38
|
+
"--no-context-files",
|
|
39
|
+
"--no-approve",
|
|
40
|
+
"--no-tools",
|
|
41
|
+
"--model",
|
|
42
|
+
model,
|
|
43
|
+
];
|
|
44
|
+
if (thinking !== null) args.push("--thinking", thinking);
|
|
45
|
+
args.push(prompt);
|
|
46
|
+
let isolation;
|
|
47
|
+
try {
|
|
48
|
+
// The evaluator's Pi defaults (retry and compaction off) apply here as for subjects; declared settings win.
|
|
49
|
+
isolation = await prepareIsolation(directory, model, { settings: effectivePiSettings(settings) });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
return { response: null, errors: [message], usage: null, isolation: null };
|
|
53
|
+
}
|
|
54
|
+
const observation = await runPiProcess({
|
|
55
|
+
command: "pi",
|
|
56
|
+
args,
|
|
57
|
+
cwd: directory,
|
|
58
|
+
eventsFile: path.join(directory, "events.jsonl"),
|
|
59
|
+
stderrFile: path.join(directory, "stderr.log"),
|
|
60
|
+
signal,
|
|
61
|
+
environment: {
|
|
62
|
+
...baseProcessEnvironment(),
|
|
63
|
+
PWD: directory,
|
|
64
|
+
PI_SKIP_VERSION_CHECK: "1",
|
|
65
|
+
PI_TELEMETRY: "0",
|
|
66
|
+
...isolation.environment,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await removeCopiedCredentials(directory);
|
|
70
|
+
await writeText(path.join(directory, "final.md"), `${observation.response}\n`);
|
|
71
|
+
|
|
72
|
+
const errors = [];
|
|
73
|
+
if (observation.exitCode !== 0 || !observation.settled || observation.assistantError !== null) {
|
|
74
|
+
errors.push(
|
|
75
|
+
`${role} did not complete: exit=${observation.exitCode} settled=${observation.settled} error=${observation.assistantError}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return { response: observation.response, errors, usage: observation.usage, isolation: isolation.evidence };
|
|
79
|
+
}
|