Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1a81c20
Check the planning agent at startup and before planning requests (#164)
mchwang Oct 21, 2026
ce80552
Give the browser stand-in a passing check; test the check against the…
mchwang Oct 21, 2026
330a01b
Start the check at once, bound the wait, recheck before the start (#164)
mchwang Oct 21, 2026
5af6cd2
Start no check inside the Store's transaction; cancel replaced checks…
mchwang Oct 21, 2026
cd918e6
Test the stop after close and the single wait deadline; refusal wordi…
mchwang Oct 21, 2026
e4f844f
Merge remote-tracking branch 'origin/main' into fix/164-planning-probe
mchwang Oct 21, 2026
a58a096
Put the check's reason last and whole; keep waiting past a replaced c…
mchwang Oct 21, 2026
8975b29
Promise only the recheck that happens; wording and test margins (#164)
mchwang Oct 21, 2026
02e32b9
Move a waiting request to a newer check at once; deferred check never…
mchwang Oct 21, 2026
d2b4a94
Add a live Claude probe of the planning check; detach waits; wording …
mchwang Oct 21, 2026
64f99bb
Merge remote-tracking branch 'origin/main' into fix/164-planning-probe
mchwang Oct 21, 2026
50b4017
Say when planning recovery runs; no listener warning for many waiters…
mchwang Oct 21, 2026
e237aab
Reject from the wrapped invoke; say recovery needs a sign-in; wording…
mchwang Oct 21, 2026
44c3fae
Exact wording on the planning lock, the image build and the verdict (…
mchwang Oct 21, 2026
8f08e4a
First job is always a check; exact shutdown wait; live probe checks D…
mchwang Oct 21, 2026
86dcf82
Bound each check; no recheck promise for a restart; exact claims (#164)
mchwang Oct 21, 2026
40de60f
A stopped wait is a refusal; no recheck promise without a sign-in; te…
mchwang Oct 21, 2026
97da5c5
Restart cases from codeboost's own text; close waits for timed-out pr…
mchwang Oct 21, 2026
82238e4
Restart wording for unsettled cleanup; stopping and Codex wording; li…
mchwang Oct 21, 2026
7bfe421
Restart wording only for codeboost's restart-only reasons; stop on ab…
mchwang Oct 21, 2026
db7f48c
Codex wording names the next request; exact lock and wait wording (#164)
mchwang Oct 21, 2026
5792621
Exact check answer, repeated keys refused; test names and real cleanu…
mchwang Oct 21, 2026
d50bbbe
Cite follow-up #198 for the review-view state and server-side limits …
mchwang Oct 21, 2026
de7c8f5
JSON whitespace only in the check answer; exact wording and stub note…
mchwang Oct 21, 2026
ccf6bd1
Say when a changed agent is being checked; exact recovery and shutdow…
mchwang Oct 21, 2026
8b223d7
Restart wording says fix first; shutdown timing; drop a dead flag (#164)
mchwang Oct 21, 2026
1f6d9d6
Restart wording fits every restart-only reason; comment fixes (#164)
mchwang Oct 21, 2026
71d4ca7
Restart-only reasons tested from their sources; a cleared agent ends …
mchwang Oct 21, 2026
2b824fa
Comment fixes; drop an abort check that cannot fire (#164)
mchwang Oct 21, 2026
857e57d
Codex's stand-in check fails at once, as the real one does (#164)
mchwang Oct 21, 2026
5c67707
Reasons in the check's tests come from the code that gives them (#164)
mchwang Oct 21, 2026
0847c47
Merge branch 'main' of https://github.com/codeabovelab/codeboost into…
mchwang Oct 21, 2026
e0e46cb
Real sign-in and Codex reasons in the check's tests; doc and layout (…
mchwang Oct 21, 2026
4cc95fb
Test the wrapped invoke forwards after a pass; wording (#164)
mchwang Oct 21, 2026
885ccb7
Pin the check's default wait, timeout and close bound (#164)
mchwang Oct 21, 2026
c5711fc
Admit nothing after close, even under a passed check (#164)
mchwang Oct 21, 2026
683c44d
Admit nothing after close with no agent chosen either (#164)
mchwang Oct 21, 2026
162f43c
A request waiting at close is refused as stopping; close tests (#164)
mchwang Oct 21, 2026
42af3bf
Close wakes a waiting request at once; docs and a still-running test …
mchwang Oct 21, 2026
6ab8f8b
Rewrap start's doc (#164)
mchwang Oct 21, 2026
0472de6
Comment: the AbortSignal also holds one listener per waiting request …
mchwang Oct 21, 2026
6888587
Rewrap a doc paragraph; drop a repeated issue tag from a test title (…
mchwang Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/live-probes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
env:
CODEBOOST_RUN_AUTH_PROBES: '1'
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
run: npx vitest run --project docker -t '\(live Claude\)$' --reporter=default --reporter=json --outputFile=live-probes.json test/agent-container.test.ts test/agent-question.test.ts test/agent-supervisor.test.ts
run: npx vitest run --project docker -t '\(live Claude\)$' --reporter=default --reporter=json --outputFile=live-probes.json test/agent-container.test.ts test/agent-question.test.ts test/agent-supervisor.test.ts test/agent-planning.test.ts
# Vitest exits 0 when it skips every probe, so check that each probe passed and nothing else ran.
- name: Check that every probe passed
run: node scripts/live-probes.ts check live-probes.json
2 changes: 1 addition & 1 deletion docs/implementation/agent-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Each row is a T9 requirement for the Docker suite. The suite fails if any row fa
| Task and scratch byte and inode limits hold | `agent-container`: task capacity; scratch capacity for Codex and Claude (`/tmp`, `HOME`, `CODEX_HOME`, output directory) |
| Hard links and alias writes from `.git/config` and objects fail, and metadata stays unchanged | `agent-container`: metadata alias probe in planning, review and execute, with a digest of `.git` before and after |
| Mountpoint replacement fails | `agent-container`: metadata and metadata alias probes (`mv` and `rm -rf` of `.git`) |
| The Claude startup probes read the schema and return bounded valid output through their documented channel | `agent-supervisor` live probes (credentials required): Claude in questions through its stdout envelope; Claude in planning returns a schema-constrained answer as bare JSON; `agent-container`: the authenticated Claude startup path. Codex has no live probe while it is refused (#93). |
| The Claude startup probes read the schema and return bounded valid output through their documented channel | `agent-supervisor` live probes (credentials required): Claude in questions through its stdout envelope; Claude in planning returns a schema-constrained answer as bare JSON; `agent-planning`: the planning check (#164) passes with a real sign-in; `agent-container`: the authenticated Claude startup path. Codex has no live probe while it is refused (#93). |
| Nothing can be written beneath a gitlink, and execute and fix launch only on a clean tree (#81) | `agent-container`: the gitlink probe (writes, `mkdir`, `rmdir` and `mv` at every gitlink fail; the rest of `/work` stays writable); a pre-populated nested checkout, a restart with an untracked symlink parent and a restart with an occupied add destination are refused; a gitlink name Docker cannot mount, more than 256 gitlinks and an unreadable tree are refused; each check serves one profile over its own storage and head; the validator rejects a gitlink mount that is missing, writable, or of another size or mode; every directory above a nested gitlink is pinned, writable and `nosuid,nodev`, so renaming one fails (#99); the validator rejects a pin that is missing, read-only, at another subpath, from another volume, `volume-nocopy`, or not a volume |
| Hostile input stays inside the boundary | `agent-container`: repositories with links that leave the checkout are refused, links inside the checkout still work, oversized repositories fail closed; `agent-policy`: option-like prompts; `agent-proxy`: hostile CONNECT traffic; `agent-supervisor`: hostile output |

Expand Down
7 changes: 4 additions & 3 deletions docs/implementation/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ settings are not stored in the repository.
| --- | --- | --- | --- |
| `.github/workflows/ci.yml` | `test` | every pull request; pushes to `main` and `codex/**` | Type check; every top-level `test/*.test.ts` file except the Docker-backed files; the Playwright browser suite |
| `.github/workflows/agent-isolation.yml` | `real-docker` | every pull request; pushes to `main` and `codex/**` | Type check; the complete Vitest suite against real Docker |
| `.github/workflows/live-probes.yml` | `live-probes` | started by hand (`workflow_dispatch`); every night at 06:23 UTC | The four live Claude startup probes only |
| `.github/workflows/live-probes.yml` | `live-probes` | started by hand (`workflow_dispatch`); every night at 06:23 UTC | The five live Claude startup probes only |

`test` and `real-docker` have no path filters. They run on every pull request,
so both can be required checks. A required check that a path filter skips
Expand Down Expand Up @@ -112,8 +112,9 @@ The job:
missing. A missing secret never gives a passing run.
2. Runs the Docker Vitest project with `CODEBOOST_RUN_AUTH_PROBES=1`. It selects
only the tests whose titles end in `(live Claude)`, in
`test/agent-container.test.ts`, `test/agent-question.test.ts` and
`test/agent-supervisor.test.ts`. The files run one at a time.
`test/agent-container.test.ts`, `test/agent-question.test.ts`,
`test/agent-supervisor.test.ts` and `test/agent-planning.test.ts` (the
planning check, #164). The files run one at a time.
3. Runs `node scripts/live-probes.ts check` on the Vitest JSON report. Vitest
exits with success when it skips every test, so this step fails unless each
probe listed in `scripts/live-probes.ts` passed and no other test ran.
Expand Down
2 changes: 1 addition & 1 deletion docs/implementation/plan-library-alignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Issue #6 tracked the v1 plan library contract and the runner obligations that ca
| Output limits at the exact byte | **This change** (tests; the supervisor already enforced the limits) | Supervisor enforcement: `test/agent-supervisor.test.ts` (real Docker). Decoder behaviour only, not a limit: the Claude decoder handles an envelope of exactly the stdout ceiling, and the 1 MiB parser limit applies to the decoded answer at the exact byte: `test/agent-adapter.test.ts`. At #6 the decoder re-serialized the answer, so the limit applied to the re-encoded text. *Later:* #166 returns the exact text of `structured_output` in the CLI's envelope, so the limit applies to those bytes |
| Prompt byte budget before spawn | Done before this change | `MAX_PROMPT_BYTES` in `core/planning-author.ts`; `test/planning-author.test.ts` ("accepts exactly the prompt byte limit and rejects the next byte without truncation") |
| Model budget before spawn | Moved to #163. *Later:* done in #163 | `MAX_PLANNING_INPUT_BYTES` and `PLANNING_MAX_TURNS` in `agents/policy.ts`; `test/agent-policy.test.ts` ("accepts a planning request at exactly its model budget and refuses one byte more"); [agent isolation](agent-isolation.md#planning-model-budget) |
| Planning profile probed at startup | Moved to #164 | — |
| Planning profile probed at startup | Moved to #164. *Later:* fixed in #164 (the review-view state is #198), checked at startup, on the first request after the vendor changes and on the first request a minute after a failure | `runner/planning.ts` (`PlanningAgent.probe`, `PlanningCheck`), `web/planning.ts`; `test/planning-production.test.ts` ("the planning check (#164)"), `test/planning-worker.test.ts`, `test/agent-planning.test.ts` (live Claude probe); `docs/implementation/planning-acceptance.md` |
| Docker daemon log limits | Moved to #165. *Later:* fixed in #165 with `--log-driver=none`, checked before launch | `agents/container/profile.ts`, `agents/container/run.ts`; `test/agent-container.test.ts` ("creates the agent container with no Docker daemon log", "refuses an agent container with %s at validation") (real Docker), `test/agent-daemon-log.test.ts`. Agent containers only; the runner's other containers (task-storage scripts, the keeper, the egress proxy) keep the daemon's default log |
| Claude structured output under the v1 parsing contract | Moved to #166 (found during this change). *Later:* fixed in #166 | `agents/adapters/claude.ts`; `test/agent-adapter.test.ts` ("lets draft validation refuse %s in a structured answer") |

Expand Down
53 changes: 53 additions & 0 deletions docs/implementation/planning-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,59 @@ suggestion timer and the request share one ten-minute budget. The recording
script still runs the provider in its own process and prints its runner label if
it leaves anything.

The planning check (#164) proves that the vendor chosen in Settings can run a
planning invocation before any request does. It is one planning job in the same
worker and profile as a real request (read, list and search only, vendor-only
egress, a mounted schema), on the review's code at the snapshot's head, with a
real request's budget, since a process's first job may build the agent image. It
asks for the schema's own answer, `{"ok": true}`, so it covers sign-in, the
schema-constrained answer and the read-only profile (`PlanningAgent.probe`).
`PlanningCheck` starts it when the server starts, before it listens, and again
on the first request after the vendor changes, cancelling the check it replaces.
`productionPlanning`'s `describe` waits for it, at most 30 s in all, before it
reads the issue or records a planning request, and the request checks again just
before it starts, so a vendor chosen during the issue read never runs unchecked.
That last check runs inside the Store's transaction, so it starts no check
itself: it refuses, and the check it needs starts on the next turn. The provider
checks once more when it is invoked, as defence in depth (E3 invokes it in a
microtask right after the recorded action); a refusal there settles the request
as failed. A failed check refuses the request with a `GuardRefusal` that starts
"Planning is unavailable:" and ends with the reason exactly as given (some end
with a command to copy), and a check still running after the wait is refused
with the same prefix; the server records the refusal and starts no agent.
Requests within a minute of a failure are refused with it; the next request
after that checks again. With no vendor chosen nothing is checked, and the
request fails later, as before, with `NO_PLANNING_AGENT`. Once planning requests
have settled, or after the server's 20 s grace for them, shutdown cancels a
running check and waits up to 20 s more for it before closing the worker. A
check that runs before the runner's setup has set the runner-owned repository
fails, on a task with runner commits, with "which is not configured"; the
recheck a minute later passes once the runner's setup has finished, and without
a runner block it fails each time, as every planning request would. A check that
has not settled within a real request's budget and a minute fails, so the
recheck applies to it. A reason that holds until codeboost restarts (an
abandoned worker, cleanup codeboost could not confirm, a database with other
hard links, a missing token) gets no promise of a recheck, and Codex is told to
choose another agent. A recheck joins the worker's one startup recovery, so a
recovery that never ends fails each recheck until codeboost restarts. A passed
check is kept until a check for another vendor replaces it: a worker abandoned
later, cleanup that did not settle (the check's own run included, when its
answer still came back), or a token revoked later, shows as a failed request. A
refusal recorded when a request stopped waiting, or when the Store could not be
read during the wait, is replayed if the browser resends the same action;
keeping a Store error out of the record needs `web/server.ts`. Showing the
check's state in the review view before a request, and the other limits that
need `web/server.ts`, are #198. A check deferred from the Store's transaction
can start during shutdown, before planning closes; close then waits for it
within its bound. If startup fails after planning's setup, the CLI exits, and
the next start's planning recovery removes what the check left. Tests:
`test/planning-production.test.ts` ("the planning check (#164)", with a check
that passes, one that fails and one that does not finish),
`test/planning-worker.test.ts` (the check in planning's worker, including a
wrong answer) and `test/agent-planning.test.ts` (against the vendor, real
Docker: an invalid sign-in fails, and a live probe in the Live probes workflow
passes with a real one).

The Store guarantees revision/snapshot binding and pending-only settlement across
processes, and E3 requires the caller's snapshot identity before admission. G4 now
composes the browser with production planning setup, lane D's provider boundary,
Expand Down
4 changes: 2 additions & 2 deletions docs/implementation/runner-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,14 +361,14 @@ F1 owns the production planning endpoints that G4 needs. They wrap E3's coordina
| `POST /api/plan/import` | `Store.importRevision` | expected revision; body of at most 6 MiB + 16 KiB, so every source the 1 MiB parser accepts fits (see `docs/plan-format.md`, #146) |
| `POST /api/plan/suggestions` | E3 coordinator start (`beginSuggestions`) | expected revision and snapshot |
| `GET /api/plan/suggestions/:id` | `Store.getSuggestions`, with the request's `mode` | reads only the request row; refuses a draft's ID ("Unknown suggestion request.") |
| `POST /api/plan/suggestions/:id/cancel` | `pending`: `handle.cancel(reason)` on the `SuggestionHandle` that `SuggestionCoordinator.start()` returned. E3's coordinator has no cancel-by-ID method, so F keeps a map from request ID to handle, adds each handle when it starts the request, and removes it only after the handle's outcome settles. If the request is `pending` but F holds no handle, F calls `Store.cancelSuggestions`. After a restart this no longer arises while planning is on (a non-demo review with a `github` block): startup fails every request an earlier process left `pending` (#124). Without planning, such a request stays `pending` until it is cancelled here. It is safe to do so because a crashed process's planning container has no route back to the Store: its output went to that process's worker, which ended with it. A live planning provider runs only through D (#117), in planning's own worker, in containers labelled with this database's planning owner (not the runner's or Ask's). Like Ask's, those containers are removed by D's `recoverLeftovers` for that owner before the first planning request of the next process, not at startup; until then a stray container can keep running read-only (its deadline is enforced from the host, which is gone) until Claude exits or that recovery removes it, and its output is discarded. If a planning provider ever runs outside D, or could report into the Store from another process, this endpoint must refuse such a request instead, until it has been reconciled. `ready`: `Store.cancelSuggestions` (dismiss), because no invocation is running. | request is `pending` or `ready`, as `Store.cancelSuggestions` already allows |
| `POST /api/plan/suggestions/:id/cancel` | `pending`: `handle.cancel(reason)` on the `SuggestionHandle` that `SuggestionCoordinator.start()` returned. E3's coordinator has no cancel-by-ID method, so F keeps a map from request ID to handle, adds each handle when it starts the request, and removes it only after the handle's outcome settles. If the request is `pending` but F holds no handle, F calls `Store.cancelSuggestions`. After a restart this no longer arises while planning is on (a non-demo review with a `github` block): startup fails every request an earlier process left `pending` (#124). Without planning, such a request stays `pending` until it is cancelled here. It is safe to do so because a crashed process's planning container has no route back to the Store: its output went to that process's worker, which ended with it. A live planning provider runs only through D (#117), in planning's own worker, in containers labelled with this database's planning owner (not the runner's or Ask's). Like Ask's, those containers are removed by D's `recoverLeftovers` for that owner in the next process's first planning job that can sign in: the planning check at startup when Claude is chosen and `CLAUDE_CODE_OAUTH_TOKEN` is set, unless that check fails before its recovery runs (for example, on a task with runner commits before the runner's setup has set the runner-owned repository, see `planning-acceptance.md`; or when planning's leftover record refuses it, or another process holds planning's lock) (#164, which also takes planning's lock from then until planning closes at shutdown, or until the process exits after an abandoned worker or a recovery stopped part way, and builds the agent image in planning's worker while the runner, if one is configured, builds its own), otherwise the first check a planning request starts (a check whose recovery fails leaves it to the next one); until then a stray container can keep running read-only (its deadline is enforced from the host, which is gone) until Claude exits or that recovery removes it, and its output is discarded. If a planning provider ever runs outside D, or could report into the Store from another process, this endpoint must refuse such a request instead, until it has been reconciled. `ready`: `Store.cancelSuggestions` (dismiss), because no invocation is running. | request is `pending` or `ready`, as `Store.cancelSuggestions` already allows |
| `POST /api/plan/suggestions/:id/apply` | `Store.applySuggestion` | request `ready` and bound to the current revision and snapshot |
| `POST /api/plan/drafts` | E3 coordinator start in draft mode (`beginSuggestions(…, 'draft')`, #124): a whole next revision of the current plan. The request lane D sees carries revision n+1, the revision the draft would become; the Store binds it to n | expected revision and snapshot; one planning invocation per plan at a time in this process, shared with suggestions (several drafts may be ready at once) |
| `GET /api/plan/drafts/:id` | `Store.getDraft` | reads only a draft request's row |
| `POST /api/plan/drafts/:id/cancel` | as suggestion cancel | request is a draft; a `pending` or `ready` one is cancelled, any other state is returned unchanged |
| `POST /api/plan/drafts/:id/apply` | `Store.applyDraft`: imports the draft as revision + 1 with every import check | request is a draft, `ready` and bound to the current revision and snapshot |

Live planning runs for a non-demo review with a `github` block (#117): `web/planning.ts` reads the issue from GitHub before each start, and `runner/planning.ts` runs the request in planning's worker. E3's suggestion timer and the request share one ten-minute budget (`PLANNING_BUDGET_MS`). At shutdown E3 aborts every request; one that has not settled within 20 seconds (`PLANNING_SHUTDOWN_GRACE_MS`, as Ask) is ended by abandoning planning's worker. A start replays its recorded outcome before reading GitHub; a GitHub failure is a 502 and is recorded for replay like a refusal (`web/server.ts`; `test/planning-production.test.ts`). Without a `github` block, or in a demo, the server returns "Planning agent not available yet". Drafts follow the same rules (#124). A draft and a suggestion are both rows of `requests`, told apart by `mode` (schema v11); each kind's routes refuse the other's IDs. At startup, once the single-runner lock is verified to name the database (`web/planning.ts`), every request an earlier process left `pending` is failed ("The server stopped before this request finished. Ask again."): its provider ran in that process's planning worker, so nothing can complete it.
Live planning runs for a non-demo review with a `github` block (#117): `web/planning.ts` reads the issue from GitHub before each start, and `runner/planning.ts` runs the request in planning's worker. E3's suggestion timer and the request share one ten-minute budget (`PLANNING_BUDGET_MS`). At shutdown E3 aborts every request; one that has not settled within 20 seconds (`PLANNING_SHUTDOWN_GRACE_MS`, as Ask) is ended by abandoning planning's worker, after the planning check (#164) has had up to 20 seconds more to stop. A start replays its recorded outcome before reading GitHub; a GitHub failure is a 502 and is recorded for replay like a refusal (`web/server.ts`; `test/planning-production.test.ts`). Without a `github` block, or in a demo, the server returns "Planning agent not available yet". Drafts follow the same rules (#124). A draft and a suggestion are both rows of `requests`, told apart by `mode` (schema v11); each kind's routes refuse the other's IDs. At startup, once the single-runner lock is verified to name the database (`web/planning.ts`), every request an earlier process left `pending` is failed ("The server stopped before this request finished. Ask again."): its provider ran in that process's planning worker, so nothing can complete it.

## Feedback-event contract (for lane J)

Expand Down
Loading
Loading