Files
PaperClipAI/tests/runner-e2e/everyday-flow.ts
T
DottaandPaperclip fe9d865250 fix(native): wake parents after revised child completions (#15372)
## Thinking Path

> - Paperclip manages agents and the tasks they perform.
> - A parent needs a new notification when a child finishes requested
revisions.
> - Native completion currently identifies that wake by the parent and
child only.
> - An already consumed notification can suppress the child's next
completion.
> - This PR binds ordinary child notifications to the committed status
decision.
> - A replay keeps the same identity, while a new completion can wake
the parent.
> - Live evaluation then exposed new child results being swallowed by an
active parent.
> - The repair preserves that input for a fresh turn and prevents
premature Done from discarding it.

## Linked Issues or Issue Description

Refs: #15218. The procedure experiments remain unshipped. This fixes
native completion wake identity and durable delivery to a busy parent.

Related open work: #10559 and #4507 address legacy wake deduplication,
#11179 addresses delivery during an active parent run, and #13044
addresses watchdog signals. This PR changes native committed-decision
keys and their delivery to an active parent, while preserving exact
watchdog behavior.

**What happened?**

A native child completed, its parent consumed the notification, and
feedback reopened the child. The second completion found the old
completed wake and created no new notification.

**Expected behavior**

Each new ordinary child completion can notify the parent. Replaying the
same committed completion must not add a notification. If the parent is
already running, the new result must remain available for a fresh turn;
an unread result must survive a parent Done claim.

**Steps to reproduce**

Complete a native child, consume its parent wake, reopen and complete
that same child, then inspect the parent wakes. The regression fails on
unchanged master because only one wake exists after two completions.

**Paperclip version or commit**

Baseline: `0fe47882cfcb12082035113c59ca96091c46ebfc`.

**Deployment mode**

Native runner with the standard server and PostgreSQL control plane.

## What Changed

- Include the durable status-decision ID in ordinary child completion
wake keys.
- Defer new ordinary native child completions behind an active parent,
carrying the committed decision identity and revised summary.
- Keep a parent in progress while that result is still queued, claimed
or deferred; recheck before the status commit. Use the existing
continuation without reviving cancelled tasks.
- Lock the parent before ordinary child status writes, making the
notification/Done ordering explicit without upgrading an implicit
foreign-key lock.
- Cover exact sequential delivery, dispatcher replay, and
queued/claimed/deferred versus consumed/current-run completion
identities in database tests.
- Apply it when the child is also a dependency and when it is only a
child.
- Preserve the stable key for exact `task_watchdog` origins to avoid
repeated watchdog loops.
- Extend real database conformance coverage for both relationships,
ordinary and near-match origins, watchdogs, revised summaries, and
replay.
- Reconcile the working checklist with the merged guidance PRs and
record the bounded next step.
- Reuse the current composer helper for Everyday task creation: capture
the returned task ID, preserve the exact prompt and chosen
assignee/project, and cover the setup with paused-agent browser tests.
The same setup correction is present in both comparison variants.

## Verification

**Ready for review and merge at
`e2fc0c8e3ddb84dd9bc045704c3d1ecb23ee3447`: both original live cases
pass, zero new failures against the frozen baseline, all checks green,
CLEAN/MERGEABLE and out of draft. Not merged.**

| Profile | Original baseline | Initial candidate | Fixed candidate |
| --- | --- | --- | --- |
| native Codex / `gpt-5.6-sol` | PASS | FAIL | PASS |
| ACPX Claude / `claude-sonnet-5` | FAIL | PASS | PASS |

One new pass, one unchanged pass, zero new failures and no pending pairs
against the original baseline. The earlier failed candidate is
preserved; it was fixed and measured at a new source, not regraded or
rerolled unchanged.

### Current source and live evidence

- [Completed campaign
37523025407](https://github.com/paperclipai/paperclip/actions/runs/37523025407)
and [public report with original
screenshots](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-37523025407-1/index.html).
Report and screenshots return HTTP 200; screenshot bytes match original
retained evidence.
- Measured candidate `e2fc0c8e3ddb84dd9bc045704c3d1ecb23ee3447`; reused
frozen baseline `7d700f43e93e89e4c196f8799b0aa3cef41d20da`. Trusted
workflow `88ff98b83d15eaa6640ee1848df4f0ad9bc82af3` is distinct from
measured sources; workflow blob
`0600886144d3e22ea2e4a38329a79177882f3948`. Prompts, fixture, oracle,
model/profile and input controls are unchanged. Both variants include
the same composer setup repair.
- Both original candidate grades PASS: all 31 checks per cell, parent
and child Done, independently tested revised ZIPs, successful cleanup.
Exactly one first attempt per profile on this source: 10 actual runs, no
retries. Reusing nine baseline runs gives 19 matched runs; the baseline
was not rerun.
- Claude directly exercised the repair: its revised child completed
while the parent was active, the parent's Done claim became InProgress
with `native_child_completion_pending`, and a fresh parent turn then
completed with the revised artifact.
- Both persisted final provider comments refer to the same attachment
whose parent-registration hash matches the revised ZIP tested by the
original oracle; native final evidence also references that attachment.
Codex provides a clickable download link. Claude describes the new
`--max-length` behavior but uses a backticked attachment ID, without a
clickable URL in the final prose. The artifact is registered on the
parent and independently downloaded/tested; prose-link usability remains
a presentation limit outside the original oracle. Artifact identity and
observed execution do not prove cognitive review.
- 68 focused scheduling/conformance/arbiter tests pass. Four regressions
fail against the original production files while four controls pass. The
database scheduler test proves one sequential continuation with the
revised summary and replay deduplication. It uses a mock adapter and is
separate from the live proof.
- Full local `pnpm -r typecheck` and `pnpm build` pass. [Exact-head full
CI
37522126627](https://github.com/paperclipai/paperclip/actions/runs/37522126627):
51 successful checks, two intentional skips, separate Snyk success.
Fresh exact-head Greptile
[5/5](https://github.com/paperclipai/paperclip/pull/15372#issuecomment-6022179172),
no new actionable findings; all review threads resolved.
- Ready-transition Contributor trust and Superagent Security Scan both
pass. The security scan completed at 2026-10-06T20:26:53Z with zero
annotations. Final total: 53 successful check runs, two intentional
skips and separate Snyk success; aggregate SUCCESS, source unchanged,
zero unresolved threads.
- The explicit parent lock precedes child writes. Its controlled
PostgreSQL ordering also passes on previous production through an
implicit foreign-key lock; this is hardening, not a reproduced
additional live failure. The four original failing regressions remain
the before/after proof of the active-parent repair.

### Preserved failures and accounting

- Initial candidate `a2ae2324ce7692e704fc43e99904b076d6246fee`: Codex
PASS → FAIL, Claude FAIL → PASS. Equal totals concealed a new failure
and did not qualify that source. Its Codex parent ended Blocked after
the revised child result coalesced into the active parent; there was no
retained later parent execution and the independent ZIP oracle was never
reached. A later server backstop log did not prove recovery.
- In the initial Claude cells, both parent finals referenced an earlier
parent ZIP while the oracle tested the revised child's ZIP. The original
candidate PASS did not establish latest-artifact delivery. Earlier
parent ZIP bytes are absent, so different hashes alone do not prove
missing functionality. Those original grades and content findings are
unchanged.
- Original baseline
[report](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-37509893406-1/index.html);
original candidate Claude
[artifact](https://github.com/paperclipai/paperclip/actions/runs/37509916344/artifacts/11434353106);
failed candidate Codex
[campaign](https://github.com/paperclipai/paperclip/actions/runs/37513070706).
The original cohort contains 17 actual runs, no retries, successful
cleanup.
- Campaign 37509916344 was cancelled after its Codex job waited over 16
minutes without a runner or steps. Completed Claude evidence was
preserved; only the missing Codex first trial was dispatched in
37513070706. The trusted workflow revision differs but workflow bytes
are identical. The cancelled campaign has no published HTML. Earlier
campaigns 37506546098 / 37506550087 retain four composer setup failures,
zero agent runs; the identical fixture-only repair restored setup
without changing the prompt or outcome oracle.
- Intermediate `3c1cf6830ce9db4123b834ffb99c594c383a8986` [campaign
37518652522](https://github.com/paperclipai/paperclip/actions/runs/37518652522)
was cancelled after a concurrency review finding. Both paid-cell steps
started and both tasks were created. Only invocation policies survived;
no grade, run inventory or cleanup receipt. Provider activity and
charges are unknown: two incomplete attempts, not passes or
zero-provider setup failures.
- Cumulative accounting: **27 known actual runs** (17 original + 10
fixed-candidate), plus unknown activity in those two cancelled
intermediate attempts. The 19-run matched comparison reuses nine
baseline runs and is not additional execution. Reported LLM amounts are
zero with original billing `complete=true`; actual charges are unknown
and local/hosted runtime is unmetered. No free-run, speed or cost claim.
- Original and new results pass the canonical result validator. Retained
source, input, result/API/story/final-ledger/usage run identities and
artifact hashes were audited. Each downloaded package omits the
pre-upload-declared `playwright-output/.last-run.json`; primary result,
API, final ledger, story, screenshots and reached ZIP oracles are
retained. No full-package completeness claim.
- The initial redundant local full test invocation was stopped after
2,492.5 seconds once that head's CI passed; completed groups recorded
23,045 passes and 87 skips. That local invocation remains incomplete.
Current full CI is the repository-wide test evidence.

## Risks

A revised completion can schedule another sequential parent run and its
normal budget use. A parent Done claim remains non-terminal while a
newer native child result awaits delivery. Company scope, governance,
workspace-finalization, terminal cancellation and exact watchdog
behavior remain enforced. This changes native completion authority and
scheduling, so the durable identity, replay and concurrent-commit
controls matter.

The two live trials qualify the observed revised-child handoff, not
broad task quality or causal/general equivalence. Claude's final prose
still gives an attachment ID without a clickable URL, although the
registered revised artifact passes the original download oracle.
Completion before any new child result exists, removal of unfinished
dependencies and broader instruction reduction remain separate
questions. There is no schema or prompt change.

## Model Used

OpenAI Codex, GPT-6 family, with repository inspection, shell execution
and code editing. The exact serving model ID, reasoning setting and
context window 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 (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
- [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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-06 16:11:43 -05:00

1608 lines
62 KiB
TypeScript

import { gradeAgentmailSetup } from "./agentmail-setup-evidence.js";
import { expect, type Page } from "@playwright/test";
import { runnerApiToolsEnabled } from "../../server/src/services/native-runtime/runner-api-rollout.js";
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir } from "node:fs/promises";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { pollUntil, type RunnerApi } from "./api.js";
import {
latestStoryDelivery,
type StoryDelivery,
} from "./everyday-delivery.js";
import type { LiveFixtureValues } from "./live-fixtures.js";
import type { MatrixExecution } from "./types.js";
import { submitTaskReply } from "./user-actions.js";
import { createTaskFromPromptThroughUi } from "./plan-task-ui.js";
import { waitForTaskChatRendered } from "./continuation-screenshot.js";
import { hasPersistedSource, isSavedSourceCheckpoint } from "./everyday-interruption.js";
import { setupAggregatorFixture } from "./aggregator-fixture.js";
import { gradeProviderChoice, gradeProviderOutcome } from "./connection-routing-evidence.js";
import { setupConnectionReview } from "./connection-reviews.js";
import {
pendingStoryDecision,
StoryDecisionError,
type StoryInteraction,
} from "./everyday-decisions.js";
import { LATE_REQUIREMENT, SLUGIFY_REVISION, requiresEverydayArtifactOracle } from "./everyday-cases.js";
import {
isActiveStoryRun,
isStoryWorkspaceDeferral,
isExpectedStoryInterruption,
storyUnexpectedRunFailure,
storyUnexercisedReviewBoundary,
storyLifecycleChecks,
storyRepliesConsumed,
storyHasAgentReply,
storyHasPendingHumanInteraction,
storyReviewContinuationTimeoutDetail,
storyHasStrandedBlockedLeaf,
storyHasDurableAgentReviewContinuation,
storyIssueHasBlockedTimelineBefore,
storyIssueHasUnresolvedDependency,
storyRunReportsDependencyBlock,
storyParentFinishedAfterChildren,
storyAcceptedAgentReview,
artifactGradeModeForPhase,
storyParentCompletionPrecedesReview,
type StoryCheck,
type StoryIssue,
type StoryRun,
} from "./everyday-observations.js";
type Row = Record<string, any>;
export interface EverydayEvidence {
schema: "paperclip.everyday-workflow.v1";
caseId: string;
prompt: string;
harnessDigest?: string;
sourceRevision?: string;
providerVersion?: string;
fixtureConfiguration?: {
apiToolsEnabled: boolean;
aiConnection?: LiveFixtureValues["aiConnection"];
};
documents?: Row[];
checks: StoryCheck[];
timeline: Array<{ at: string; action: string; detail?: unknown }>;
issues: Array<StoryIssue & Row>;
runs: StoryRun[];
agents: Row[];
downloads: Row[];
allowedInterruptedRuns: string[];
}
interface Input {
page: Page;
api: RunnerApi;
fixtures: LiveFixtureValues;
execution: MatrixExecution;
nonce: string;
workspacePath: string;
privateDir: string;
deadlineAt: number;
restart(): Promise<void>;
observe(issue: StoryIssue, runs: StoryRun[]): void;
capture(id: string, label: string, file: string): Promise<void>;
evidence(name: string, value: unknown): Promise<void>;
}
function runCommand(
command: string,
args: string[],
timeout = 20_000,
): Promise<{ code: number; stdout: string }> {
return new Promise((resolve, reject) => {
const env = Object.fromEntries(
Object.entries(process.env).filter(([key]) =>
["PATH", "SYSTEMROOT", "TMPDIR"].includes(key),
),
);
const child = spawn(command, args, {
env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => child.kill("SIGKILL"), timeout);
child.stdout.on("data", (chunk) => {
stdout += String(chunk);
if (stdout.length > 1_000_000) child.kill("SIGKILL");
});
child.stderr.on("data", (chunk) => {
stderr += String(chunk);
});
child.on("error", (error) => {
clearTimeout(timer);
reject(error);
});
child.on("exit", (code) => {
clearTimeout(timer);
if (code === null)
reject(new Error("Bounded evaluator command timed out"));
else resolve({ code, stdout: stdout || stderr });
});
});
}
/** No DB writes, fabricated tool receipts, or corrective messages after a failed check. */
export async function runEverydayFlow(input: Input) {
const { page, api, fixtures, execution, nonce } = input;
const prefix = fixtures.company.issuePrefix!;
const ev: EverydayEvidence = {
schema: "paperclip.everyday-workflow.v1",
caseId: execution.task.id,
prompt: execution.task.buildPrompt(nonce),
fixtureConfiguration: {
apiToolsEnabled: runnerApiToolsEnabled(fixtures.company.id),
aiConnection: fixtures.aiConnection,
},
checks: [],
timeline: [],
issues: [],
runs: [],
agents: [],
downloads: [],
allowedInterruptedRuns: [],
};
const note = (action: string, detail?: unknown) =>
ev.timeline.push({
at: new Date().toISOString(),
action,
...(detail === undefined ? {} : { detail }),
});
const check = (id: string, passed: boolean, detail: string) => {
ev.checks.push({ id, passed, detail });
};
let parent: StoryIssue | undefined;
let lastSubmissionAt = 0;
const submittedCommentIds: string[] = [];
let review: Awaited<ReturnType<typeof setupConnectionReview>> | undefined;
let project = fixtures.project;
const caseId = execution.task.id;
const providerChoice = caseId === "provider-decline" || caseId === "provider-second";
const nativeProviderCase = caseId === "provider-native";
let aggregatorFixture: Awaited<ReturnType<typeof setupAggregatorFixture>> | undefined;
const agentmailSetup = caseId === "agentmail-setup";
const decliningConnection = caseId === "connection-decline" || nativeProviderCase || agentmailSetup;
const declining = decliningConnection || caseId === "service-decline";
let decisionId: string | undefined;
let decisionResolvedAt: string | undefined;
let initialConnections: string[] = [];
let stoppedWorkspace: Record<string, string> | undefined;
let settledAgentReply: Row | undefined;
let reviewHandoffBoundary: {
childId: string;
assigneeAgentId: string | null | undefined;
interactionId: string;
interactionCreatedAt?: string;
parentRunId?: string;
parentBlockedObserved: boolean;
} | undefined;
async function workspaceFiles() {
const workspaceRoot = input.workspacePath;
const files: Record<string, string> = {};
for (const entry of await readdir(workspaceRoot, {
withFileTypes: true,
})) {
if (entry.isFile() && /\.(py|md|zip)$/.test(entry.name))
files[entry.name] = createHash("sha256")
.update(await readFile(path.join(workspaceRoot, entry.name)))
.digest("hex");
}
return files;
}
async function runnerStopped(run: StoryRun) {
if (!run.processPid) return false;
const identity = await runCommand("ps", [
"-p",
String(run.processPid),
"-o",
"command=",
]);
return (
identity.code !== 0 || !identity.stdout.includes(`--run-id ${run.id} `)
);
}
async function refresh() {
const listed = await api.get<StoryRun[]>(
`/api/companies/${fixtures.company.id}/heartbeat-runs?limit=100`,
);
ev.runs = await Promise.all(
listed.map((r) => api.get<StoryRun>(`/api/heartbeat-runs/${r.id}`)),
);
const issues = await api.get<StoryIssue[]>(
`/api/companies/${fixtures.company.id}/issues`,
);
ev.issues = await Promise.all(
issues.map(async (issue) => ({
...issue,
comments: await api.get<Row[]>(
`/api/issues/${issue.id}/comments?order=asc`,
),
queuedComments: await api.get<Row>(
`/api/issues/${issue.id}/queued-comments`,
),
interactions: await api.get<Row[]>(
`/api/issues/${issue.id}/interactions`,
),
wakeDiagnostics: await api.get<Row>(
`/api/issues/${issue.id}/diagnostics/wakes`,
),
activity: await api.get<Row[]>(`/api/issues/${issue.id}/activity`),
})),
);
if (parent) {
parent = ev.issues.find((i) => i.id === parent!.id) as StoryIssue;
input.observe(parent!, ev.runs);
}
return ev;
}
const taskUrl = (issue: StoryIssue) =>
`/${prefix}/issues/${issue.identifier ?? issue.id}`;
async function openTask(issue: StoryIssue) {
await page.goto(taskUrl(issue), { waitUntil: "domcontentloaded" });
if (providerChoice || nativeProviderCase) {
await expect(page.locator('[data-testid="task-chat-thread"], [data-testid="thread-root"]').first()).toBeVisible({timeout:30_000});
await expect(page.getByRole("heading", {name:String(issue.title),exact:true})).toBeVisible();
await expect(page.getByTestId("issue-chat-skeleton")).toHaveCount(0);
} else await waitForTaskChatRendered(page, String(issue.title));
}
async function openParent() {
await openTask(parent!);
}
function observableAgentIds(state: EverydayEvidence) {
return [
fixtures.agent.id,
...state.issues.flatMap((issue) =>
[
issue.assigneeAgentId,
...(issue.interactions ?? []).map(
(interaction) => interaction.addresseeAgentId,
),
].filter((agentId): agentId is string => Boolean(agentId)),
),
];
}
async function reply(message: string, target: StoryIssue = parent!) {
const priorFailures = ev.checks.filter((c) => !c.passed);
if (priorFailures.length)
throw new Error(
`Story prerequisite failed before the next user request: ${priorFailures.map((c) => c.id).join(", ")}`,
);
const before = await api.get<Row[]>(`/api/issues/${target.id}/comments`);
lastSubmissionAt = Date.now();
await submitTaskReply(page, message);
const after = await pollUntil({
label: "one persisted user reply",
deadlineAt: Date.now() + 30_000,
load: () => api.get<Row[]>(`/api/issues/${target.id}/comments`),
accept: (rows) =>
rows.filter(
(c) => !c.authorAgentId && !before.some((old) => old.id === c.id),
).length > 0,
});
const added = after.filter(
(c) => !c.authorAgentId && !before.some((old) => old.id === c.id),
);
check(
`reply-${ev.timeline.length}-stored-once`,
added.length === 1,
"A composer submission creates exactly one user message.",
);
submittedCommentIds.push(...added.map((c) => c.id));
note("composer-message-persisted", {
issueId: target.id,
commentIds: added.map((c) => c.id),
});
}
async function settled(expectedAgentReply?: string) {
const settledState = await pollUntil({
label: `everyday ${caseId} settled`,
deadlineAt: input.deadlineAt,
timeoutDetail: (state) => state &&
storyReviewContinuationTimeoutDetail(
state.issues, parent?.id ?? "", fixtures.agent.id, state.runs,
observableAgentIds(state),
),
intervalMs: 1000,
load: refresh,
accept: (state) =>
state.issues.length > 0 &&
state.issues.every(
(i) => i.status === "done" && i.queuedComments.entries.length === 0,
) &&
storyRepliesConsumed(state.runs, submittedCommentIds) &&
state.runs.length > 0 &&
!state.runs.some(isActiveStoryRun) &&
state.runs.some(
(r) => Date.parse(r.finishedAt ?? "") >= lastSubmissionAt,
) &&
(!expectedAgentReply ||
storyHasAgentReply(
state.issues.find((issue) => issue.id === parent?.id),
fixtures.agent.id,
expectedAgentReply,
)),
reject: (state) => {
if (state.runs.length > 12) return "bounded execution count exceeded";
const bad = storyUnexpectedRunFailure(state.runs, ev.allowedInterruptedRuns);
if (bad)
return `native execution failed ${bad.errorCode ?? ""}: ${bad.error ?? bad.status}`;
if (
state.runs.some(isActiveStoryRun) ||
state.issues.some((i) => i.scheduledRetry || i.activeRecoveryAction)
)
return;
if (
state.runs.length &&
storyHasStrandedBlockedLeaf(state.issues, observableAgentIds(state)) &&
!storyHasDurableAgentReviewContinuation(
state.issues,
parent?.id ?? "",
fixtures.agent.id,
state.runs,
)
)
return "task is Blocked without an active continuation";
if (
state.issues.some(
(i) =>
i.status === "in_review" &&
storyHasPendingHumanInteraction(i, observableAgentIds(state)),
)
)
return "unexpected human interaction: task did not finish autonomously";
},
});
if (expectedAgentReply) {
const issue = settledState.issues.find((candidate) => candidate.id === parent?.id);
const reply = issue?.comments?.find(
(comment) =>
comment.authorAgentId === fixtures.agent.id &&
String(comment.body ?? "").includes(expectedAgentReply),
);
if (reply) {
// Keep the exact snapshot that satisfied the readiness predicate. A
// later refresh may return a newer projection and must not change the
// evidence used by the assertion below.
settledAgentReply = { ...reply };
note("expected-agent-reply-visible-at-settlement", {
issueId: issue?.id,
commentId: reply.id,
authorAgentId: reply.authorAgentId,
createdAt: reply.createdAt,
body: reply.body,
});
}
}
await openParent();
await expect(
page.getByTestId("issue-detail-header").getByRole("button", {
name: "Change status (current: Done)",
exact: true,
}),
).toBeVisible();
note("all-tasks-done");
}
async function downloadDelegated(
mode: "base" | "separator" | "max-length",
phase: string,
after?: number,
) {
const related = ev.issues.filter(
(issue) => issue.id === parent!.id || issue.parentId === parent!.id,
);
const attachments = (
await Promise.all(
related.map(async (issue) =>
(await api.get<Row[]>(`/api/issues/${issue.id}/attachments`)).map(
(a) => ({ ...a, issueId: issue.id }) as StoryDelivery,
),
),
)
).flat();
const delivery = latestStoryDelivery(
attachments,
related.map((issue) => issue.id),
after,
after === undefined ? [] : ev.downloads.map((d) => d.sha256),
);
if (!delivery) {
check(
`${phase}.zip-delivered`,
false,
"No new downloadable ZIP was delivered on the parent or its child tasks.",
);
return;
}
note("delivery-selected", { attachmentId: delivery.id, issueId: delivery.issueId, sha256: delivery.sha256, after });
await download(delivery.issueId, mode, phase, delivery.id);
}
async function download(
issueId: string,
mode: "base" | "separator" | "max-length",
phase: string,
selectedAttachmentId?: string,
) {
const attachments = await api.get<Row[]>(
`/api/issues/${issueId}/attachments`,
);
const zips = attachments
.filter(
(a) =>
String(a.originalFilename ?? a.filename ?? "").endsWith(".zip") ||
a.contentType === "application/zip",
)
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
if (!zips.length) {
check(
`${phase}.zip-delivered`,
false,
"No downloadable ZIP attachment was delivered.",
);
return;
}
const attachment = selectedAttachmentId
? zips.find((a) => a.id === selectedAttachmentId)
: zips[zips.length - 1];
if (!attachment) throw new Error("Selected delivery is no longer available");
const issue = ev.issues.find((i) => i.id === issueId)!;
await openTask(issue);
const links = page.locator(
`a[href*="/api/attachments/${attachment.id}/content"]`,
);
const link = links.filter({ hasText: /Download/i }).first();
await expect(link).toBeVisible({ timeout: 15_000 });
const pending = page.waitForEvent("download", { timeout: 30_000 });
await link.click();
const file = await pending;
const target = path.join(input.privateDir, "snapshots", `${phase}.zip`);
await file.saveAs(target);
const bytes = await readFile(target);
const digest = createHash("sha256").update(bytes).digest("hex");
const result = await runCommand(process.env.PYTHON ?? "python3", [
path.join(import.meta.dirname, "everyday-artifact.py"),
target,
"--mode",
mode,
], Math.max(1, Math.min(60_000, input.deadlineAt - Date.now())));
const oracle = JSON.parse(result.stdout) as { checks: StoryCheck[] };
ev.checks.push(
...oracle.checks.map((c) => ({ ...c, id: `${phase}.${c.id}` })),
);
ev.downloads.push({
phase,
attachmentId: attachment.id,
issueId,
sha256: digest,
bytes: bytes.length,
filename: file.suggestedFilename(),
mode,
});
note("download-verified", {
phase,
attachmentId: attachment.id,
sha256: digest,
});
await openParent();
}
async function recordSource(
label = "source-saved-before-interruption",
evidenceName = "source-before-interruption.json",
requireActive = false,
maxWaitMs = 180_000,
) {
const filePath = path.join(input.workspacePath, "slugify.py");
const bytes = await pollUntil({
label: requireActive ? "active run with saved source" : "saved source after interruption",
deadlineAt: Math.min(input.deadlineAt, Date.now() + maxWaitMs),
intervalMs: 500,
load: async () => {
await refresh();
const source = await readFile(filePath).catch(() => undefined);
return { active: ev.runs.some(isActiveStoryRun), source };
},
accept: ({ active, source }) =>
requireActive
? isSavedSourceCheckpoint(active, source)
: hasPersistedSource(source),
});
if (!bytes.source) throw new Error("Saved source was not available at the controlled boundary");
await input.evidence(evidenceName, {
body: bytes.source.toString("utf8"),
sha256: createHash("sha256").update(bytes.source).digest("hex"),
});
note(label, {
sha256: createHash("sha256").update(bytes.source).digest("hex"),
bytes: bytes.source.length,
active: bytes.active,
});
}
async function sourceReady() {
await recordSource("source-saved-before-interruption", "source-before-interruption.json", true);
}
async function prepareStopBoundary() {
// Providers can finish a short first turn before the browser can click
// Stop. If that happens, submit one ordinary user follow-up through the
// composer and use that fresh run as the controlled interruption boundary.
// Save the checkpoint even if a fast provider has already finished. Do
// not shorten the normal source-creation budget to manufacture a timeout.
await recordSource("source-saved-before-interruption", "source-before-interruption.json");
await refresh();
if (ev.runs.some(isActiveStoryRun)) return;
await submitTaskReply(page, `${SLUGIFY_REVISION}\nContinue working until the source file is saved.`);
note("stop-boundary-continuation-submitted");
await recordSource(
"source-saved-before-interruption",
"source-before-interruption.json",
true,
);
}
try {
await mkdir(path.join(input.privateDir, "snapshots"), { recursive: true });
const revision = await runCommand("git", ["rev-parse", "HEAD"]);
if (revision.code === 0) ev.sourceRevision = revision.stdout.trim();
// Native providers run the packaged runtime (possibly remotely). A host
// `claude`/`codex` binary is neither required nor its observed version.
const harnessFiles = [
"everyday-flow.ts",
"everyday-cases.ts",
"everyday-decisions.ts",
"aggregator-fixture.ts",
"connection-routing-evidence.ts",
"agentmail-setup-evidence.ts",
"everyday-delivery.ts",
"everyday-observations.ts",
"everyday-artifact.py",
"user-actions.ts",
"runner.spec.ts",
"api.ts",
"harness-env.ts",
"failure-classifier.ts",
"live-fixtures.ts",
"connection-reviews.ts",
"catalog.ts",
];
ev.harnessDigest = createHash("sha256")
.update(
(
await Promise.all(
harnessFiles.map((f) =>
readFile(path.join(import.meta.dirname, f)),
),
)
)
.map((b) => b.toString())
.join("\n"),
)
.digest("hex");
if (requiresEverydayArtifactOracle(caseId)) {
try {
const sandbox = await runCommand(process.env.PYTHON ?? "python3", [
path.join(import.meta.dirname, "everyday-artifact.py"), "--preflight",
]);
if (sandbox.code !== 0) throw new Error(sandbox.stdout);
note("artifact-sandbox-qualified", { isolation: "docker", network: "none" });
} catch (error) {
throw new Error(`Artifact sandbox qualification failed before task creation: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
}
}
if (!project && !caseId.startsWith("service-") && !decliningConnection && !providerChoice) {
project = await api.post(
`/api/companies/${fixtures.company.id}/projects`,
{
name: `Studio project ${nonce}`,
description: "Small software project",
executionWorkspacePolicy: {
enabled: true,
defaultMode: "shared_workspace",
sharedWorkspaceConcurrency: "serialize",
allowIssueOverride: false,
environmentId: fixtures.environment.id,
workspaceStrategy: { type: "project_primary" },
},
workspace: {
name: "Primary",
sourceType: "local_path",
cwd: input.workspacePath,
isPrimary: true,
},
},
);
}
await api.patch(`/api/agents/${fixtures.agent.id}/permissions`, {
canCreateAgents: true,
canAssignTasks: true,
});
if (caseId === "delegate-feedback" || caseId === "agent-review-handoff") {
const config = execution.profile.buildAgent({
environmentId: fixtures.environment.id,
environmentFixtureId: execution.environment.id,
workspacePath: input.workspacePath,
secretRefs: fixtures.secretRefs,
executionId: nonce,
});
await api.post(`/api/companies/${fixtures.company.id}/agents`, {
...config,
name: "Riley Builder",
role: "engineer",
title: "Engineer",
reportsTo: fixtures.agent.id,
instructionsBundle: {
entryFile: "AGENTS.md",
files: {
"AGENTS.md":
"You implement small software projects and verify your work. Follow task feedback and deliver usable files.",
},
},
});
}
if (caseId.startsWith("service-"))
review = await setupConnectionReview({
page,
api,
prefix,
companyId: fixtures.company.id,
agentId: fixtures.agent.id,
marker: `Pages: Roadmap, Meeting notes. Verification code: SERVICE_${nonce}`,
authenticated: true,
});
if (providerChoice || nativeProviderCase) {
if (caseId === "provider-second") aggregatorFixture = await setupAggregatorFixture(api, fixtures.company.id, fixtures.agent.id, `CONTACTS_${nonce}`);
const state = await api.get<{connections:Row[]}>(`/api/companies/${fixtures.company.id}/tools/connections`);
initialConnections = state.connections.map(c=>c.id);
}
if (agentmailSetup) await api.patch("/api/instance/settings/experimental", { enableChatConnectors: true });
if (decliningConnection) {
const state = await api.get<{ connections: Row[] }>(
`/api/companies/${fixtures.company.id}/tools/connections`,
);
initialConnections = state.connections.map((c) => c.id);
check(
"connection-starts-unconfigured",
state.connections.length === 0,
"This isolated company has no service connection before the request.",
);
if (state.connections.length)
throw new Error("New-connection story requires an unconnected company");
}
const created = await createTaskFromPromptThroughUi({
page,
companyId: fixtures.company.id,
issuePrefix: prefix,
agentName: fixtures.agent.name,
prompt: ev.prompt,
projectName: project?.name,
});
parent = await api.get<StoryIssue>(`/api/issues/${created.id}`);
input.observe(parent!, []);
note("task-submitted", { issueId: parent!.id });
await openParent();
if (caseId === "agent-review-handoff") {
const boundary = await pollUntil({
label: "blocked parent with agent review wake",
deadlineAt: input.deadlineAt,
load: refresh,
accept: (state) => {
const child = state.issues.find((issue) => issue.parentId === parent!.id);
const interaction = child?.interactions?.find(
(candidate) =>
["pending", "accepted"].includes(String(candidate.status)) &&
candidate.addresseeAgentId === fixtures.agent.id &&
candidate.effectiveResolverPolicy !== "human_only",
);
const parentIssue = state.issues.find((issue) => issue.id === parent!.id);
const reviewRun = interaction?.resolvedByRunId
? state.runs.find((run) => run.id === interaction.resolvedByRunId)
: undefined;
const parentRun = state.runs.find(
(run) =>
(run.nativeIssueId === parent!.id ||
run.contextSnapshot?.issueId === parent!.id ||
run.contextSnapshot?.taskId === parent!.id) &&
run.status === "succeeded" &&
run.finishedAt &&
(reviewRun?.startedAt
? Date.parse(run.finishedAt) <= Date.parse(reviewRun.startedAt)
: parentIssue?.status === "blocked"),
);
const parentBlockedEvidence =
Boolean(
parentIssue && storyIssueHasUnresolvedDependency(parentIssue),
) ||
Boolean(
parentIssue &&
storyIssueHasBlockedTimelineBefore(
parentIssue,
reviewRun?.startedAt ?? undefined,
),
) ||
Boolean(parentRun && storyRunReportsDependencyBlock(parentRun));
return Boolean(
parentIssue &&
["blocked", "done"].includes(parentIssue.status) &&
["in_review", "done"].includes(String(child?.status)) &&
interaction &&
parentBlockedEvidence &&
parentRun &&
(parentIssue.status === "blocked" || reviewRun),
);
},
reject: (state) => {
const failed = state.runs.find((run) =>
["failed", "timed_out"].includes(run.status),
);
return failed
? `Review handoff prerequisite failed: ${failed.errorCode}: ${failed.error}`
: storyUnexercisedReviewBoundary(state.issues, state.runs, parent!.id, fixtures.agent.id);
},
});
const child = boundary.issues.find((issue) => issue.parentId === parent!.id)!;
const interaction = child.interactions!.find(
(candidate) =>
["pending", "accepted"].includes(String(candidate.status)) &&
candidate.addresseeAgentId === fixtures.agent.id,
)!;
const reviewRun = interaction.resolvedByRunId
? boundary.runs.find((run) => run.id === interaction.resolvedByRunId)
: undefined;
const parentAtBoundary = boundary.issues.find(
(issue) => issue.id === parent!.id,
);
const parentRun = boundary.runs.find(
(run) =>
(run.nativeIssueId === parent!.id ||
run.contextSnapshot?.issueId === parent!.id ||
run.contextSnapshot?.taskId === parent!.id) &&
run.status === "succeeded" &&
run.finishedAt &&
storyParentCompletionPrecedesReview(
run.finishedAt,
reviewRun?.startedAt,
parentAtBoundary?.status === "blocked",
),
);
reviewHandoffBoundary = {
childId: child.id,
assigneeAgentId: child.assigneeAgentId,
interactionId: interaction.id!,
interactionCreatedAt: interaction.createdAt,
parentRunId: parentRun?.id,
parentBlockedObserved: boundary.issues.some(
(issue) =>
issue.id === parent!.id && storyIssueHasUnresolvedDependency(issue),
),
};
check(
"parent-blocked-before-agent-review",
reviewHandoffBoundary.parentBlockedObserved || Boolean(parentRun),
"The completed lead run and durable dependency projection precede the child review card.",
);
note("agent-review-requested", {
childId: child.id,
childAssigneeAgentId: child.assigneeAgentId,
interactionId: interaction.id,
parentStatus: boundary.issues.find((issue) => issue.id === parent!.id)?.status,
parentRunId: parentRun?.id,
parentRunFinishedAt: parentRun?.finishedAt,
interactionCreatedAt: interaction.createdAt,
});
await openParent();
}
if (caseId === "delegate-feedback") {
await pollUntil({
label: "active delegated child",
deadlineAt: input.deadlineAt,
load: refresh,
accept: (state) =>
state.issues.some(
(i) =>
i.parentId === parent!.id &&
state.runs.some(
(r) =>
isActiveStoryRun(r) &&
(r.contextSnapshot?.issueId === i.id ||
r.contextSnapshot?.taskId === i.id),
),
),
reject: (state) => {
const failed = state.runs.find((run) =>
["failed", "timed_out"].includes(run.status),
);
return failed
? `Delegation prerequisite failed before feedback: ${failed.errorCode}: ${failed.error}`
: undefined;
},
});
const child = ev.issues.find((i) => i.parentId === parent!.id)!;
note("late-feedback-boundary", {
childId: child.id,
activeRunIds: ev.runs.filter(isActiveStoryRun).map((r) => r.id),
});
await openTask(child);
await reply(LATE_REQUIREMENT, child);
note("late-feedback-delivered-to-child", { childId: child.id });
await openParent();
}
if (
caseId === "recover-controller" ||
caseId === "stop-redirect"
) {
if (execution.environment.id === "daytona") {
// A completed, downloaded first version is an observable remote persistence checkpoint.
await settled();
await download(parent!.id, "base", "before-restart");
if (!ev.downloads.length || ev.checks.some((c) => !c.passed))
throw new Error(
"Remote fault boundary unexercised: no verified saved project",
);
await reply(SLUGIFY_REVISION);
note("remote-revision-submitted");
await pollUntil({
label: "remote revision executing",
deadlineAt: input.deadlineAt,
load: refresh,
accept: (s) => s.runs.some(isActiveStoryRun),
});
} else if (caseId === "stop-redirect") await prepareStopBoundary();
else await sourceReady();
const active = ev.runs.find((r) => r.status === "running");
if (!active)
throw new Error(
"Fault boundary was not exercised: no active execution",
);
if (caseId === "stop-redirect") {
await page.getByTestId("task-chat-composer-stop").last().click();
ev.allowedInterruptedRuns.push(active.id);
note("stop-clicked", { runId: active.id });
await pollUntil({
label: "owned runner stopped",
deadlineAt: input.deadlineAt,
load: () => runnerStopped(active),
accept: Boolean,
intervalMs: 250,
});
await recordSource("source-saved-after-interruption", "source-after-interruption.json");
stoppedWorkspace = await workspaceFiles();
note("stopped-workspace-snapshot", stoppedWorkspace);
await reply(
`Change direction. Leave the project as it is. Reply with just this short note: "The studio is ready. Reference ${nonce}."`,
);
note("new-direction-submitted");
await page.reload();
} else {
await reply(LATE_REQUIREMENT);
note("followup-submitted-before-interruption");
ev.allowedInterruptedRuns.push(active.id);
await input.restart();
note("controller-restarted");
await openParent();
}
}
if (providerChoice) {
const rows = await pollUntil({
label: "external-provider choice", deadlineAt: input.deadlineAt,
load: async () => {
const runs = await api.get<StoryRun[]>(`/api/issues/${parent!.id}/runs`);
const failure = runs.find(run => ["failed", "timed_out"].includes(run.status));
if (failure) throw new Error(`Stopped waiting for external-provider choice: agent failed before selection: ${failure.error ?? failure.status}`);
const rows = await api.get<Row[]>(`/api/issues/${parent!.id}/interactions`);
const issue = await api.get<StoryIssue>(`/api/issues/${parent!.id}`);
if (!rows.some(row=>row.status==="pending") && ["done", "blocked", "cancelled"].includes(issue.status)) throw new Error(`Stopped waiting for external-provider choice: task reached ${issue.status} without asking the user`);
return rows;
},
accept: rows => rows.some(row=>row.status==="pending"),
});
const decision = gradeProviderChoice(rows as any, aggregatorFixture?.invocationCount() ?? 0);
decisionId = decision.interaction.id;
check("provider-disclosed-before-choice", true, "Ranked external providers and None were offered before any call.");
// Exercise durable selection across a real controller restart and browser reload.
await input.restart();
await openParent();
const choice = page.getByRole("radio", {name: caseId === "provider-decline" ? /None for now/ : /^Arcade/});
await expect(choice).toBeVisible();
await input.capture("provider-choice", "External service choice after restart", "provider-choice.png");
await choice.click();
await page.getByRole("button", {name:"Submit answers",exact:true}).click();
await pollUntil({label:"provider choice saved", deadlineAt:input.deadlineAt,
load:()=>api.get<Row[]>(`/api/issues/${parent!.id}/interactions`),
accept:rows=>rows.some(row=>row.id===decisionId && row.status==="answered"),
});
note("provider-choice-submitted", {interactionId:decisionId, selected:caseId === "provider-decline" ? "none" : "via:arcade:hubspot"});
}
if (review || decliningConnection) {
const interactions = await pollUntil({
label: "story decision request",
deadlineAt: input.deadlineAt,
load: async () => {
const [interactions, issue] = await Promise.all([
api.get<StoryInteraction[]>(
`/api/issues/${parent!.id}/interactions`,
),
api.get<StoryIssue>(`/api/issues/${parent!.id}`),
]);
return { interactions, issue, calls: review?.invocationCount() ?? 0 };
},
accept: (state) =>
state.interactions.some((i) => i.status === "pending"),
reject: (state) =>
state.calls > 0
? `The provider received ${state.calls} call(s) before approval.`
: ["done", "blocked", "cancelled"].includes(state.issue.status)
? `Task reached ${state.issue.status} without requesting the expected decision.`
: undefined,
});
await openParent();
await expect(
page
.locator(
'[data-testid="task-chat-thread"], [data-testid="thread-root"]',
)
.first(),
).toBeVisible();
await expect(page.getByTestId("issue-chat-skeleton")).toHaveCount(0);
const pendingInteraction = pendingStoryDecision(
interactions.interactions,
review
? { kind: "tool", connectionId: review.connectionId }
: { kind: "connection", serviceSlug: agentmailSetup ? "agentmail" : nativeProviderCase ? "jira" : "notion" },
);
await expect(
page.getByRole("button", {
name: decliningConnection ? "Not now" : "Review request",
exact: true,
}),
).toBeVisible();
await input.capture(
"decision-pending",
"Request before the user decision",
"decision-pending.png",
);
decisionId = pendingInteraction.id;
check(
"decision-request-matches-story",
true,
review
? "Tool approval belongs to the installed page service."
: `New connection request is for ${agentmailSetup ? "AgentMail" : nativeProviderCase ? "Jira" : "Notion"}, without an external-provider question.`,
);
if (agentmailSetup) {
// Reload proves the card is durable, rather than a transient model UI.
await page.reload();
const form = page.getByTestId("agentmail-inline-setup");
await expect(form).toBeVisible();
const card = page.getByTestId("connection-intent-focus-target").filter({ has: form });
const rows = await api.get<Parameters<typeof gradeAgentmailSetup>[0]["interactions"]>(`/api/issues/${parent!.id}/interactions`);
const checks = gradeAgentmailSetup({
// This harness boots a dedicated local_trusted instance.
interactions: rows, agentId: fixtures.agent.id, userId: "local-board",
visible: await form.isVisible(),
inputTypes: await card.locator("input").evaluateAll(inputs => inputs.map(input => input.getAttribute("type") ?? "text")),
keyLink: await form.getByRole("link", { name: "Get an AgentMail API key" }).getAttribute("href"),
accessSelectorCount: await card.locator('[role="radiogroup"], [role="combobox"], select').count(),
dialogCount: await page.getByRole("dialog").count(),
});
ev.checks.push(...checks);
await input.capture("agentmail-inline-key", "AgentMail API-key card after reload", "agentmail-inline-key.png");
if (checks.some(result => !result.passed)) throw new Error("AgentMail inline setup evidence failed");
}
if (review)
check(
"no-call-before-approval",
review.invocationCount() === 0,
"Service must not execute before the user decides.",
);
if (decliningConnection) {
await page
.getByRole("button", { name: "Not now", exact: true })
.click();
} else {
const dismiss = page.getByRole("button", {
name: "Dismiss Approve tool action",
});
if (await dismiss.isVisible()) await dismiss.click();
await page
.getByRole("button", { name: "Review request", exact: true })
.click();
await page
.getByRole("button", {
name: declining ? "Decline" : "Approve & run",
exact: true,
})
.click();
}
const decisionStatus = declining ? "rejected" : "accepted";
const decided = await pollUntil({
label: "connection decision persisted",
deadlineAt: Math.min(input.deadlineAt, Date.now() + 30_000),
load: () => api.get<Row[]>(`/api/issues/${parent!.id}/interactions`),
accept: (rows) =>
rows.some(
(interaction) =>
interaction.id === pendingInteraction.id &&
interaction.status === decisionStatus,
),
});
decisionResolvedAt = decided.find((i) => i.id === decisionId)?.resolvedAt;
check(
"user-decision-persisted",
true,
`Interaction ${decisionId} saved as ${decisionStatus}.`,
);
note("connection-decision", {
decision: caseId,
interactionId: pendingInteraction.id,
status: decisionStatus,
});
}
await settled(
caseId === "stop-redirect" ? `Reference ${nonce}`
: caseId === "provider-second" ? `CONTACTS_${nonce}` : undefined,
);
if (caseId === "create-skill-studio") {
const createdSkills = await api.get<Row[]>(
`/api/companies/${fixtures.company.id}/skills`,
);
const created = createdSkills.find(
(skill) => skill.slug === "release-readiness-checklist",
);
check(
"skill-persisted",
Boolean(created && String(created.name) === "release-readiness-checklist"),
"The runner-created skill is present in the company library after the run.",
);
if (created) {
await openParent();
const card = page.getByRole("article", {
name: "Skill created: release-readiness-checklist",
});
await expect(card).toHaveCount(1);
await expect(card).toBeVisible();
await card.getByRole("button").click();
await expect(page.getByRole("heading", { name: "release-readiness-checklist" })).toBeVisible();
await expect(page.getByText("Verify checks.", { exact: true })).toBeVisible();
check("feed-card-opened", true, "The task thread card opened the created skill sidebar.");
const openStudio = page.getByRole("button", { name: "Open in Skill Studio", exact: true });
await expect(openStudio).toBeVisible();
await openStudio.click();
await expect(page).toHaveURL(new RegExp(`/skills/studio/${created.id}$`));
// Studio's skill selector identifies the resource. Headings inside the
// authored document can differ from its canonical skill name.
await expect(page.getByRole("combobox").filter({ hasText: "release-readiness-checklist" })).toBeVisible();
const editor = page.getByRole("textbox", { name: "editable markdown", exact: true });
await editor.click();
await editor.press("ControlOrMeta+End");
await editor.press("Enter");
await editor.press("Enter");
await editor.pressSequentially("Studio edit marker: verified");
await page.getByRole("button", { name: /^Save$/ }).click();
await pollUntil({
label: "Skill Studio edit persisted",
deadlineAt: Math.min(input.deadlineAt, Date.now() + 30_000),
load: () => api.get<Row>(
`/api/companies/${fixtures.company.id}/skills/${encodeURIComponent(String(created.id))}`,
),
accept: (detail) => JSON.stringify(detail).includes("Studio edit marker: verified"),
});
const detail = await api.get<Row>(
`/api/companies/${fixtures.company.id}/skills/${encodeURIComponent(String(created.id))}`,
);
check(
"studio-edit-persisted",
JSON.stringify(detail).includes("Studio edit marker: verified"),
"The Skill Studio edit remains in the persisted skill after returning to the page.",
);
check("studio-opened", true, "The skill detail opened in Skill Studio.");
await page.goBack();
await expect(page).toHaveURL(new RegExp(`/issues/`));
const returnedCard = page.getByRole("article", {
name: "Skill created: release-readiness-checklist",
});
await expect(returnedCard).toBeVisible();
await returnedCard.getByRole("button").click();
await expect(page.getByRole("heading", { name: "release-readiness-checklist" })).toBeVisible();
await expect(page.getByText("Studio edit marker: verified", { exact: true })).toBeVisible();
check("return-content-persisted", true, "Returning to the task shows the saved Skill Studio edit.");
}
}
if (providerChoice) {
const issue = ev.issues.find(i=>i.id===parent!.id)!;
const state = await api.get<{connections:Row[]}>(`/api/companies/${fixtures.company.id}/tools/connections`);
ev.checks.push(...gradeProviderOutcome({rows:issue.interactions as any, decisionId:decisionId!,
selected:caseId === "provider-decline" ? "none" : "via:arcade:hubspot", calls:aggregatorFixture?.invocationCount() ?? 0,
response: (issue.comments ?? []).filter((c:Row)=>c.authorAgentId).map((c:Row)=>c.body).join("\n"), marker:`CONTACTS_${nonce}`,
sameConnections:isDeepStrictEqual(state.connections.map(c=>c.id).sort(), initialConnections.sort()),
}));
}
if (declining) {
const issue = ev.issues.find((i) => i.id === parent!.id)!;
const requests = issue.interactions as Row[];
check(
"decline-not-repeated",
requests.length === 1 &&
requests[0]?.id === decisionId &&
requests[0]?.status === "rejected",
"The saved decline remains rejected and no replacement request appears.",
);
const replies = (issue.comments ?? []).filter(
(c: Row) =>
c.authorAgentId &&
Date.parse(c.createdAt) >= Date.parse(decisionResolvedAt ?? ""),
);
const text = replies.map((c: Row) => String(c.body ?? "")).join("\n");
check(
"decline-visible-explanation",
replies.length > 0 &&
/declin|not now|could(?:n.t| not)|cannot|can.t|unable|not (?:connect|retriev)|without (?:access|connect)/i.test(
text,
),
"A new agent response explains the missing access after the saved decline.",
);
check(
"decline-no-fabricated-result",
!text.includes(`SERVICE_${nonce}`),
"The fallback does not claim the private verification code.",
);
if (decliningConnection) {
const state = await api.get<{ connections: Row[] }>(
`/api/companies/${fixtures.company.id}/tools/connections`,
);
check(
"decline-no-connection-created",
isDeepStrictEqual(
state.connections.map((c) => c.id).sort(),
initialConnections.sort(),
),
"Not now did not create a service connection.",
);
}
}
if (caseId === "build-revise") {
await download(parent!.id, "base", "initial");
await reply(SLUGIFY_REVISION);
note("revision-requested");
await settled();
await download(parent!.id, "separator", "revised");
check(
"new-artifact-revision",
ev.downloads.length === 2 &&
ev.downloads[0]!.sha256 !== ev.downloads[1]!.sha256,
"The follow-up must deliver a new version.",
);
if (ev.downloads[0]) {
const response = await api.request.get(
`/api/attachments/${ev.downloads[0].attachmentId}/content`,
);
check(
"prior-download-preserved",
response.ok() &&
createHash("sha256")
.update(await response.body())
.digest("hex") === ev.downloads[0].sha256,
"The first delivered version remains retrievable.",
);
}
} else if (caseId === "hire-reuse") {
let agents = await api.get<Row[]>(
`/api/companies/${fixtures.company.id}/agents`,
);
const hires = agents.filter((a) => a.name === "Morgan QA");
check(
"exactly-one-hire",
hires.length === 1,
"One Morgan QA must be hired.",
);
check(
"manager-correct",
hires.length === 1 && hires[0]!.reportsTo === fixtures.agent.id,
"The hired agent reports to the lead.",
);
const lead = agents.find((a) => a.id === fixtures.agent.id);
check(
"hire-native-connection",
hires.length === 1 &&
hires[0]!.adapterType === "paperclip_runner" &&
hires[0]!.adapterConfig?.model === lead?.adapterConfig?.model &&
isDeepStrictEqual(
hires[0]!.runtimeConfig?.aiConnection,
fixtures.aiConnection?.binding,
),
"The hire keeps the native model and inherits the managed AI account binding.",
);
const children = ev.issues.filter((i) => i.parentId === parent!.id);
check(
"hired-agent-executed",
hires.length === 1 &&
ev.runs.some(
(r) =>
r.agentId === hires[0]!.id &&
r.status === "succeeded" &&
(r.contextSnapshot?.aiConnection as Row | undefined)
?.connectionId === fixtures.aiConnection?.connectionId,
),
"The new hire must complete a run using the fixture managed account.",
);
await downloadDelegated("base", "hired-delivery");
const reuseRequestedAt = Date.now();
await reply(
`Have the existing Morgan QA add --separator support to the delivered project. Use the same agent; do not hire another. ${SLUGIFY_REVISION}`,
);
note("reuse-requested");
await settled();
agents = await api.get<Row[]>(
`/api/companies/${fixtures.company.id}/agents`,
);
check(
"hire-reused",
agents.filter((a) => a.name === "Morgan QA").length === 1 &&
agents.some((a) => a.id === hires[0]?.id),
"The original hired identity remains unique.",
);
check(
"hired-agent-executed-revision",
ev.runs.some(
(r) =>
r.agentId === hires[0]?.id &&
r.status === "succeeded" &&
Date.parse(r.finishedAt ?? "") >= reuseRequestedAt,
),
"The same hired agent performs the follow-up work.",
);
await downloadDelegated("separator", "reused-delivery", reuseRequestedAt);
} else if (caseId === "delegate-feedback") {
const children = ev.issues.filter((i) => i.parentId === parent!.id);
check(
"one-child",
children.length === 1,
"Exactly one delegated child task.",
);
check(
"child-consumed-feedback",
Boolean(children[0]) &&
storyRepliesConsumed(
ev.runs.filter(
(run) =>
run.contextSnapshot?.issueId === children[0]?.id ||
run.contextSnapshot?.taskId === children[0]?.id,
),
submittedCommentIds,
),
"A completed child execution consumed the delivered user feedback.",
);
if (children[0])
await downloadDelegated(
artifactGradeModeForPhase("delegated-delivery"),
"delegated-delivery",
);
check(
"feedback-delivered-to-child",
Boolean(
children[0]?.comments?.some((c: Row) =>
String(c.body).includes("--max-length"),
),
),
"The child history contains the late requirement.",
);
} else if (caseId === "agent-review-handoff") {
const children = ev.issues.filter((i) => i.parentId === parent!.id);
const child = children.find((candidate) =>
candidate.id === reviewHandoffBoundary?.childId,
);
const interaction = storyAcceptedAgentReview(
child,
reviewHandoffBoundary?.interactionId,
fixtures.agent.id,
ev.runs,
) as Row | undefined;
const reviewRun = interaction?.resolvedByRunId
? ev.runs.find((run) => run.id === interaction.resolvedByRunId)
: undefined;
const parentContinuationRun = ev.runs.find(
(run) =>
run.agentId === fixtures.agent.id &&
run.status === "succeeded" &&
run.id !== reviewHandoffBoundary?.parentRunId &&
(run.nativeIssueId === parent!.id ||
run.contextSnapshot?.issueId === parent!.id ||
run.contextSnapshot?.taskId === parent!.id) &&
interaction?.resolvedAt &&
run.startedAt &&
Date.parse(run.startedAt) >= Date.parse(interaction.resolvedAt),
);
check(
"agent-review-one-child",
children.length === 1 && Boolean(child),
"Exactly one child remains attached to the lead task.",
);
check(
"agent-review-child-completed",
child?.status === "done" && interaction?.status === "accepted",
"The named agent review is accepted and the child reaches Done.",
);
check(
"agent-review-assignee-preserved",
child?.assigneeAgentId === reviewHandoffBoundary?.assigneeAgentId,
"Review resolution preserves the child task assignee.",
);
check(
"agent-review-run-scoped",
reviewRun?.status === "succeeded" &&
reviewRun.agentId === fixtures.agent.id &&
reviewRun.contextSnapshot?.nativeReviewInteractionId ===
interaction?.id &&
typeof reviewRun.contextSnapshot?.nativeReviewDecisionId === "string",
"The lead resolves the review from a successful review-scoped native run.",
);
check(
"agent-review-parent-resumed",
parent?.status === "done" &&
Boolean(parentContinuationRun) &&
Boolean(
parentContinuationRun?.finishedAt &&
parentContinuationRun.startedAt &&
Date.parse(parentContinuationRun.finishedAt) >=
Date.parse(parentContinuationRun.startedAt),
),
"The parent continuation starts after review acceptance and finishes successfully.",
);
note("agent-review-accepted", {
childId: child?.id,
childAssigneeAgentId: child?.assigneeAgentId,
interactionId: interaction?.id,
interactionStatus: interaction?.status,
resolvedByRunId: interaction?.resolvedByRunId,
reviewRunId: reviewRun?.id,
reviewRunAgentId: reviewRun?.agentId,
parentContinuationRunId: parentContinuationRun?.id,
parentContinuationStartedAt: parentContinuationRun?.startedAt,
nativeReviewInteractionId:
reviewRun?.contextSnapshot?.nativeReviewInteractionId,
nativeReviewDecisionId:
reviewRun?.contextSnapshot?.nativeReviewDecisionId,
});
// The review handoff case uses the base slugify requirements. The
// max-length grader belongs to the later follow-up requirement cases.
if (child)
await downloadDelegated(
artifactGradeModeForPhase("reviewed-delivery"),
"reviewed-delivery",
);
} else if (caseId === "recover-controller")
await download(
parent!.id,
artifactGradeModeForPhase("recovered-delivery"),
"recovered-delivery",
);
else if (caseId === "stop-redirect")
check(
"new-direction-delivered",
settledAgentReply?.authorAgentId === fixtures.agent.id &&
String(settledAgentReply.body ?? "").includes(`Reference ${nonce}`),
"The new request is answered after Stop and reload.",
);
if (stoppedWorkspace) {
check(
"stop.workspace-unchanged",
isDeepStrictEqual(stoppedWorkspace, await workspaceFiles()),
"Project files remain unchanged from verified runner stop through the new response.",
);
check(
"stop.no-old-run-active",
ev.runs
.filter((r) => ev.allowedInterruptedRuns.includes(r.id))
.every((r) => !isActiveStoryRun(r)),
"The stopped run is terminal after the new response.",
);
}
if (review) {
check(
"service-call-count",
review.invocationCount() === (caseId === "service-decline" ? 0 : 1),
"Exactly one approved service call; none after decline.",
);
if (caseId === "service-approve") {
const docs = await api.get<Row[]>(
`/api/issues/${parent!.id}/documents`,
);
const bodies = await Promise.all(
docs.map((d) =>
api.get<Row>(
`/api/issues/${parent!.id}/documents/${encodeURIComponent(d.key)}`,
),
),
);
const attachments = await api.get<Row[]>(
`/api/issues/${parent!.id}/attachments`,
);
for (const attachment of attachments.filter(
(a) =>
/\.(?:md|txt)$/i.test(a.originalFilename ?? a.filename ?? "") ||
["text/markdown", "text/plain"].includes(a.contentType),
)) {
const url = `/api/attachments/${attachment.id}/content`;
const response = await api.request.get(url);
if (!response.ok()) continue;
await expect(page.locator(`a[href*="${url}"]`).first()).toBeVisible();
bodies.push({
id: attachment.id,
title: attachment.originalFilename ?? attachment.filename,
body: await response.text(),
source: "delivered-attachment",
});
}
const text = bodies
.map((d) => String(d.body ?? d.revision?.body ?? ""))
.join("\n");
check(
"briefing-uses-real-result",
text.includes(`SERVICE_${nonce}`),
"A delivered issue document or Markdown attachment contains the actual service verification code.",
);
check(
"briefing-includes-page-titles",
text.includes("Roadmap") && text.includes("Meeting notes"),
"The briefing includes both page titles returned by the service.",
);
ev.documents = bodies;
ev.issues.find((i) => i.id === parent!.id)!.documents = bodies;
}
}
await page.reload();
await refresh();
check(
"submitted-replies-consumed",
storyRepliesConsumed(ev.runs, submittedCommentIds),
"Every submitted user message appears in a successfully completed native execution input.",
);
if (
caseId === "delegate-feedback" ||
caseId === "hire-reuse" ||
caseId === "agent-review-handoff"
) {
check(
"parent-finishes-after-child",
storyParentFinishedAfterChildren(
ev.runs,
parent!.id,
fixtures.agent.id,
ev.issues.filter((i) => i.parentId === parent!.id).map((i) => i.id),
),
"The lead completes only after the final child execution.",
);
}
const expectedModel = execution.profile.model;
check(
"native-model-config",
ev.runs.length > 0 &&
ev.runs
.filter((r) => !isStoryWorkspaceDeferral(r))
.every(
(r) =>
(r.runnerProfileJson?.nativeExecutionInput as Row | undefined)
?.provider?.model === expectedModel,
),
"Persisted native execution inputs use the selected model; this does not claim provider-side model identity.",
);
check(
"native-terminal-contract",
ev.runs
.filter(
(r) =>
!isStoryWorkspaceDeferral(r) &&
!isExpectedStoryInterruption(r, ev.allowedInterruptedRuns),
)
.every(
(r) =>
(r.resultJson?.nativeTerminal as Row | undefined)?.schema ===
"paperclip.prp.terminal.v1",
),
"Successful runs retain the native terminal contract.",
);
ev.checks.push(
...storyLifecycleChecks({
issues: ev.issues as StoryIssue[],
runs: ev.runs,
parentId: parent!.id,
leadId: fixtures.agent.id,
allowedInterruptedRuns: ev.allowedInterruptedRuns,
}),
);
check(
"no-pending-bookkeeping",
ev.issues.every((i) =>
(i.interactions ?? []).every((x: Row) => x.status !== "pending"),
),
"No completion confirmation or unanswered interaction remains.",
);
if (providerChoice || nativeProviderCase) await openParent();
else await waitForTaskChatRendered(page, String(parent!.title));
const latestAgentComment = ev.issues
.find((i) => i.id === parent!.id)
?.comments?.filter((c: Row) => c.authorAgentId)
.at(-1);
if (latestAgentComment && !providerChoice && !nativeProviderCase) {
const response = page.locator(`[id="comment-${latestAgentComment.id}"]`);
await expect(response).toBeVisible();
await response.scrollIntoViewIfNeeded();
}
await input.capture(
"final-state",
"Finished everyday workflow",
"final-state.png",
);
const failed = ev.checks.filter((c) => !c.passed);
if (failed.length)
throw new Error(
`Everyday outcome checks failed: ${failed.map((c) => c.id).join(", ")}`,
);
return { issue: parent!, runs: ev.runs, evidence: ev };
} catch (error) {
check(
error instanceof StoryDecisionError
? error.checkId
: "workflow-completed",
false,
error instanceof Error ? error.message : String(error),
);
throw error;
} finally {
try {
await refresh();
ev.agents = await api.get<Row[]>(
`/api/companies/${fixtures.company.id}/agents`,
);
if (ev.documents && parent)
ev.issues.find((i) => i.id === parent!.id)!.documents = ev.documents;
} catch (error) {
note("evidence-capture-error", String(error));
}
if (caseId === "recover-controller") {
note("recovery-final-observation", {
taskStatus: parent?.status,
pendingCommentIds: ev.issues.flatMap((i) =>
(i.queuedComments?.entries ?? []).map((e: Row) => e.comment.id),
),
failureCodes: ev.runs
.filter((r) => r.errorCode)
.map((r) => ({ runId: r.id, code: r.errorCode })),
});
if (execution.environment.id === "local") {
try {
const source = await readFile(
path.join(input.workspacePath, "slugify.py"),
"utf8",
);
await input.evidence("source-after-interruption.json", {
body: source,
sha256: createHash("sha256").update(source).digest("hex"),
});
} catch {
note("saved-source-unavailable-at-final-capture");
}
}
}
if (review) {
note("service-final-observation", {
invocationCount: review.invocationCount(),
requests: review.captures,
decisionTaken: ev.timeline.some(
(entry) => entry.action === "connection-decision",
),
});
}
try {
await input.evidence("everyday-workflow.json", ev);
await input.evidence("api-state.json", {
capturePhase: "everyday-final", issue: parent, runs: ev.runs,
issues: ev.issues, checks: ev.checks,
});
if (aggregatorFixture) await input.evidence("aggregator-provider-calls.json", { calls: aggregatorFixture.captures });
} finally {
try { await aggregatorFixture?.close(); }
finally { await review?.close(); }
}
}
}