mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
## Thinking Path > - Paperclip manages agents and the work they perform. > - Product E2E checks verify real user paths through those agents. > - A recorded GitHub review-bot walkthrough had no repeatable registered case. > - App setup, automatic triggering, publication, and task completion need separate evidence. > - This pull request adds an explicit attended journey to the existing catalog and report tools. > - Reusable setup profiles and independent assertions preserve the same outcome across compatible targets. ## Linked Issues or Issue Description Refs: #17262. This case remains an explicit attended attached-target flow. It does not replace the shared deployment-target or paid-operations framework. **What existing behavior does this improve?** The Product E2E catalog can now select and retain evidence for GitHub review-bot setup and its first automatic review. **Current behavior** Recorded walkthroughs cannot be rerun as a fixed registered case. A published review alone can hide a failed run or unfinished task. **Proposed behavior** Create and install an owned App, save automatic new-PR settings, create a defective PR, and require correlated review delivery, an inline finding, a consistent check, a successful run, and a Done task. Retain failures and confirm owned-resource cleanup. **Reason and benefit** The case makes a useful external integration reproducible without treating setup, unit checks, or report rendering as proof of a completed live journey. ## What Changed - Add two explicit attended cells for an existing configured agent in local or Daytona execution. - Declare hosting, account, connection, and credential prerequisites through private setup bindings. - Add fixed browser controls, ownership checks, independent grading, and cleanup. - Package declared screenshots through the existing sanitizer, manifest, validator, and dashboard. - Reject operations-managed execution until budgeted routing is qualified. - Document hosted QA credential setup and evidence limits. ## Verification - Focused catalog, oracle, report, connection, and ownership checks: 127 tests passed. - Inherited-environment preflight regression failed against the previous commit and passed after the fix. Actual-run environment verification remains required. - Harness TypeScript check and full workspace typecheck passed. Full build passed. The local full-test attempt was interrupted after repeated embedded PostgreSQL startup failures; its exit code of zero does not establish a pass; Exact-head Linux CI passed the full test, typecheck, build, Runner, serialized, browser, and canary gates on `abbddeade848edefc8de75f11cc9008b2cd52764`. - A real hosted attempt published a rated review and inline finding. It failed when its native session timed out before the task reached Done. Cleanup passed. Full live qualification remains open. - Credential scan found no known credential values in the authored files or retained report corpus. ## Risks - The full case creates external test resources and uses the configured provider. It requires an explicit selection and current task authority. It is excluded from default and CI matrices. - Time and run limits are not a hard dollar cap. Missing dollar cost stays unknown. Fixture setup does not weaken budget policy. - Fresh personal OAuth, unattended authentication, runtime skill-read proof, and cross-host qualification are separate claims. - The selected live fixture needs environment/lifecycle recovery before it can adopt a longer provider deadline. ## Model Used OpenAI GPT-6 family through Codex, with reasoning, code execution, and browser tools. The exact deployment model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used, including the available 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 described the issue in-PR following the enhancement template - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal ticket ID - [x] I have run focused tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation - [x] I have considered and documented risks - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open findings - [x] I will address all Greptile and reviewer comments before merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
63 lines
4.1 KiB
TypeScript
63 lines
4.1 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { packageEvidence } from "./evidence.js";
|
|
import { buildRunnerCampaign } from "./history.js";
|
|
import { renderRunnerE2EDashboard } from "./dashboard.js";
|
|
import { stageDashboardBrandAssets } from "./report-assets.js";
|
|
import { assertSecretFree, sanitizeJson } from "./redaction.js";
|
|
import { validateRetainedRunnerResult } from "./result-validation.js";
|
|
import type { GitHubJourneyResources } from "./github-review-ownership.js";
|
|
import type { MatrixExecution, RunnerE2EResult } from "./types.js";
|
|
|
|
/** Use the normal sanitizer, artifact manifest, validator, and dashboard. Raw
|
|
* operator receipts stay in the private attempt; they are not auto-published. */
|
|
export async function writeGitHubReviewReport(input: {
|
|
output: string; campaignId: string; result: RunnerE2EResult; resources: GitHubJourneyResources;
|
|
catalog: readonly MatrixExecution[]; secrets: readonly string[];
|
|
}) {
|
|
const stage = await mkdtemp(path.join(os.tmpdir(), "github-journey-report-"));
|
|
const output = path.resolve(input.output);
|
|
const evidenceDir = path.join(output, "evidence");
|
|
const realOutput = await realpath(output);
|
|
const save = async (root: string, name: string, value: unknown) => {
|
|
const text = JSON.stringify(sanitizeJson(value, input.secrets), null, 2) + "\n";
|
|
assertSecretFree(text, input.secrets, name);
|
|
await writeFile(path.join(root, name), text, { mode: 0o600 });
|
|
};
|
|
try {
|
|
await mkdir(path.join(stage, "snapshots"), { mode: 0o700 });
|
|
const result = structuredClone(input.result);
|
|
for (const shot of result.screenshots ?? []) {
|
|
const source = path.resolve(output, shot.file);
|
|
if (!source.startsWith(`${output}${path.sep}`) || !/^[a-z0-9_-]+$/.test(shot.id) || path.extname(source) !== ".png") throw new Error("unsafe_journey_screenshot_reference");
|
|
if (!(await realpath(source)).startsWith(`${realOutput}${path.sep}`)) throw new Error("unsafe_journey_screenshot_reference");
|
|
const bytes = await readFile(source);
|
|
if (shot.sha256 && createHash("sha256").update(bytes).digest("hex") !== shot.sha256) throw new Error("journey_screenshot_digest_mismatch");
|
|
shot.file = `snapshots/${shot.id}.png`;
|
|
await writeFile(path.join(stage, shot.file), bytes, { mode: 0o600 });
|
|
}
|
|
validateRetainedRunnerResult(result);
|
|
await save(stage, "result.json", result);
|
|
await save(stage, "snapshots/journey-resources.json", input.resources);
|
|
await save(stage, "snapshots/journey-evidence.json", result.githubReviewJourney);
|
|
const manifest = await packageEvidence({ privateDir: stage, uploadDir: evidenceDir, secrets: input.secrets, expectPassScreenshot: false });
|
|
if (manifest.leaks.length || manifest.missing.length) {
|
|
result.status = "failed"; result.failureClass = "secret_leak"; result.error = "journey_evidence_packaging_failed";
|
|
if (result.githubReviewJourney) result.githubReviewJourney.outcome = "failed";
|
|
}
|
|
await save(evidenceDir, "result.json", result);
|
|
await save(evidenceDir, "evidence-manifest.json", manifest);
|
|
const campaign = buildRunnerCampaign({ campaignId: input.campaignId, generatedAt: result.finishedAt, expected: [result.executionId], results: [result] });
|
|
await save(output, "campaign.json", campaign);
|
|
await save(output, "normalized-results.json", { ...campaign, results: [{ ...result, evidenceValid: !manifest.leaks.length && !manifest.missing.length, evidenceErrors: [] }] });
|
|
await stageDashboardBrandAssets(output);
|
|
const html = renderRunnerE2EDashboard({ title: "Ponytail GitHub user journey", generatedAt: result.finishedAt, expected: [result.executionId], catalog: input.catalog, campaign,
|
|
entries: [{ result, valid: !manifest.leaks.length && !manifest.missing.length, errors: [], evidenceBaseHref: "evidence" }] });
|
|
assertSecretFree(html, input.secrets, "dashboard.html");
|
|
await writeFile(path.join(output, "dashboard.html"), html, { mode: 0o600 });
|
|
return result;
|
|
} finally { await rm(stage, { recursive: true, force: true }); }
|
|
}
|