mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 19:35:04 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Its adapters supply task context and access to Paperclip skills and tools. > - The default hire manual and shared prompts also repeat general work procedures. > - Those procedures overlap with stock provider instructions and the Paperclip skill. > - Existing E2E fixtures supply a QA manual, so they do not qualify the production default. > - This pull request reduces the generic instructions and adds real default-hire coverage. > - The benefit is less competing guidance, with inspectable evidence for preserved skills and task context. ## Linked Issues or Issue Description Refs: #14920. That merged change preserves native Codex base instructions. This PR covers the default manual, shared legacy prompts, operational skill guidance, and the narrowly approved ACP skill-discovery/session-environment repair for measured delivery and credential-persistence failures. **What existing behavior does this improve?** New non-CEO hires without a custom bundle and legacy task/chat startup and continuation prompts. **Current behavior** The shipped default manual contains 602 words. Generic task/chat prompts and ordinary resume deltas repeat work procedures already available through the harness and Paperclip skill. **Proposed behavior** The default manual contains only the eight-word company identity. Shared startup prompts retain identity and connection guidance. Ordinary resume deltas retain current work context without the generic execution contract. **Reason and benefit** Let the stock harness guide general work. Keep Paperclip-specific capabilities and independently test default hires, skills, ordered comments, and chat restart. **Breaking changes** New default hires receive less guidance. Existing saved manuals, explicit custom bundles, CEO templates, and specialized wake contracts retain their behavior. The obsolete includeExecutionContract option remains accepted for source compatibility. ## What Changed - Reduce the default hire manual to one sentence. - Reduce shared task/chat defaults and remove the generic ordinary-resume contract. - Keep connection guidance, auth, skills, custom prompts, and specialized wake context. - Add credential-free instruction-boundary gates and 26 explicit Product E2E cells across eight legacy/native profiles, including two focused Paperclip-storage cases. - Capture public hire receipts before providers run, then grade delivered prompts and independent task/chat outcomes. - Add an early legacy skill API recipe for saving a task document, checking the saved revision receipt and linking the document. Improve stock task/heartbeat skill-selection metadata and show a clickable Markdown UI-link example. Keep native tool completion separate. - Advertise bounded routing descriptions and exact successfully staged SKILL.md paths in legacy ACP Claude; keep full bodies on demand and preserve remote path rebasing. - Remove only the provider environment from copied persisted ACP session records, while loading current run credentials and preserving all other options/conversation state. - Regenerate both capability metadata inventories and reject stale manifests/inventories before provider admission. - Publish the original reduction and focused skill-repair comparisons, preserving all failures, automatic recovery, cost coverage and limitations. ## Verification **Behavioral qualification remains pending.** Original legacy ACP Claude loses the issue document only in the reduced cohort beneath an unchanged credential failure. A source-backed diagnosis finds that neither ordinary assignment reads the staged operational skill, while the runtime persists provider environment in session state. The new common repairs expose skill metadata/path and omit persisted env; strict document and credential guards stay intact. [Inspectable diagnosis and retained hashes](https://github.com/paperclipai/paperclip/blob/9f654db4541d3d002769c988f6e51fc0b08dadbd/doc/plans/2026-10-03-legacy-acp-claude-readiness.md). Current repair head `de0965984ff3edf611ae6d0e7ca5c7d5ae3947bb` incorporates master `569c7203aa24b95440682983ce7940ba1d4247bd` (merged #14961/#15007). All 222 affected adapter tests, adapter-utils/E2E typechecks, and final 96 variant/grader/retry calibrations pass. The frozen historical comparator is `c25697f4260b6f3adfea143c3ae9932e2f42986d`: 8,280 of 8,291 paths identical, exactly two production instruction paths plus nine declared unit expectations differ. The operational skill/discovery/environment repairs, selected model/profile/task/core grader/auth/permissions/retry policy are identical. Both actual launcher prepare→verify admissions pass with zero providers. [Immutable manifest and exact receipts](https://github.com/paperclipai/paperclip/blob/9f654db4541d3d002769c988f6e51fc0b08dadbd/doc/plans/2026-10-03-legacy-acp-claude-evidence/manifest.json). One original legacy ACP Claude cell per variant is authorized, with enforced single campaign attempts, 12-minute deadlines and company/agent 1,000-cent hard stops; every product recovery run/cost is counted. Actual live outcomes are pending. Current normal CI has one failed server shard and failed aggregate verify under diagnosis; other normal gates including typecheck/build/Rust/all eight browser shards pass. Fresh review completed successfully; the valid historical startup/resume masking finding was fixed with per-invocation task/chat checks and strict complete-snapshot capture, calibrated and resolved. Prior heads, failures and campaigns below remain historical evidence, not checks on this repair head. - Prior head `36aa4d81c49a1a8f6f04b1a068fae19aa901955f` is replayed on merged hiring master `862a5758ba0e88a33232c1f1fa645e85c38a3113`. All 52 current-head checks pass with two intentional Storybook skips, including repository typecheck/test/build and the browser shard. Fresh Greptile is 5/5 with zero unresolved review threads. Exact-head stock prerequisites pass 599 assertions (598 TypeScript + 1 Rust), all six gates and retained receipt verification, zero providers/source errors. Fingerprint `a7f5a22d860a88fe20cce213c6d5e0004788f32c8363930729aea4fd740ad16d`. Combined catalog/hiring calibrations pass 67 assertions, E2E typecheck and 26-cell stock discovery pass. Canonical contract/inventory checks and the later issue-derived reference calibration are retained; that reference-only follow-up is not live-qualified by earlier frozen runs. - Prior full repository typecheck/build passed. The complete local Vitest run executed 14,956 tests: 14,870 passed, 83 skipped, three timing failures. All three affected files passed unchanged narrow reruns; original failures remain retained. Current-head CI now passes the full general checks; the original local failures remain retained. - The original 24-pair default-manual/shared-prompt comparison has two new overall classic Claude/OpenCode document-delivery failures plus an additional legacy ACP Claude document loss beneath an unchanged credential-guard failure (not closed by later runs), two newly passing OpenCode ordered cases, seven unchanged failures and 13 unchanged passes. Equal 15/24 totals do not establish behavioral equivalence. [Complete original report](https://github.com/paperclipai/paperclip/blob/875f4c397d9e8c3f12f39dedd59abaf1eaf5236e/doc/plans/2026-10-02-stock-harness-live-comparison.md). - The skill-only repair holds the eight-word manual/shared prompts and merged #14920 fixed. All four matched profile configurations and 203 fixture/behavior files match. Candidate `abd0b628ca642c09a54a4edc56a5227402f6686e` varies only the two skill sources against baseline `bc83fe030234439ac51279502a28803958963e2e`. [Candidate workflow](https://github.com/paperclipai/paperclip/actions/runs/37060885547) and [baseline workflow](https://github.com/paperclipai/paperclip/actions/runs/37060888047) each pass 571 exact-source prerequisites before providers; all eight cells clean up successfully. Failed campaigns publish successfully and remain failed. - Repair pairs: Claude original Fail → Pass; Claude explicit Pass → Pass; both OpenCode cases Fail → Fail. Explicit OpenCode's handoff worsens beneath the unchanged failing UI-link grade: baseline gives a clickable API URL, candidate gives a code-formatted path without an anchor. The request's usable-link wording is narrower in the UI-only oracle. [Complete repair report and safe projection](https://github.com/paperclipai/paperclip/blob/875f4c397d9e8c3f12f39dedd59abaf1eaf5236e/doc/plans/2026-10-02-legacy-document-skill-repair.md). - The subsequent narrow stock metadata/link correction has two matched Pass → Pass cases, zero new machine failures/passes and no pending pairs. Both original-case handoff links remain deficient: candidate uses a wrong PAP prefix, baseline supplies a bare prefix-less slug path; the preserved original oracle only requires a durable document. Both explicit clickable UI-link cases pass revision/content/link grading. All four exact-source 587-check gates, single assignment runs and cleanup pass. This does not establish fix causality because baseline also succeeds. [Candidate workflow](https://github.com/paperclipai/paperclip/actions/runs/37069547401) freezes `fe9dc1e3c518825242ed889ab9c8352986f8c2ed`; [matched baseline](https://github.com/paperclipai/paperclip/actions/runs/37069552374) freezes `0d7ecfa96d72fba79b7f0a25052b42c0686c0488`. This is a skill-only comparison with reduced manuals/shared prompts held constant, not a repeat of the historical-manual comparison. Only original and clarified explicit classic OpenCode cases are selected, two per variant/four expected turns. 8,242 other tracked files and both profile hashes match; protected workflows admit each exact source before credentials. [Complete qualification report](https://github.com/paperclipai/paperclip/blob/74d0d3d945f4c52d0814b5a845ab5bd09f33cd6b/doc/plans/2026-10-02-opencode-skill-routing-link-qualification.md). Candidate original loads Paperclip/reference before saving publicly; baseline original loads it after writing locally, then saves publicly within the same assignment. Reported cost totals are $0.0107824490 candidate / $0.0107909015 baseline, with unmetered runtime. The later reference-only issue-derived link correction is provider-free calibrated and **not live-qualified** by these frozen runs; no further paid runs. - Retained tool calls show the repaired original OpenCode assignment loads only its assigned output skill before writing locally. Operational Paperclip is first loaded during automatic disposition recovery; its early recipe is visible then, but it never saves the missing document. Explicit candidate loads Paperclip and reads the new reference before saving successfully. All nine actual runs are counted. Reported LLM totals are $0.3802537209 baseline and $0.4918990161 candidate; local runtime is unmetered. - Initial setup, packaging, cancelled/missing-cell recovery, callback test and relative-output attempts remain retained. No completed provider failure was rerun. Frozen measurement branches are unchanged by later canonical metadata maintenance. - Run `pnpm test:e2e:runner:stock-harness`, `pnpm test:e2e:runner:unit`, and `pnpm test:e2e:runner:typecheck`. Select `stock-harness` explicitly for paid execution; it is excluded from `--all`. Prior-head integration: `36aa4d81c49a1a8f6f04b1a068fae19aa901955f` replays this PR on merged hiring #14985 (`862a5758ba0e88a33232c1f1fa645e85c38a3113`), preserving the four explicit custom-CEO-bundle checks, minimal generic manual boundary, and both suites. The combined fixture catalog and hiring calibrations pass 67 assertions; exact-head stock prerequisites pass 599 assertions (598 TypeScript + 1 Rust), all six gates and retained-receipt verification, zero providers/source errors, fingerprint `a7f5a22d860a88fe20cce213c6d5e0004788f32c8363930729aea4fd740ad16d`. E2E typecheck and 26-cell stock discovery pass. Fresh current-head CI passes all 52 checks with two intentional skips, and fresh Greptile is 5/5 with zero unresolved review threads. The prior source-plan browser failure is retained: a deterministic process fixture replayed its last `fixture:plan` command on `chat_task_completed`, writing revision 2 with identical body after the approval handoff. This was not paid provider execution. Rebased current-head CI passes the same assertion without an old-head retry or a change to that browser fixture. The merged hiring change was measured separately on immutable matched unions, with this reduced/shared/operational context and native completion guidance held constant. [Complete original two-profile report](https://github.com/paperclipai/paperclip/blob/f0512647656be78e48abd8c22a3078db8bf6bcd2/doc/plans/2026-10-02-hiring-template-live-comparison.md): [candidate](https://github.com/paperclipai/paperclip/actions/runs/37075466208) / [historical baseline](https://github.com/paperclipai/paperclip/actions/runs/37075469463), 705 provider-free prerequisites each. Both pairs are unchanged Fail → Fail on the exact-five count, with six core delivery checks passing all four cells; 28 actual successful runs include eight automatic completion wakes, zero retries, four successful cleanups. Source-read coverage is uncomparable, actual model charges unknown. Separately versioned provider-free accounting remains analytical work; original verdicts are preserved. This does not rerun or qualify the completed default-manual or native campaigns. ## Risks - Legacy ACP Claude's additional delivery loss is not closed by any later matched run and blocks the no-extra-failing-behavior merge criterion. Legacy document delivery may have relied on the prior manual/shared prompts. The early skill repair improves Claude in one trial; the later OpenCode pairs pass in both variants and cannot establish causality or robust recovery. Both original-case links remain deficient beneath the storage-only grade. The later issue-derived reference correction has only provider-free validation. Native finish/block descriptions must not be supplied to legacy agents. - The comparison holds merged native Codex fix #14920 constant; it cannot measure that fix's before/after task performance. - These bounded skill/context/chat workflows do not measure general coding quality. Unrepresented providers remain unqualified. - Saved manuals and old Codex sessions are not automatically migrated. Codex through ACP still has a separate base-instruction follow-up. ## Model Used OpenAI Codex, GPT-6 family as identified by this session. The exact deployment ID and context-window size are not exposed. The assistant used reasoning, repository tools, code execution, and delegated PR/eval work. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes: #` / `Refs: #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass (relevant suites and all three unchanged narrow reruns pass; complete-run timing failures retained in Verification) - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green on the new repair head (prior-head checks retained above) - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups on the new repair head - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
247 lines
18 KiB
JavaScript
247 lines
18 KiB
JavaScript
// Deterministic prerequisite for the stock-harness Product E2E suite. No model
|
|
// calls or credentials. Reuse the existing protocol assertions, not model
|
|
// self-reports about what its system instructions contain.
|
|
import { spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { readStockInstructionVariant } from "./stock-harness-instruction-variant.mjs";
|
|
|
|
const root = resolve(import.meta.dirname, "../..");
|
|
export const stockHarnessGates = [
|
|
{ id: "SH-1", name: "Native Codex additive instructions", cwd: "packages/paperclip-runner", files: [
|
|
"src/drivers/codex/codex-app-server-driver.test.ts",
|
|
"src/drivers/codex/codex-app-server-driver.lifecycle.test.ts",
|
|
"src/live/runnerd-codex-transport.test.ts",
|
|
"src/live/live-session.test.ts",
|
|
"src/cli/eval-provider-runtime.test.ts",
|
|
], testPattern: "adds Paperclip developer instructions|preserves stock Codex instructions|captures exact provider frames and correlates|direct eval provider runtime|exposes native completion consistently on fresh and resumed sessions|passes caller-supplied native system instructions",
|
|
required: ["adds Paperclip developer instructions", "preserves stock Codex instructions on task recovery",
|
|
"preserves stock Codex instructions on prepared recovery", "preserves stock Codex instructions on direct recovery",
|
|
"captures exact provider frames and correlates", "exposes native completion consistently on fresh and resumed sessions",
|
|
"passes caller-supplied native system instructions"] },
|
|
{ id: "SH-2", name: "Production-default hire bundle", cwd: ".", files: [
|
|
"server/src/__tests__/agent-skills-routes.test.ts",
|
|
"server/src/services/onboarding-first-task-assets.test.ts",
|
|
], required: ["materializes minimal default instructions for non-CEO agents with no prompt template"] },
|
|
{ id: "SH-3", name: "Shared legacy startup and continuation", cwd: ".", files: [
|
|
"packages/adapter-utils/src/server-utils.test.ts",
|
|
"packages/adapter-utils/src/prompt-sections.test.ts",
|
|
"packages/adapter-utils/src/acpx-engine/execute.test.ts",
|
|
"packages/adapter-utils/src/acpx-engine/ephemeral-session-environment.test.ts",
|
|
"packages/adapters/pi-local/src/server/execute.remote.test.ts",
|
|
"packages/adapters/opencode-local/src/server/execute.test.ts",
|
|
"packages/adapters/cursor-cloud/src/server/execute.test.ts",
|
|
"server/src/__tests__/codex-local-execute.test.ts",
|
|
], required: ["keeps task and chat defaults to identity and connection guidance",
|
|
"does not restore generic procedures on resume or with the legacy opt-in",
|
|
"integrates the env-free store", "advertises folded routing metadata", "bounds routing descriptions",
|
|
"loads an old env-bearing file with rotated credentials", "drops a skill that fails to materialize"] },
|
|
// Hermes is not in the root Vitest project list. Run its package config so
|
|
// the requested file cannot silently disappear from discovery.
|
|
{ id: "SH-3-hermes", name: "Hermes shared-prompt delivery", cwd: "packages/adapters/hermes",
|
|
files: ["src/server/prompt-rendering.test.ts"],
|
|
required: ["renders standard assignment wake with task authority", "renders scoped planning wake authority"] },
|
|
{ id: "SH-eval", name: "Independent oracle and qualification admission", cwd: ".",
|
|
config: "tests/runner-e2e/vitest.config.ts",
|
|
files: ["tests/runner-e2e/stock-harness-manifest.test.ts", "tests/runner-e2e/paperclip-document.test.ts", "tests/runner-e2e/stock-harness.test.ts", "tests/runner-e2e/stock-harness-checks.test.mjs",
|
|
"tests/runner-e2e/stock-harness-admission.test.ts", "tests/runner-e2e/stock-harness-digest.test.ts",
|
|
"tests/runner-e2e/select-rerun-artifacts.test.ts", "tests/runner-e2e/stock-harness-instruction-variant.test.mjs", "tests/runner-e2e/automatic-retry.test.ts"],
|
|
required: ["requires generated capability manifests for the current skill sources", "the shipped recipe delivers the current", "the shipped recipe supports an unnumbered issue", "rejects old SHA before providers", "allows toolchain paths and excludes every present or future credential",
|
|
"changes when the evaluated server/src/onboarding-assets/default/AGENTS.md changes",
|
|
"changes when the evaluated packages/adapter-utils/src/server-utils.ts changes",
|
|
"changes when the evaluated packages/shared/src/connection-intent-guidance.ts changes",
|
|
"retains credential-free prerequisites inside the exact campaign root"] },
|
|
];
|
|
|
|
export function stockHarnessGatesForVariant(variant) {
|
|
if (variant !== "reduced" && variant !== "historical") throw new Error("Unknown stock instruction variant.");
|
|
return stockHarnessGates.map(gate => variant !== "historical" ? gate : {
|
|
...gate,
|
|
required: gate.required.map(name => name === "materializes minimal default instructions for non-CEO agents with no prompt template"
|
|
? "materializes the bundled default instruction set for non-CEO agents with no prompt template"
|
|
: name === "keeps task and chat defaults to identity and connection guidance"
|
|
? "keeps the default local-agent prompt action-oriented"
|
|
: name === "does not restore generic procedures on resume or with the legacy opt-in"
|
|
? "adds the execution contract to resume delta prompts and opted-in fresh prompts" : name),
|
|
});
|
|
}
|
|
|
|
export function gradeGate(gate, report, exitCode) {
|
|
const assertions = (report?.testResults ?? []).flatMap(file => file.assertionResults ?? []);
|
|
const requirements = (gate.required ?? []).map(name => ({ name,
|
|
passed: assertions.some(assertion => assertion.fullName?.includes(name) && assertion.status === "passed") }));
|
|
const files = gate.files.map(file => ({ file,
|
|
passed: (report?.testResults ?? []).some(result => result.name?.endsWith(file) && result.status === "passed" &&
|
|
result.assertionResults?.some(assertion => assertion.status === "passed") &&
|
|
result.assertionResults.every(assertion => assertion.status === "passed" ||
|
|
(gate.testPattern && assertion.status === "skipped" && !(gate.required ?? []).some(name => assertion.fullName?.includes(name))))) }));
|
|
return { id: gate.id, name: gate.name, passed: exitCode === 0 && requirements.every(row => row.passed) && files.every(row => row.passed),
|
|
exitCode, files, requirements, total: report?.numTotalTests ?? 0, passedTests: report?.numPassedTests ?? 0,
|
|
failedTests: report?.numFailedTests ?? 0, pendingTests: report?.numPendingTests ?? 0 };
|
|
}
|
|
|
|
export function sourceFingerprint() {
|
|
const hash = createHash("sha256");
|
|
const sources = new Set([
|
|
...stockHarnessGates.flatMap(gate => gate.files.map(file => join(gate.cwd, file))),
|
|
"tests/runner-e2e/stock-harness.ts", "tests/runner-e2e/stock-harness-checks.mjs", "tests/runner-e2e/catalog.ts",
|
|
"tests/runner-e2e/stock-harness-admission.ts", "tests/runner-e2e/launch.ts", "tests/runner-e2e/runner.spec.ts",
|
|
"tests/runner-e2e/stock-harness-instruction-variant.mjs", "tests/runner-e2e/stock-harness-instruction-variant.d.mts", "tests/runner-e2e/fixtures/stock-harness/historical-default-agents.md",
|
|
"tests/runner-e2e/automatic-retry.ts", "tests/runner-e2e/types.ts",
|
|
"packages/adapter-utils/src/acpx-engine/execute.ts", "packages/adapter-utils/src/acpx-engine/ephemeral-session-environment.ts",
|
|
"tests/runner-e2e/context-integrity-cases.ts", "tests/runner-e2e/context-integrity-flow.ts", "tests/runner-e2e/context-integrity-scoring.ts",
|
|
"tests/runner-e2e/stock-harness-manifest.ts", "packages/paperclip-runner/scripts/generate-capability-contract.mjs",
|
|
"packages/paperclip-runner/spec/capability/source-contract.json",
|
|
"packages/paperclip-runner/scripts/check-capability-inventory.mjs", "packages/paperclip-runner/scripts/lib/capability-inventory.mjs",
|
|
"packages/paperclip-runner/spec/capability/capabilities.yaml", "packages/paperclip-runner/spec/capability/eval-traceability.yaml",
|
|
"packages/paperclip-runner/spec/capability/mcp-tool-map.yaml", "packages/paperclip-runner/spec/capability/inventory.schema.json",
|
|
"packages/paperclip-runner/src/generated/capability-contract.ts", "packages/paperclip-runner/docs/capability-contract.md",
|
|
...["capabilities.yaml", "mcp-tool-map.yaml", "eval-traceability.yaml", "capability-contract.md", "downstream-handoff.md"]
|
|
.map(file => `packages/paperclip-runner/generated/capability/${file}`),
|
|
"packages/adapter-utils/src/server-utils.ts", "packages/shared/src/connection-intent-guidance.ts",
|
|
"server/src/onboarding-assets/default/AGENTS.md", "server/src/routes/agents.ts", "scripts/ensure-plugin-build-deps.mjs",
|
|
"packages/paperclip-runner/src/drivers/codex/codex-app-server-driver-impl.ts",
|
|
"packages/paperclip-runner/src/live/runnerd-codex-transport.ts",
|
|
"packages/paperclip-runner/src/live/live-session.ts",
|
|
"packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs",
|
|
"packages/paperclip-runner/runner/crates/runner-core/src/bin/fake-codex-app-server.rs",
|
|
"packages/paperclip-runner/runner/Cargo.toml", "packages/paperclip-runner/runner/Cargo.lock",
|
|
"packages/paperclip-runner/runner/crates/runner-core/Cargo.toml",
|
|
"packages/paperclip-runner/runner/crates/runner-core/tests/codex_provider.rs",
|
|
]);
|
|
const sourceErrors = [];
|
|
sources.add("skills/paperclip/SKILL.md");
|
|
sources.add("skills/paperclip/references/issue-documents.md");
|
|
for (const source of [...sources].sort()) {
|
|
hash.update(source);
|
|
try { hash.update("present\0").update(readFileSync(join(root, source))); }
|
|
catch (error) {
|
|
if (source === "skills/paperclip/references/issue-documents.md" && error.code === "ENOENT") hash.update("absent\0");
|
|
else sourceErrors.push(source);
|
|
}
|
|
}
|
|
return { fingerprint: hash.digest("hex"), sourceErrors };
|
|
}
|
|
|
|
export function assertPreflightReceipt(report, current) {
|
|
const instructionVariant = readStockInstructionVariant();
|
|
const expected = [...stockHarnessGates.map(gate => gate.id), "SH-1-rust"];
|
|
if (report?.schema !== "paperclip.stock-harness-preflight.v3" || report.passed !== true ||
|
|
report.setup?.passed !== true || report.setup?.exitCode !== 0 ||
|
|
report.setup?.sdkExitCode !== 0 || report.setup?.runnerdExitCode !== 0 ||
|
|
report.setup?.runnerdSha256 !== current.runnerdSha256 ||
|
|
report.setup?.fakeCodexSha256 !== current.fakeCodexSha256 ||
|
|
report.providerCalls !== 0 || report.sourceSha !== current.sha ||
|
|
report.sourceFingerprint !== current.fingerprint || report.sourceErrors?.length !== 0 ||
|
|
report.instructionVariant?.variant !== instructionVariant.variant || report.instructionVariant?.sha256 !== instructionVariant.sha256 ||
|
|
!Array.isArray(report.gates) || report.gates.length !== expected.length ||
|
|
expected.some(id => report.gates.filter(gate => gate.id === id && gate.passed === true && gate.exitCode === 0).length !== 1)) {
|
|
throw new Error("Stock harness requires passing prerequisites for this exact source SHA and fingerprint.");
|
|
}
|
|
return report;
|
|
}
|
|
|
|
export function main(args = process.argv.slice(2)) {
|
|
const { variant, sha256 } = readStockInstructionVariant();
|
|
const instructionVariant = { variant, sha256 };
|
|
const gates = stockHarnessGatesForVariant(variant);
|
|
if (args.includes("--list")) {
|
|
console.log(JSON.stringify({ gates, instructionVariant, rust: "runtime_instructions_are_additive_for_codex_on_start_and_resume", live: "not invoked" }, null, 2));
|
|
return;
|
|
}
|
|
if (args.some(arg => !arg.startsWith("--output-dir=") && !arg.startsWith("--verify=") && arg !== "--allow-rust-network"))
|
|
throw new Error("Use --list, --output-dir=<path>, --verify=<receipt>, or --allow-rust-network; never paid providers.");
|
|
const git = spawnSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" });
|
|
const verify = args.find(arg => arg.startsWith("--verify="))?.slice("--verify=".length);
|
|
if (verify) {
|
|
const source = sourceFingerprint();
|
|
if (git.status !== 0 || source.sourceErrors.length) throw new Error("Cannot verify stock harness source provenance.");
|
|
const report = assertPreflightReceipt(JSON.parse(readFileSync(verify, "utf8")), {
|
|
sha: git.stdout.trim(), fingerprint: source.fingerprint,
|
|
runnerdSha256: createHash("sha256").update(readFileSync(join(root,
|
|
"packages/paperclip-runner/runner/target/debug", `paperclip-runnerd${process.platform === "win32" ? ".exe" : ""}`))).digest("hex"),
|
|
fakeCodexSha256: createHash("sha256").update(readFileSync(join(root,
|
|
"packages/paperclip-runner/runner/target/debug/fake-codex-app-server"))).digest("hex"),
|
|
});
|
|
const output = resolve(verify, "..");
|
|
for (const gate of gates) {
|
|
const grade = gradeGate(gate, JSON.parse(readFileSync(join(output, `${gate.id}.json`), "utf8")), 0);
|
|
if (!grade.passed) throw new Error(`Missing or failed retained prerequisite assertions: ${gate.id}`);
|
|
}
|
|
if (!/test runtime_instructions_are_additive_for_codex_on_start_and_resume \.\.\. ok/.test(readFileSync(join(output, "rust.txt"), "utf8")))
|
|
throw new Error("Missing passing retained Rust prerequisite.");
|
|
console.log(JSON.stringify(report));
|
|
return report;
|
|
}
|
|
const output = args.find(arg => arg.startsWith("--output-dir="))?.slice("--output-dir=".length) ??
|
|
join(root, "tests/runner-e2e/results", `stock-harness-preflight-${new Date().toISOString().replaceAll(":", "-")}`);
|
|
mkdirSync(output, { recursive: true });
|
|
const env = Object.fromEntries([
|
|
"PATH", "HOME", "TMPDIR", "TMP", "TEMP", "SYSTEMROOT", "LANG", "LC_ALL",
|
|
"CARGO_HOME", "RUSTUP_HOME", "CI", "GITHUB_ACTIONS",
|
|
].flatMap(name => process.env[name] === undefined ? [] : [[name, process.env[name]]]));
|
|
// A protected cold install disables lifecycle scripts. Use the same ordinary
|
|
// SDK dependency builder as server startup, before importing server tests.
|
|
const setupRun = spawnSync(process.execPath, [join(root, "scripts/ensure-plugin-build-deps.mjs")], {
|
|
cwd: root, env, encoding: "utf8", timeout: 5 * 60_000,
|
|
});
|
|
writeFileSync(join(output, "setup.txt"), `${setupRun.stdout ?? ""}\n${setupRun.stderr ?? ""}`);
|
|
// The exact daemon-frame test uses the real local Rust daemon. Legacy cold
|
|
// cells do not download native artifacts, so compile it before TS discovery.
|
|
const runnerd = setupRun.status === 0 ? spawnSync("cargo", ["build", "--locked",
|
|
...(args.includes("--allow-rust-network") ? [] : ["--offline"]),
|
|
"--workspace", "--bins"], {
|
|
cwd: join(root, "packages/paperclip-runner/runner"), env, encoding: "utf8", timeout: 10 * 60_000,
|
|
}) : null;
|
|
writeFileSync(join(output, "runnerd-build.txt"), `${runnerd?.stdout ?? ""}\n${runnerd?.stderr ?? ""}`);
|
|
const setup = { passed: setupRun.status === 0 && runnerd?.status === 0,
|
|
exitCode: setupRun.status !== 0 ? setupRun.status : runnerd?.status ?? null,
|
|
sdkExitCode: setupRun.status, runnerdExitCode: runnerd?.status ?? null };
|
|
if (!setup.passed) {
|
|
const source = sourceFingerprint();
|
|
writeFileSync(join(output, "preflight.json"), JSON.stringify({
|
|
schema: "paperclip.stock-harness-preflight.v3", instructionVariant, sourceSha: git.stdout?.trim() || null,
|
|
sourceFingerprint: source.fingerprint, measuredAt: new Date().toISOString(), providerCalls: 0,
|
|
live: "not_run", passed: false, sourceErrors: source.sourceErrors, setup, gates: [],
|
|
}, null, 2) + "\n");
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
const runnerdBinary = join(root, "packages/paperclip-runner/runner/target/debug",
|
|
`paperclip-runnerd${process.platform === "win32" ? ".exe" : ""}`);
|
|
setup.runnerdSha256 = createHash("sha256").update(readFileSync(runnerdBinary)).digest("hex");
|
|
setup.fakeCodexSha256 = createHash("sha256").update(readFileSync(join(root,
|
|
"packages/paperclip-runner/runner/target/debug/fake-codex-app-server"))).digest("hex");
|
|
const results = [];
|
|
for (const gate of gates) {
|
|
console.log(`Checking ${gate.id}: ${gate.name}`);
|
|
const file = join(output, `${gate.id}.json`);
|
|
const run = spawnSync(process.execPath, [join(root, "node_modules/vitest/vitest.mjs"), "run", ...gate.files,
|
|
...(gate.config ? ["--config", gate.config] : []),
|
|
...(gate.testPattern ? ["--testNamePattern", gate.testPattern] : []),
|
|
"--reporter=default", "--reporter=json", `--outputFile.json=${file}`],
|
|
{ cwd: resolve(root, gate.cwd),
|
|
env: gate.id === "SH-1" ? { ...env, PAPERCLIP_STOCK_PREFLIGHT_RUNNERD: runnerdBinary } : env,
|
|
stdio: "inherit", timeout: 10 * 60_000 });
|
|
let report;
|
|
try { report = JSON.parse(readFileSync(file, "utf8")); } catch { /* missing evidence fails closed below */ }
|
|
results.push(gradeGate(gate, report, run.status));
|
|
}
|
|
const rust = spawnSync("cargo", ["test", ...(args.includes("--allow-rust-network") ? [] : ["--offline"]), "-p", "paperclip-runner-core", "--test", "codex_provider",
|
|
"runtime_instructions_are_additive_for_codex_on_start_and_resume", "--", "--exact"],
|
|
{ cwd: join(root, "packages/paperclip-runner/runner"), env, encoding: "utf8", timeout: 10 * 60_000 });
|
|
const rustOutput = `${rust.stdout ?? ""}\n${rust.stderr ?? ""}`;
|
|
writeFileSync(join(output, "rust.txt"), rustOutput);
|
|
results.push({ id: "SH-1-rust", passed: rust.status === 0 && /test runtime_instructions_are_additive_for_codex_on_start_and_resume \.\.\. ok/.test(rustOutput), exitCode: rust.status });
|
|
const { fingerprint, sourceErrors } = sourceFingerprint();
|
|
const report = { schema: "paperclip.stock-harness-preflight.v3", instructionVariant, sourceSha: git.stdout?.trim() || null,
|
|
sourceFingerprint: fingerprint, measuredAt: new Date().toISOString(), providerCalls: 0,
|
|
live: "not_run", passed: git.status === 0 && sourceErrors.length === 0 && results.every(row => row.passed), sourceErrors, setup, gates: results };
|
|
writeFileSync(join(output, "preflight.json"), JSON.stringify(report, null, 2) + "\n");
|
|
console.log(`Stock harness prerequisite evidence: ${output}/preflight.json`);
|
|
if (!report.passed) process.exitCode = 1;
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) main();
|