Commit Graph
12 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 0ddc9f1ef1 test(chaos): count an M1 subtask stall only when its recovery needs the board (#20178)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The M1 chaos oracle checks that a parent gets exactly one
`issue_blocker_stalled` wake per stalled subtask (#20147)
> - The product wakes a parent only for a stall that needs the board.
The harness recorded a stall for any blocked subtask, whatever its
latest recovery action
> - In the Daytona M1 wave, a subtask blocked twice on agent- and
system-owned recovery that resolved by itself. The harness counted two
stalls and failed the parent for getting no wake, a false failure
> - This pull request records a stall only when the subtask's latest
recovery action needs the board, matching the product's definition
> - The benefit is that `parent_woken_once_per_stall` fails only for a
wake the product owed

## Linked Issues or Issue Description

Refs #20147, #20098

**What happened?**
In wave `w-20261109T041248-s9501`, t10's subtask 195b5fd7 was blocked
twice. Action 56d036e0 was agent-owned (`native_session_interrupted`)
and was cancelled. Action 53eb91b0 was system-owned
(`native_continuation_requires_reconciliation`) and resolved with
outcome cancelled. Neither needed the board, so the product correctly
sent no stall wake. The harness still failed
`parent_woken_once_per_stall` with 2 stalls and 0 wakes.

**Expected behavior**
A stall counts when the subtask's latest recovery action is:
- open (active or escalated) and board-owned, or escalated; or
- board-owned and resolved with outcome blocked, with the subtask still
blocked.

That is the definition the product's blocker-stall wake uses.

## What Changed

- `observeChildStalls` reads the parent's `stalledDependencies` from
`GET /api/issues/:id`. That is the product's own stalled-dependency view
(`listStalledDependencies`, built with the same `readStalls` the
blocker-stall wake uses).
- Each `relation: "child"` entry is recorded as a stall
(`stalledIssueId`, `recoveryAction.id`). The harness no longer infers
stalls from `/recovery-actions`. That endpoint lists only open actions
and selected holds, and can omit a board action resolved with outcome
blocked.
- Clicking the board Retry on a blocked subtask is unchanged: it still
happens whenever the product offers Retry, and it still counts as a
needed Retry.
- Tests:
  - a stall the product lists is recorded;
- for t10's two non-board recovery states, the product lists nothing, so
no stall is recorded, and the subtask's Retry is still clicked.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 212 passed.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- The harness checks `stalledDependencies` every 10 s. A stall that
appears and clears inside one interval is not recorded, so its wake goes
unchecked. It is never reported as a false failure.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:36:40 -06:00
DottaandClaude Opus 5.5 61fa13c6d8 test(chaos): add S1, a board Stop across a restart then a continue comment (#20161)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness drives real user actions across
control-plane restarts: comments, Retry, approvals and budget resumes
> - The board's Stop button on a running task was not exercised. The
cancel route stamps the stop as operator-initiated so that recovery
stands down instead of re-waking the agent
> - A restart right after a Stop is where recovery could wrongly restart
work the operator stopped, or lose the operator's later "continue"
direction
> - This pull request adds S1: stop the running run, fire a fault right
after, then comment to continue
> - The benefit is a graded check that a Stop holds across a restart and
that a later continue comment finishes the task

## Linked Issues or Issue Description

Refs #20071

**What is missing?**
The campaign's scenario list includes mid-run stops. C1 covers a mid-run
comment (a steer), but no workload clicks Stop. Without one, a
regression where recovery restarts operator-stopped work, or drops the
continue comment, would go unseen.

**Expected behavior**
- After the board stops a run and a restart follows, the stopped run
stays cancelled.
- Nothing runs until the operator comments.
- The continue comment then finishes the task.

## What Changed

- S1 workload ("Stop while running, then continue"): a brief task. At
its follow-up time, while a run is running, the observer posts `POST
/api/heartbeat-runs/:id/cancel` (the board's Stop) and records `stop`.
- New `stop` trigger, for S1 only: the planned fault fires 0–2 s after
the stop.
- The continue comment is posted only after the stop has settled: at
least 20 s after the stop, with no run running or queued and no fault in
progress. It asks the agent to continue and add a `CONTINUE-<nonce>`
line.
- The oracle requires:
  - status done;
  - the brief with the chaos and continue markers;
  - `stop_took_effect`: the stopped run ended `cancelled`;
- `nothing_ran_after_stop`: no run was created between the stop and the
continue comment.
- S1 is not a default workload. There is a README section.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 205 passed. New tests
cover:
- the S1 grade (pass, a run restarted after the stop, the stop not
taking effect, no stop);
  - the trigger-to-workload pairing;
- an observer test that clicks Stop once and posts the continue comment
only after the 20 s settle.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- A live S1 wave runs after the current chaos wave. Its result will be
posted here before merge.

## Risks

- If the product moves the stopped run to a status other than
`cancelled`, `stop_took_effect` fails. That check is intentional.
- The continue comment waits for the stop to settle, so a run stuck
`queued` delays it. R5 and the wave deadline still bound the wait.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:52:48 -06:00
DottaandClaude Opus 5.5 0502cc9fac test(chaos): retry a blocked M1 subtask and check the parent's stall wake (#20147)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades the M1 scenario: a parent task
delegates two subtasks across restarts
> - When a subtask's run failed recovery, the harness only clicked Retry
on the parent. The parent's Retry is correctly held behind the blocked
subtask (#20109), so the task waited until its deadline
> - A real board user would also click the subtask's Retry. #20098 also
added a wake for a parent whose subtask stalls, and the grade did not
check it
> - This pull request clicks the board Retry on a blocked subtask
(graded as a needed Retry) and checks that the parent got exactly one
stall wake per stall
> - The benefit is that M1 grades the full user path and proves the
stall wake from the product's own activity record

## Linked Issues or Issue Description

Refs #20098, #20109, #20094

**What happened?**
In wave `w-20261109T003126-s9701`, the M1 parent t05 rested blocked
behind a blocked codex subtask. The harness clicked Retry on the parent.
The product held that Retry ("waiting on a blocked task") as designed.
Nothing retried the subtask, and the task waited 38 minutes until the
wave deadline. The wave also could not show whether the parent got the
new `issue_blocker_stalled` wake.

**Expected behavior**
The harness acts like the board: it clicks Retry on a blocked subtask
that offers one, and it counts that as a needed Retry. The M1 oracle
checks the parent's activity for one `issue.blocker_stalled_wake` per
stalled subtask.

## What Changed

- `observer.ts`:
- For M1, the observer checks the parent's subtasks every 10 s while the
parent is open, independent of the parent's rest window.
- Each blocked subtask is recorded as a stall: the subtask plus its
latest recovery action, from `GET /api/issues/:id/recovery-actions`.
- When a blocked subtask's interruption offers Retry, the harness posts
the board Retry. It clicks once per subtask; a click that was not
accepted can be tried again on a later check.
- The parent's `issue.blocker_stalled_wake` activity entries are
collected as `stallWakes` through `GET /api/issues/:id/activity`.
- `grader.ts`: R3 fails with `retry_needed_on_subtask_<n> (<ids>)` when
a subtask needed a board Retry, unless the parent's own Retry already
failed R3.
- `workloads.ts`: the new M1 check `parent_woken_once_per_stall` is
added when a subtask stalled. Each (subtask, recovery action) stall must
have exactly one matching wake. A stall with an unknown action matches
any wake for that subtask.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 203 passed. New tests
cover:
- an observer test: a blocked subtask gets one Retry click, and the
parent is still watched;
  - the R3 subtask-retry grade;
- the stall-wake check: woken once, not woken, woken twice, two stalls
of one subtask with only one woken, and no stall.
- G1, which also has subtasks, is unaffected: the existing governance
tests pass.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- The harness now acts on subtasks, which it did not before. It clicks
once per subtask and only when the product offers Retry.
- The stall-wake check reads activity through the board API. If the API
omits these entries for a board user, the check fails visibly instead of
passing.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:12:03 -06:00
DottaandClaude Opus 5.5 89265f7312 test(chaos): add R1, a code change in a fresh clone of a real repository (#20111)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness proves that real agent tasks survive
control-plane restarts
> - Its coding workloads run in a tiny chaoscli fixture. Real work
happens in large repositories, where workspace setup, copy and restore
cost more and fail differently
> - The campaign asks for real coding tasks in a fresh Paperclip clone,
run under restarts
> - This pull request adds the smallest support for that: a
`--workspace-repo` option and an R1 workload
> - The benefit is restart coverage for code changes in a real
repository, graded by running the agent's own test and a hidden case

## Linked Issues or Issue Description

Refs #20071, #20089

**What is missing?**
The harness can only seed the generated chaoscli fixture as the project
workspace. There is no way to run restart chaos against a code change in
a real repository.

**Expected behavior**
An operator can name a git repository. The harness clones it fresh as
the workspace, and a workload asks for a real code change in it with an
oracle that runs the result.

## What Changed

- `--workspace-repo <git repository>`: `seedChaosLab` clones it
(shallow, from committed HEAD, `--no-hardlinks`) as the project
workspace instead of the chaoscli fixture. Local only.
- R1 workload: the agent adds
`packages/shared/src/chaos/slug-<nonce>.ts` with `chaosSlug` and a
`node:test` file, and runs it. The nonce names keep concurrent tasks
from colliding in one repository.
- The R1 oracle runs the agent's test with `node --test` and a hidden
case against the module through Node's type stripping.
- Collect commands can take an `argv` function of the nonce.
- Config rules: R1 and `--workspace-repo` go together. B1 and B5 (which
need the chaoscli fixture) do not combine with it. R1 is not a default
workload.
- README section "Code change in a real repository".

## Verification

- `npx vitest run` in `tests/restart-chaos`: 195 passed, including new
tests for the config pairing, R1 grading with the nonce argv, and a
shallow clone of a fixture repository.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.
- I checked the oracle commands by hand on Node 26: `node --test` on a
`.ts` test and `node --input-type=module -e 'import … from "./…ts"'`
both work.
- A live R1 wave against a Paperclip clone runs after the current chaos
wave frees the harness slot. Its result will be posted here before
merge.

## Risks

- A shallow clone of a large repository adds seconds to wave seeding.
That is one clone per wave.
- The oracle uses Node's built-in type stripping. A runtime without it
fails `node_test` and `hidden_case`, which shows as an oracle failure,
not a false pass.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 16:55:02 -06:00
DottaandClaude Opus 5.5 88835e624a test(chaos): fail M1 fast on a blocked subtask and grade interrupted waves (#20094)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness proves that real agent tasks survive
control-plane restarts
> - In a K4/K6 wave with the M1 multi-agent scenario, one subtask
failed. Its parent waited on it, and the harness kept watching for over
50 minutes
> - When the operator stopped the wave, the harness wrote no grade
files, so the 17 finished tasks had no grade
> - Separately, master's harness test for time to first activity failed
after #20077 changed how time is measured from run creation
> - This pull request makes M1 fail fast on a blocked subtask, grades
the finished tasks of an interrupted wave, and aligns the speed grade
with #20077
> - The benefit is shorter waves with a grade for every task, and a
green harness suite on master

## Linked Issues or Issue Description

Refs #20071, #20077, #20088

**What happened?**
Wave `w-20261108T181313-s9701` (local, codex and claude, A1/C1/M1, K4
and K6) ran 71 minutes against a planned 30. Task t05 (M1) delegated
CHA-8 and CHA-9. The CHA-8 run failed after a K4 restart (the defect
that #20088 fixes), and CHA-8 went to `blocked`. The observer kept
waiting for CHA-8 to finish. After SIGINT, the harness threw before
grading, so no task had a grade file. Also, `wave-speed.test.ts` fails
on master: #20077 changed `runMetrics` to measure from the start of
`heartbeat.queue`.

**Steps to reproduce**
1. Run an M1 task whose subtask run fails and leaves the subtask
`blocked`.
2. See the observer wait until the parent deadline.
3. Send SIGINT to the harness and see that no grade files are written.

**Expected behavior**
A blocked subtask ends the wait, and the parent is graded as a failure
with a `task_child_blocked` timeline entry. An interrupted wave grades
every finished task and fails the wave.

## What Changed

- `observer.ts`: `childrenPending` treats a `blocked` subtask as
settled, so the parent rests and is graded. The timeline records
`task_child_blocked` with the subtask ids.
- `observer.ts`: `markUnfinished` closes an open task at interrupt as
unfinished (`timedOut`, last seen status) and records
`task_unfinished_at_interrupt`.
- `main.ts`: an interrupt now sets the harness error and grades the
wave, instead of throwing before the grade. The wave still fails.
- `wave-speed.ts`: `firstActivityMs` uses
`runMetrics().firstActivityAfterRunCreatedMs` from #20077, so there is
one definition of time from run creation.
- `wave-speed.test.ts`: the fixture starts `heartbeat.queue` when the
comment wait ends, as in real runs.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 172 passed. Before this
change, the wave-speed test failed on master.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- A subtask that is `blocked` only for a short time and then resumes
would end the parent's watch after the 60-second grace. The grade then
shows a failure that a longer wait could avoid. M1 subtasks have no
blockers, so `blocked` means a stuck subtask.
- An interrupted wave grades tasks while its server is still up. If the
server is already down, artifact collection fails, and those tasks are
graded as failures.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:20:57 -06:00
DottaandClaude Opus 5.5 c3c226d165 test(restart-chaos): governance (approval, budget hard stop) and upgrade mid-run scenarios (#20089)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness kills and restarts the control plane
during real agent tasks and grades every task
> - It covers run lifecycle, comments, delegation and long tools, but
not two areas that must also survive a restart: governance (approvals
and budget hard stops) and an upgrade in the middle of a run
> - An approval that disappears, runs twice or runs after a rejection,
or a budget pause that dispatches work, breaks governance. An upgrade
that cannot resume saved state breaks the upgrade-safe-saved-state rule
> - This pull request adds the harness support for both scenarios, using
only the paths a real board user and agent use
> - The benefit is that the next chaos waves can test these boundaries
and report failures with evidence

## Linked Issues or Issue Description

Refs #20084, #20067

**What happened?**
The restart chaos harness had no scenario for an approval-gated action
pending across a restart, for a budget hard stop crossed mid-run with a
restart, or for a server upgrade in the middle of a run. These
boundaries were untested.

**Expected behavior**
- **G1:** a pending approval survives K1, K2 or K4. Approving it after
the restart runs the gated action exactly once; rejecting it never runs
it.
- **H1:** a budget hard stop crossed mid-run pauses the company, nothing
dispatches while paused (also across a restart), and resuming continues
the work.
- **K14:** a wave started on an older build and upgraded mid-turn
resumes old-saved state on the new build with no Retry, no lost comments
and no stuck records.

**Steps to reproduce**
1. Run the harness commands in `tests/restart-chaos/README.md`
("Governance across restarts" and "Upgrade mid-run").
2. Each task is graded R1–R6 plus its oracle.

**Paperclip version or commit**
master at 4f7eee10c2.

## What Changed

- **G1 workload: approval across a restart.**
- The agent requests board approval through its `call_api` tool (`POST
/api/companies/{companyId}/approvals`, `request_board_approval`, linked
to its task), blocks, and ends its turn.
- A new `approval` trigger fires the fault once the observer sees the
approval pending.
- After the restart, with the server ready, the observer acts as the
board through `POST /api/approvals/:id/approve` or `/reject` (rejection
also cancels the task). The decision is fixed by the nonce.
- The oracle checks one request, still pending when decided after the
restart, the decision, and the gated subtask exactly once on approval
and never after rejection.
- R1 and R3 grade a rejected task against `cancelled`
(`expectedFinalStatus`).
- **H1 workload: budget hard stop across a restart.**
- Before launch the observer sets the company budget to observed spend
plus one cent through `PATCH /api/companies/:id/budgets`.
- A new `paused` trigger fires the fault once the company shows `paused`
/ `budget`.
- After the restart the pause must hold 30 s with the server ready. The
observer records runs that started while paused, then resolves the
incident with `raise_budget_and_resume`.
- The oracle checks the brief, the pause, no dispatch while paused, the
resume after the fault, and an active company at rest.
- H1 runs alone, at concurrency 1, and not with `--rc06-masked` (config
errors).
- **K14 fault: upgrade mid-run, with `--upgrade-from <older
checkout>`.**
  - The server starts from the older checkout.
- The first K14 stops it with SIGTERM and starts this checkout on the
same database, instance directory, port and secrets. The runner binary,
pinned Codex and source dependencies are prepared per checkout.
  - The receipt names both commits.
- K14 is local-only and needs `--upgrade-from` (and the reverse). It is
excluded from storm defaults. Preflight refuses an older checkout that
is not prepared.
- G1 and H1 are opt-in: they are not in the default workloads.
- README sections for both scenarios.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
every new and changed test passes; 188 of 189 pass in total. The one
failure, `wave-speed.test.ts` "judges time to first agent activity…",
also fails on master: #20077 gave the local TTFT a budget after #20071's
test expected none. This PR does not touch `wave-speed` or the speed
budgets. The new `governance.test.ts` covers:
- the board decides only after the fault the pending approval armed,
with the server ready, for approve and reject;
  - a blocked task waiting on its approval is not counted as resting;
- G1 and H1 oracle pass and fail cases (duplicate request, action twice
or after rejection, decision or resume that crossed no restart, dispatch
while paused);
- H1 budget set, pause hold and incident resolve, against a fake board
API;
- trigger gating, opt-in workloads, H1 exclusivity, and K14 option
pairing.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passed.
- Not run yet: a live wave of G1, H1 or K14. Phase 3 holds the harness
slot, and the smoke runs are requested there. Their results will be
added here.

## Risks

- Test harness only; no product code changes.
- G1 relies on the agent following the instructions (request, block, act
only when approved). A task where the agent does not follow them fails
the oracle visibly.
- K14 seeds the company with this checkout's harness code while the
older server serves. This works for recent builds; a much older build
could reject a seed call.
- Live behavior is not proven until the smoke runs.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, test execution, a read-only research subagent).

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:40:39 -06:00
DottaandClaude Opus 5.5 a65626a2a2 test(chaos): fail waves on a needed Retry and on speed regressions (outage removed); add M1 multi-agent workload (#20071)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (tests/restart-chaos) kills and restarts
the control plane during real agent tasks, and grades every task on
reliability rules R1–R6 plus a task oracle
> - Dotta's bar for today's campaign: restarts must continue on their
own (Retry is only a fallback), the system must stay fast (speed budgets
in `native-speed-budgets.ts`), and realistic scenarios must be covered
> - The harness graded a task that needed a user Retry as a pass, graded
speed only in crash-point mode, and counted a K4 controller hold as slow
TTFT. It also had no multi-agent scenario
> - This pull request closes those four gaps
> - The benefit is that every chaos wave now fails on a needed Retry and
on a real speed regression, but not on the fault's own outage

## Linked Issues or Issue Description

Refs #19831 (durable ownership plan) and the restart chaos plan
(`doc/plans/2026-11-06-control-plane-restart-chaos.md`). Used by today's
campaign waves:
- w-20261108T134237-s9401 re-graded to 104/107 under the Retry rule;
- w-20261108T153612-s9401: 100/101, with a Speed section showing 6
breaches after outage removal.

## What Changed

- **R3 Retry rule** (`grader.ts`): a task that needed a Retry fails R3
as `retry_needed (<failed runs' error codes>)`, and a second click fails
as `retry_needed_<n>_clicks`. The harness still clicks Retry so the task
can finish and be graded.
- **Per-wave speed grade** (new `wave-speed.ts`, wired in `main.ts`):
- It reads every task run's run log through the API and reports TTFT
(from run created) and agent boot p50/p95 per adapter and start kind,
restart recovery (ready → the run's next provider event), and single-run
`run.performance.budget_exceeded` events, against the local budgets.
  - A breach fails the wave.
- Output goes to `speed.json` and a Speed section in `SUMMARY.md`.
Remote environments report numbers with no budget.
- **Outage removal**: each performance span is shortened by its overlap
with fault outage windows (fault start to replacement server ready)
before it is judged. The summary reports how many spans and how much
time were removed.
- **M1 workload** (multi-agent delegation):
- The parent task creates two subtasks, part 1 delegated to the other
Chaos Lab agent.
- Each subtask saves a marked document, and the parent writes a summary
of both before it completes.
- The observer collects the subtasks and their documents, and the oracle
checks the parent links, assignees, statuses and markers.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
14 files, 163 tests pass. New tests cover the M1 grade, the R3 Retry
rule (one click and two clicks), the wave speed grade (budget judgement,
restart recovery, remote no-budget, fault→run attribution) and outage
removal.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Real waves:
  - The R3 rule re-graded w-20261108T134237-s9401 to 104/107.
- w-20261108T153612-s9401 produced the Speed section: 923 spans
adjusted, 2,452 s of outage removed, 6 breaches.
- M1 has not yet run in a live wave. It's next in the campaign rotation.

## Risks

- Waves that passed before can now fail: on a needed Retry, or on a
speed breach. That is the intended bar.
- Speed is judged at wave concurrency on a shared host, which is noisier
than the idle baseline the budgets came from. Breaches are reported per
row, so a reviewer can see the margin.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:27:39 -06:00
DottaandClaude Opus 5.5 051834bdc8 feat(chaos): add the on-demand crash-point suite and its manual CI workflow (#19946)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A control-plane restart between two durable writes leaves an
in-progress record owned by a dead process
> - Random restart-chaos waves found these 20 ms–1 s windows only by
luck
> - #19937 added `crashPoint(name)`, and #19938 put it at 20 durable
write boundaries
> - Nothing yet runs a task, kills the server at a chosen point,
restarts it, and checks the plan's invariants
> - This pull request adds that on-demand suite, as a local command and
as a manual CI workflow (not a required check, per Decision 2)
> - The benefit is a repeatable work list: which boundary breaks which
invariant, for Phases 2–4

## Linked Issues or Issue Description

Refs #19831 (plan, Phase 5 and Decision 2). Refs #19861 (report-only
reconciler used for I1–I7). Stacked on #19938 (call sites), which is
stacked on #19937 (helper).

**Problem**
The named crash points cannot be exercised end to end. No command arms
one point, runs real agent tasks across it, restarts, and grades the
result.

**Expected behavior**
`node scripts/crash-points.mjs --points <glob> --kinds <id> --adapters
<list>` runs one wave per selected point and reports each point as pass,
fail (with failing R rules and violated invariants), not reached, or
harness error. A `workflow_dispatch` workflow runs the same thing on
demand.

## What Changed

- **Runtime arming.** `PAPERCLIP_CRASH_POINT_CONTROL=<file>`, in the
shared helper and the runner mirror. Each hit reads `<token>
<name>[:<nth>]`, and a new token resets the count. It is test mode only:
`NODE_ENV=production` ignores it, and the disarmed path is still one
comparison.
- **`tests/restart-chaos/main.ts --crash-point
<a>:<nth>,<b>:<nth>,...`** (config, types, ledger):
  - one booted server and no Vite UI;
- per point: take an invariant baseline, launch the point's batch, arm
the point, then on rest disarm it and check invariants as violations
that are new since the baseline;
  - every task is graded R1–R6 and T;
  - `crash-point.json` (v2) holds one entry per point.
- **`tests/restart-chaos/supervisor.ts`:**
  - every spawn gets the control file;
- a SIGKILL whose receipt names the child's pid is restarted like K1:
the control file is emptied first, the owned tree is killed, and a
replacement starts. A failed tree cleanup fails the fault;
- the isolated instance pins `PAPERCLIP_IN_WORKTREE=false` (a
`.paperclip/.env` above the checkout otherwise suppressed all
scheduling);
  - `RESTART_CHAOS_HEALTH_TIMEOUT_MS`.
- **`tests/restart-chaos/invariants.ts`:**
- the Phase 1 report-only pass with `record: false`: two real-clock
passes for I1 and I2, and one pass 15 minutes ahead for I3, I4 and I7;
  - spool kinds are read from the wave's instance;
  - an unfinished pass is a harness error;
  - each violation carries its wake and run state as evidence.
- **`tests/restart-chaos/crash-points.ts` and
`scripts/crash-points.mjs`:**
- select points by glob, kind and environment, and run them in one wave;
  - a per-point outcome: pass, fail, not_reached or harness_error;
  - `SUMMARY.md` with a violation evidence column.
- **`.github/workflows/crash-points.yml`**, manual only, under the
paid-workflow contract:
  - a credential-free `authorize` job;
  - a `suite` job in `runner-e2e-paid` whose first step reauthorizes;
  - a frozen install with `--ignore-scripts` and no shared cache;
  - only the selected adapters' keys;
  - the workflow is added to both security-test allowlists.
- **Docs:** "Crash-point suite" in `tests/restart-chaos/README.md`, and
"Built (Phase 5)" plus the first work list in the plan.

## Verification

- Focused tests:
- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts
tests/restart-chaos/crash-points.test.ts`;
- `cd packages/shared && npx vitest run src/crash-points.test.ts`: 15
tests, including runtime arming in a real child process;
- `node --test scripts/__tests__/release-verify-workflow.test.mjs`: 72
tests;
  - `tests/runner-e2e/workflow-security.test.ts`: 19 tests.
- Typecheck: `node scripts/typecheck-test-projects.mjs
tests/restart-chaos` passes.
- Loop line: question "how long is one crash-point loop?" | loop: `node
scripts/crash-points.mjs --points <one>` (codex A1) | elapsed 2.7 min
end to end: boot 9 s, crash 9 s after arming, replacement ready in 6 s,
rest about 2 min | result: one point alone takes about 3 min. Before
this change it took about 15 min (one cold boot per point, Vite UI, host
at load 77).
- Full local run, 14 points, codex A1, 11.8 min in all:
- every point was reached (the server killed itself at each one, and the
replacement came back);
  - 11 pass;
- `workspace.finalize.after_start_before_copyback` and
`workspace.finalize.after_copyback_before_done` fail I2 and R6: a
`workspace_finalize` operation stays running after the run succeeded;
- `wake.claim.after_run_running_before_wakeup_claimed` reports I4. Its
evidence shows the wake failed with "…retrying once", and the retry
delivered the work, so this is the known refusal-regex false positive.
- The 6 Daytona-only points were not run.

## Risks

- Test harness and an on-demand workflow only. No product code changes.
- `.github/**` needs a CODEOWNER review. The workflow uses provider
keys, so it refuses non-default branches and actors that are not on the
allowlist, like `runner-full-stack-e2e.yml`.
- `PAPERCLIP_IN_WORKTREE=false` is pinned in every chaos wave, not only
crash-point waves. A chaos instance is never a worktree dev instance, so
scheduling must not be suppressed.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits).

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:41:15 -06:00
DottaandClaude Opus 5.5 644ee66f5d test(restart-chaos): R5 live tools and processes; fail a wave whose worktree changes mid-wave (#19697)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness (`tests/restart-chaos/`) grades every task
on R1–R6; R5 catches silent stalls, including a run that stays `running`
with no live evidence
> - A B5 task runs `bench.sh` for about four minutes with no output, so
a healthy Retry run was graded `R5:silent_dead_run` while its runner
process was alive and its tool was still running
> - Separately, a runner `dist` rebuilt in the harness worktree during a
wave made later restarts run different code and produced a false
signature (RC-32's investigation)
> - This pull request makes R5 treat a run as live while a tool it
started has not ended or while its recorded local runner process still
runs, and fails a wave whose worktree changes mid-wave
> - The benefit is fewer false R5 failures and no silently mixed-code
waves

## Linked Issues or Issue Description

Refs #19629
Refs #19641

**What is the problem?**
R5 flags a quiet but live run (a long tool call, a live runner process)
as `silent_dead_run`. A build, install or branch switch in the harness
worktree during a wave goes unnoticed, though every restart re-runs the
control plane from that worktree.

**What is the expected behavior?**
A run with a tool in flight, or with its recorded local runner process
still alive (same PID and start time), is live for R5. A wave whose
checkout or runner build output changes during the wave fails as a
harness error.

**Why does it matter?**
False R5 failures send fixers after non-bugs, and mixed-code waves
produce signatures that are not product behavior.

## What Changed

- `observer.ts`: the run-event reader counts tool starts minus tool ends
(PRP item ids are not unique per tool call), and remembers the run's
latest local process identity until recovery reports it stopped. Samples
carry `toolInFlight` and `processAlive`. `localProcessAlive` checks `ps`
for the same PID with the same start time, so a recycled PID never
counts. Daytona runs are not checked (their process is remote).
- `grader.ts`: `deadRunningRun` skips a run with a tool in flight or a
live process.
- `types.ts`: `RunSample.toolInFlight` and `RunSample.processAlive`.
- `wave-guard.ts` (new) and `main.ts`: fingerprint `HEAD`, the tracked
diff contents (`git diff --binary HEAD`), untracked file contents, and
size/mtime of `packages/paperclip-runner/dist` and the runnerd binary.
The baseline is taken after the harness's own setup (preflight rebuilds
provider entrypoints). Re-check after each fault and at the end; fail
the wave (`worktree_changed_mid_wave`) if it changed.
- `main.ts`, `scheduler.ts`: `faults.json` and a new `task-faults.json`
(each task's launch time and crossing faults) are written after every
fault. `--replay` falls back to that log when a task has no grade file
(an interrupted or wedged wave), so a storm task replays the faults that
crossed it at the same offsets.
- `README.md`: both grading notes.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
111 tests pass. New tests cover:
- a B5 run quiet for 4 minutes passes with a tool in flight or a live
process, and fails otherwise
- tool and process liveness come from run events and clear on tool end
and process stop
- `localProcessAlive` accepts this process's real PID and start time and
rejects a different start time
- the guard catches rebuilt dist, a re-edited dirty file, an edited
untracked file and a rebuilt runner binary, in a real temporary git repo
- a storm task with no grade file replays the faults that crossed it at
their offsets from its launch
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passes.

## Risks

- Low risk, test harness only.
- The R5 exemption narrows R5. A run that is truly stuck while its
process lives (a hung provider) now passes R5 and falls to R1's time
budget instead.
- One `ps` call per running run per poll on local waves.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude
Code with tool use (shell, file edits).

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:07:42 -06:00
DottaandClaude Opus 5.5 d26372dc8a test(restart-chaos): add a result trigger and recovered-run comment-wake guards (#19689)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness kills the control plane at chosen moments
and grades whether each task still finishes correctly
> - One wave lost a user's follow-up comment after a kill that landed
after the run accepted its result but before the run settled
> - No existing trigger aims a fault at that moment, so the case was
hard to reproduce on purpose
> - The recovery path that settles such a run had no regression test
that covers the saved comment
> - This pull request adds a `result` trigger to the harness and a
regression test for the recovery path
> - The benefit is that the harness can aim faults at the
result-accepted window, and a change that drops, repeats, or re-wakes a
saved comment fails a test

## Linked Issues or Issue Description

**What happened?**
In a restart-chaos storm wave, a user posted a comment while a native
run was active. A hard kill (K1) landed after the run accepted its
result and before the run settled. The run settled `succeeded` and the
task stayed `done`, but no follow-up run read the comment.

**Steps to reproduce**
The original failure did not reproduce. Three targeted waves ran 12
tasks with K1 at comment time or in the new result window. Every
follow-up comment was consumed exactly once. The new regression test
also passes on current `master`. The test and the trigger guard the path
and do not claim a fix.

**Expected behavior**
A comment posted during a run is consumed exactly once after the run
ends, including when restart recovery settles the run.

## What Changed

- `tests/restart-chaos`: add `--triggers result` (C1 only). K1 fires 0–3
s after the run that received the follow-up proposes or accepts its
result (`run.result.proposed` or `run.result.accepted`). The fault fires
only while that run is still `running`: the observer checks the latest
sample and rereads the runs right before dispatch. A run that settled
during the delay, or a follow-up posted at rest, records a `result`
miss. Add poll-driven unit tests for these cases and a README row.
- `server/src/__tests__/native-run-projection-work.test.ts`: add
embedded Postgres tests in which background recovery, not the executor,
settles the run (projection, environment settlement, and
deferred-receipt workers), with a deferred user comment and an active
local lease left by the killed executor. The tests check:
- The comment is promoted into exactly one follow-up run, and a second
restart adds none.
- A comment the run already received as a same-turn steer is not woken
again. The other saved comment still is.
- A queue whose only comment was steered stays cancelled, and no
follow-up starts.
- A Retry click does not consume the comment a second time. A click that
observed the source is stale. A click that observed the follow-up joins
it.

## Verification

- `cd tests/restart-chaos && npx vitest run --config vitest.config.ts`
(96 tests pass)
- `cd server && npx vitest run
src/__tests__/native-run-projection-work.test.ts` (54 tests pass)
- Live, local, ACPX Claude, Codex and OpenCode: `node
cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts --env
local --workloads C1 --triggers comment --faults K1 --adapters
codex,claude,opencode --tasks 4 --concurrency 2 --rc06-masked` passed 4
of 4. Two waves with `--triggers result` consumed each follow-up exactly
once. One task in those waves found a separate defect: a kill 1.4 s
after the result commit left the run `running` behind the dead
controller's 20-minute finalization lease. A separate pull request fixes
that defect.

## Risks

- Low risk. These are test and harness changes only. No production code
changes.
- The `result` trigger depends on the observer's poll interval (about 3
s), so the exact kill point within the window varies.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, run as an agent in
Claude Code with tool use (shell, file editing, local test execution).

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:53:58 -06:00
DottaandClaude Opus 5.5 e8f07b2a39 test(restart-chaos): fire faults relative to user actions and run state, add K6 crash loop (#19513)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The control plane must keep tasks moving when the server restarts
> - The restart chaos harness (#19477) kills the server only at a random
time after a task starts
> - Many restart bugs depend on what the user or the run was doing at
that moment: a comment just sent, a Retry just clicked, a run still
queued, a long tool call, or a second crash during startup recovery
> - The harness cannot aim at these moments yet, so these cases occur
only by chance
> - This pull request adds fault triggers that fire relative to a user
action or a run state, and the K6 crash loop
> - The benefit is that chaos waves can target these boundaries on
purpose and replay them

## Linked Issues or Issue Description

Refs #19477

**What is the problem?**
The restart chaos harness fires each fault at a seeded random offset
from task launch. It cannot fire a fault right after the harness posts a
comment to an active run, right after a Retry click, while a run is
queued, during a long tool call, or a second time while the server is in
the `recovering` startup phase.

**Why is it a problem?**
These are the boundaries where restart bugs hide (plan faults K6 and K8,
boundaries B1, B6 and B8). Random timing seldom hits them, and a replay
cannot repeat them.

**What is the proposed solution?**
Add a `--triggers` option. Each task samples a trigger with its cell.
The observer arms the fault when the trigger condition holds, then fires
it after a seeded delay. Add the K6 crash-loop fault to the supervisor.

## What Changed

- `types.ts`, `config.ts`: add `TRIGGER_IDS` (`time`, `comment`,
`retry`, `queued`, `tool`), the `--triggers` option, and the `K6` fault
kind.
- `scheduler.ts`: sample a trigger per task. `comment` pairs only with
C1 and `tool` only with B5. Each trigger has a seeded delay range.
`retry` adds an extra K1 after the planned fault. A replay re-arms the
task's own triggered faults.
- `observer.ts`: arm each triggered fault from the observed state: the
follow-up reached a running run, an accepted Retry, the first run still
queued, or the first `bench.sh` progress. A trigger that can no longer
hold is recorded as `triggerMissed`.
- `supervisor.ts`: K6 kills the replacement again as soon as
`/api/ready` reports `recovering` (or `ready`, if recovery was too fast
to see), then once more just after `ready`. The receipt records the
phase that each kill hit.
- `main.ts`, `ledger.ts`: pass the trigger to the fault receipt, the
timeline and the ledger. The summary shows the trigger in each cell.
- `triggers.test.ts`: unit tests for planning, firing, misses, replay
and the crash-loop phase rule.
- `README.md`: document the triggers and K6.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`
(52 tests pass).
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos` passes.
- Live: `node cli/node_modules/tsx/dist/cli.mjs
tests/restart-chaos/main.ts --env local --adapters codex,claude,opencode
--faults K1,K6 --triggers comment,retry,queued,tool --concurrency 3
--tasks 6` runs waves with the new triggers.

## Risks

- Low risk. The change is in the test harness only. The default trigger
is `time`, so existing waves and replays keep their behavior.
- A trigger can miss its window (for example, a run starts before the
first poll). The ledger records the miss, so the task is not counted as
a crossing of that boundary.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits).

## 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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:49:52 -06:00
DottaandClaude Opus 5.5 871a9b3180 test(restart-chaos): add Phase 1 control-plane restart chaos harness (#19477)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The control plane owns task execution, recovery and Retry for every
agent run
> - Server restarts were the largest remaining source of stalled tasks
in the staging soak campaign
> - The soak could only observe restarts that staging happened to
perform, so a failure could not be repeated
> - This pull request adds a script harness that causes the restarts on
purpose and grades each task on fixed rules
> - The benefit is a repeatable restart failure with a replay command
and an evidence bundle for each fixer

## Linked Issues or Issue Description

**What existing behavior does this improve?**
The restart test coverage. Today the runner-e2e restart cells test one
fixed restart point per cell. They cannot run many tasks at once, cannot
choose a random fault time, and cannot replay a failure.

**Subsystem affected**
Test tooling (`tests/restart-chaos/`). It reuses the runner-e2e process
code in `tests/runner-e2e/`. Product code does not change.

**Current behavior**
Restart robustness is checked through a small set of fixed cells. A
random restart during a real task has no defined pass rule.

**Proposed behavior**
A harness starts an isolated control plane and seeds a "Chaos Lab"
company. It launches seeded workloads, injects K1, K2 or K4 faults at
seeded times, and grades each task on rules R1 to R6 plus a
deterministic task oracle. `--replay <wave>/<task>` repeats one task.

**Reason and benefit**
Fixers get a replay command and an evidence bundle for each failure
signature. The first smoke wave already found three product signatures
(dead Retry after a restart, a false over-budget stop after restarts,
and an OpenCode turn killed mid-turn that ends as a provider failure).

**Breaking changes**
None. The change adds new test files, one entry in the test-project
typecheck list and a gitignore entry.

## What Changed

- Add `tests/restart-chaos/`:
- `supervisor.ts` owns one isolated server. It reuses
`replaceController`, the process-tree stopper, the environment isolation
helpers, the config seed, the port reservation, the wrapper-owned
Postgres and `server-entry.ts` from `tests/runner-e2e/`. It does not
copy them. Faults: K1 (SIGKILL, immediate restart), K2 (SIGTERM,
restart) and K4 (SIGKILL, 2 to 5 minute hold). Postgres and the
controller runtime cache stay across a fault.
- `seed.ts` creates the company, encrypted provider secrets, native
Codex, ACPX Claude and OpenCode agents from the runner-e2e catalog, and
the `chaos-sandbox` project. The project is a git repository with a
Python CLI, one failing test and a four-minute `bench.sh`.
- `scheduler.ts` makes a seeded plan of workload, adapter, fault, fire
time, hold and nonce for each task. It saves the plan so that `--replay`
can run one task again.
- `observer.ts` polls each issue and its runs. When a task stops and
offers Retry, it sends `POST /api/issues/:id/retry` one time, with the
same body as `useTaskRetry.ts`.
- `grader.ts` grades R1 to R6 as pure functions. `workloads.ts` defines
workloads A1, B1, B5 and C1 and their oracles.
- `evidence.ts` and `ledger.ts` write an evidence bundle for each
failure, a JSONL ledger and `SUMMARY.md`. All results go to the
gitignored `tests/restart-chaos/results/`. Known provider key values are
redacted.
- Unit tests: `scheduler.test.ts` (determinism, replay and CLI parsing)
and `grader.test.ts` (R1 to R6 and the oracles on fixture timelines).
These tests need no credentials.
- Add `tests/restart-chaos` to the test-project typecheck list
(`scripts/typecheck-test-projects.mjs`), with a `tsconfig.layers.json`
like `tests/runner-e2e`. The harness runs through direct script
invocations, not `package.json` scripts.
- After the first Greptile review:
- A failed fault or an unexpected server exit now gets one bounded
recovery. If the recovery fails, the wave fails and does not hang.
- Incomplete cleanup is now a harness error, and the harness keeps the
instance.
- After the C1 follow-up comment, the observer needs a fresh rest window
before it grades the task.
- `--replay` now fires the recorded faults that crossed the task, at the
same offsets, including the faults that other tasks requested.
- A failed workspace-operation read is now a sweep error. It cannot give
a clean rest.
- R2 now checks that workspace files from before the first fault still
exist at rest.
- B5 now needs the completion record that `bench.sh` writes for each
task.
- After the second round of reviews:
- An idle `todo` or `in_progress` task with no live run and no named
wait is no longer treated as resting. The observer keeps watching it
until the task resumes or passes the R5 stall limit.
  - A failed injected fault now fails the wave with exit 2.
- T-oracle commands run agent-editable code. They now run under a macOS
Seatbelt sandbox with a scratch `HOME` and no network, and they cannot
read the operator's home directory. On other platforms these commands
are refused unless the operator explicitly allows them.
- R4 now counts every active run. A wake comment may be served again
only after a run that did not succeed.
- Evidence redaction now covers every environment variable name that
ends in `KEY`, `TOKEN`, `SECRET` or `PASSWORD`.
- If the pre-fault snapshot fails, only R2 for that task is left
unchecked. The wave continues.
- B5 also needs bench progress that the harness observed while it
polled.
- Add `doc/plans/2026-11-06-control-plane-restart-chaos.md` and its
campaign ledger.

## Verification

- `pnpm exec vitest run --config tests/restart-chaos/vitest.config.ts`:
38 tests pass. The tests include observer tests with a fake API, a real
`bench.sh` run, and a sandbox test that cannot read a canary secret.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`:
passes.
- `server/scripts/typecheck-layers.test.mjs`: 45 tests pass, including
"covers every project under tests/".
- A supervisor check with no credentials passed. K1 was ready again in
about 6 seconds. K2 with a 3-second hold was ready again in about 9
seconds. Company data stayed readable.
- One paid smoke wave ran: `--env local --adapters codex,claude,opencode
--workloads A1,B1,C1 --faults K1 --tasks 3 --concurrency 2 --seed 7`.
The harness worked from start to end, and all three tasks crossed a
restart. The 0/3 result comes from product failures, which the campaign
ledger records. A scan found no provider key in the results.
- To run a wave:

  ```sh
  pnpm --filter @paperclipai/paperclip-runner build:runner-binaries
  set -a; source ~/.secrets; set +a
node cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts --env
local --adapters codex,claude,opencode --concurrency 4 --tasks 8
--faults K1,K2,K4 --seed 42
node cli/node_modules/tsx/dist/cli.mjs tests/restart-chaos/main.ts
--replay <wave>/<task>
  ```

## Risks

- Low product risk. The change adds test tooling only and does not
change product code.
- A wave spends provider money. The company budget hard cap
(`--budget-cents`, default 3000) is the only spend limit. The plan's
per-wave and per-night refusal is not implemented yet.
- Known gaps:
- `--env daytona` is reserved. It fails with a TODO in `seed.ts`. The
Daytona sandbox sweep for R6 is not implemented yet.
- The project requests isolated git-worktree workspaces. A B1 task still
ran in the shared fixture repository. Concurrent B1 and B5 tasks can
share files. The oracles use marker files for each task.
- Only K1, K2 and K4 at seeded random times are available. Boundary
triggers (B1 to B16), other faults and the UI probe come in Phase 2.
  - The paid smoke did not exercise B5 or K4.
- On macOS, B1 oracle commands need `python3` outside the operator's
home directory, for example `/usr/bin` or Homebrew. On Linux they are
refused until a sandbox is added.
- The B5 proof is best-effort against a deliberately adversarial agent,
because that agent can write to the workspace.
- Startup phase timings come from polling `/api/ready`. The server
records its own timings only as OpenTelemetry spans.
- `pnpm verify` and the CI Vitest jobs do not select these unit tests.
The root Vitest projects cover workspace packages only. CI typechecks
the files, but the tests run only through the vitest command above.
- After the smoke, rest detection and the review fixes changed. A
recovery action that the board owns now counts as a rest, and R6 reports
it. A paid wave did not run again after these changes.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, used in
Claude Code as an agent with tool use (shell, file edits) and extended
thinking.

## 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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:45:52 -06:00