Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 .claude/skills/codetrial-improve/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ None of this is a gate; each piece names what it can and cannot see.
| `scripts/agent-failure-loop.py` (PostToolUse) | Prompts for the shared premise on the second identical gate failure | Know the premise, or block |
| `scripts/test.sh` receipts | Records lane, verdict, skipped count and the fingerprint of the tree as the run started, under the git directory | Prove the run was complete, or stop an agent with a shell from writing one; CI recomputes the verdict |
| `scripts/agent-closeout-check.py` (Stop) | Tells the user once per tree when it has changes the gate has not passed on in every lane (`all`, or rust, portable and browser) | Block, or see a gate run elsewhere |
| `scripts/ceiling-hotspots.py` | Ranks the files recorded as exceptions to the size limit by commits in a window times lines | Say a file is worth splitting, or whether its tests cover it; it is one input |
| `scripts/ceiling-hotspots.py` | Ranks the Rust files over the default size limit by commits in a window times lines | Say a file is worth splitting, or whether its tests cover it; it is one input |
| pre-push note | Notes `src/` changes with no `tests/` change | Judge whether a test was needed |

## Local evidence at closeout
Expand Down
40 changes: 25 additions & 15 deletions .claude/skills/codetrial-improve/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,27 @@ through `clippy.toml`.

## File size

New Rust files stay within 1500 lines. Existing larger files are recorded in
`scripts/rust-style-ceilings.json` and may only shrink: after splitting one by
responsibility, run `scripts/check-rust-style.py --ratchet` to lower its entry.
`scripts/ceiling-hotspots.py` ranks them by churn times size, as one input to
choosing which to split first. Count a file's mutants before splitting it with
`cargo mutants --list -f <file> | wc -l`; `make mutants` downloads a compiler
first. Never raise an entry to land a change; a new module is the answer. The
gate passes `--baseline origin/main`, so a raised entry or a new exemption fails
against what main records, and under CI a baseline ref that was not fetched
fails too, so a shallow checkout cannot skip it. Test bodies live under
`tests/unit/` and answer to the same ceiling.
New Rust files stay within 1500 lines. By default, the checker derives each
limit from the committed file at the same path in HEAD, with a floor of 1500;
working files may shrink freely but larger files may not grow. Without Git
every file gets 1500 and the gate's baseline cannot be read, so a source
archive fails the gate; run it from a clone.
`scripts/ceiling-hotspots.py` ranks the larger files by churn times size, as
one input to choosing which to split first. Count a file's mutants before
splitting it with `cargo mutants --list -f <file> | wc -l`; `make mutants`
downloads a compiler first.

The gate passes `--baseline origin/main`. For a larger file present in both
that baseline and HEAD, its limit is the smallest size across those two trees
and every reachable committed version at that path in `origin/main..HEAD`,
with a floor of 1500. All reachable commits count, including merged history.
A committed shrink automatically ratchets the limit; a new or renamed path
gets 1500, and deletion retires the allowance. Split a large file by
responsibility rather than grow it. An explicit baseline requires resolvable
HEAD and baseline refs, common history and a complete, non-shallow
repository; an error fails locally and in CI. Diverged refs are supported,
but the branch must still satisfy any smaller baseline limit. Test bodies
live under `tests/unit/` and answer to the same ceiling.

### Splitting a file

Expand Down Expand Up @@ -106,9 +116,9 @@ What splitting `src/agent/prompts.rs` (3944 lines, six modules under
fn name`) fails with "is still defined here" in `cargo test --lib` and not in
clippy, and a source digest that hashes the old file (the report evaluation's
identity) stops covering the code that left it.
7. **Run `scripts/check-rust-style.py --ratchet`** so the old entry leaves
`scripts/rust-style-ceilings.json`, then clippy at `-D warnings`, then
`./scripts/test.sh rust`.
7. **Commit the split, then run clippy** at `-D warnings` and
`./scripts/test.sh rust`. The old file's limit drops to its committed size
on its own; there is no table to update.

A comment added to a file that sits exactly at its ceiling fails the checker, so
a note for a ceilinged file goes here or in the split, not in the file.
Expand Down Expand Up @@ -188,7 +198,7 @@ in [recipes/rust.md](recipes/rust.md).
cargo fmt
cargo clippy --locked --workspace --all-targets
cargo doc --locked --workspace --no-deps
python3 scripts/check-rust-style.py
python3 scripts/check-rust-style.py --baseline origin/main
python3 scripts/check-rust-style.py --workspace
cargo test --locked <one filter>
```
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/codetrial-review/rules/paths
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ src/gemini* room.md
src/gemini* agent.md
src/dispatch.rs room.md
src/main.rs room.md
src/launch.rs room.md
src/config* room.md
src/web/setup.rs room.md
src/web/token.rs room.md
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ windows-permissions = "0.2.4"
# Independent implementations verify production signatures and digest fixtures.
hmac = "0.13.0"
sha2 = "0.11.1"
# Borrowed socket inspection for real loopback transport fixtures.
socket2 = "0.6.5"
livekit-protocol = "0.8.2"
# LiveKit's transport, reached directly to check the vendored proxy path.
livekit-net = "0.1.4"
# The paused clock, so the Live socket's keepalive and idle limits are tested
# at their real values instead of waiting them out.
tokio = { version = "1.54.1", features = ["test-util"] }
# at their real values instead of waiting them out; signals, so the serving
# fixture can treat TERM as a stop request.
tokio = { version = "1.54.1", features = ["signal", "test-util"] }
# The continuity fixture installs a logger to read the SDK's queue-overflow
# line; the SDK already depends on this crate.
log = "0.4.34"
Expand Down
9 changes: 5 additions & 4 deletions docs/desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ server in: it runs the `codetrial` executable that ships beside it as a child
process, and its window shows the page that child serves on
`http://127.0.0.1:3101`.

A child process rather than a library call, because the server's startup lives
in `src/main.rs` (config search, the Setup handover, every refusal it makes
before serving) and moving that into the library would have been a rewrite of
the file the CLI's behavior is pinned to. As a child it is the binary the CLI
A child process rather than a library call, because the server's startup is
the binary's: `src/main.rs` runs the Setup handover and binds the socket, and
builds the server from `src/launch.rs` (config search, every refusal it makes
before serving). Calling that from the app would have meant a second launcher
beside the one the CLI's behavior is pinned to. As a child it is the binary the CLI
tests already cover, started with five things a launcher needs that the CLI
did not have. Each is decided in the server, so the app keeps no copy of the
server's rules:
Expand Down
55 changes: 49 additions & 6 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,21 @@ locks held across await and `mem::forget` are explicit denials too.
`unsafe` count at zero — `forbid` rather than `deny`, so a local `#[allow]`
cannot reintroduce it. `scripts/check-rust-style.py` holds what a lint cannot
ask: that this table stays strict, that no `allow` or bare `#[ignore]` appears,
that no marker or CJK text lands in Rust or `web/`, and that no file passes the
line ceiling in `scripts/rust-style-ceilings.json`, which `--ratchet` only
lowers. The rules and the lints left off on purpose are in the Rust profile of
codetrial-improve, `.claude/skills/codetrial-improve/rust.md`.
that no marker or CJK text lands in Rust or `web/`, and that no file passes its
Git-derived line ceiling. By default, a file may not exceed 1500 lines or its
committed size at the same path in HEAD, whichever is larger. Without Git
every file gets the 1500-line limit and the explicit baseline below cannot be
read, so a source archive fails this gate; run it from a clone. The gate passes
`--baseline origin/main`: larger files present in both the baseline and HEAD
are limited by their smallest size across those two trees and every reachable
commit in `origin/main..HEAD`, with a floor of 1500. Committed shrinking
automatically ratchets the limit; new or renamed paths get 1500, and deletion
retires an allowance. An explicit baseline requires resolvable HEAD and
baseline refs, common history and a complete, non-shallow repository; missing
history fails locally and in CI. Diverged refs are supported, but the branch
must still satisfy any smaller baseline limit. The rules and the lints left
off on purpose are in the Rust profile of codetrial-improve,
`.claude/skills/codetrial-improve/rust.md`.

The compiler those lints run on is pinned in `rust-toolchain.toml`, so a new
Rust release cannot fail an unchanged tree: Dependabot proposes each bump as
Expand Down Expand Up @@ -1058,8 +1069,10 @@ python3 scripts/interview-run-budget.py run \
`validate` is free and should always precede `run`. `run` passes the worker
`BROWSER_CHECK_RUN_MANIFEST`, `BROWSER_CHECK_RUN_ID`,
`BROWSER_CHECK_RUN_MANIFEST_SHA256` (the hash of the manifest bytes),
`BROWSER_CHECK_BUDGET_SOCKET`, `BROWSER_CHECK_BUDGET_TELEMETRY` and
`BROWSER_CHECK_BUDGET_STOP`. `scripts/browser-check.sh` refuses a partial set, a
`BROWSER_CHECK_BUDGET_SOCKET`, `BROWSER_CHECK_BUDGET_TELEMETRY`,
`BROWSER_CHECK_BUDGET_STOP` and `BROWSER_CHECK_BUDGET_REAP_AT` (when its reap
begins, in Unix seconds, which the serving fixture's own deadline is set
before). `scripts/browser-check.sh` refuses a partial set, a
run ID or hash that disagrees with the manifest, a binary whose sha256 changed,
and `CODETRIAL_WEB_URL`, since an external server is shared and was not built
from the pinned executable. With `BROWSER_CHECK_VALIDATE_ENV_ONLY=1` it stops
Expand Down Expand Up @@ -1121,6 +1134,36 @@ reserve is revalidated before launch against the resolved handover grace
at or after the boundary is `late_idle` or `late_terminal_preempted` and never
counts toward the two completed rotations a run needs.

`scripts/long-interview-evidence.cjs` reads the acquisition report the test
fixture leaves (receipts, executed batches, interviewer segments and turn
endings, each with its socket and how many of the others preceded it) and gives
`judgeCheckpoint` its evidence. It claims an association only when the facts
leave one reading: a reply cites the calls received on its own socket after the
previous ending, executed before it began, and not received while output was
being discarded. A swallowed or missing ending, a segment sealed by a cut, a
reply that spans a tool call, a socket replacement, a rewritten segment, two
replies in one window, a window edge inside the clock uncertainty, a reply
that began after the question but before its estimated end (a recording can
finish speaking sooner than its word count says) or a latched capture failure
makes the checkpoint `not_assessed` with the reason, never a pass. The
interview is `verified` only when every checkpoint passed.
`tests/fixtures/acquisition-report.json` is real producer output, regenerated
with `CODETRIAL_UPDATE_FIXTURES=1` on the test that writes it.

What a reply is judged on is what its question asks and no more
(`scripts/long-interview-schedule.cjs`). An inspect question asks about the
duplicate case, so the revision is judged in what the tool read (the answer
key's `tool.result_fact_ids`), and a reply may state the current revision
without penalty but not an earlier one. A board has no marker a model can read
back: `read_board` gives it a count and an image, never text, so a whiteboard
revision is told apart by the stroke count the tool reports (four strokes are
drawn per revision and the board keeps them, so revision n reads `4n strokes`),
its questions do not ask for a revision, and `tests/agent/whiteboard.rs` pins
the wording the judge reads, which the judge's own test takes from
`tests/golden/prompts.json`. The rubrics are lexical and were checked against
the first pilot's real replies. Changing any of this changes the catalogue, so
`SCHEDULE_VERSION`, the pins and every clip manifest move with it.

`run` also samples the owned process tree (RSS, open descriptors and threads
for every process, keyed by PID and start time so a reused PID is another
process) every 30 seconds into `resource-samples.jsonl`, for either arm of a
Expand Down
12 changes: 8 additions & 4 deletions docs/latency-observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,18 @@ Run the replay and the long-interview report checks without credentials:

```sh
python3 scripts/run-python-tests.py tests/test_analyze_latency.py
node --test tests/browser/soak-judge.test.js tests/browser/long-interview-driver.test.js
node --test tests/browser/soak-judge.test.js tests/browser/long-interview-driver.test.js \
tests/browser/long-interview-evidence.test.js
```

A long-interview report's `assessment_scope` is `structural`, even when
`run_kind` declares a qualification workload. Its `qualification` remains
`not_evaluated`: checkpoint `reply_content` and post-swap `decoded_signal`
are `not_assessed` by this driver. Separate evidence is required before
qualifying a build. `exposure_s` ends at the harness's observation boundary;
`not_evaluated`. Checkpoint `reply_content` is `not_assessed` by this driver;
in serving-fixture mode `scripts/browser-check.sh` judges it from the
Comment thread
jserv marked this conversation as resolved.
Resolved
fixture's report once the fixture has stopped and writes the verdict into the
capture. Post-swap `decoded_signal` (`verified`, `not_required`, `failed` or
`not_assessed`) is reported beside it. Neither changes the qualification or the
exit code. Separate evidence is required before qualifying a build. `exposure_s` ends at the harness's observation boundary;
the analyzer's exposure can extend through the terminal socket's end and must
not be substituted for it. `end_permit_granted` records permission, while
`end_clicked` records the page action; the countdown can win after the permit
Expand Down
19 changes: 12 additions & 7 deletions docs/provider-cost-and-degradation.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,13 @@ answers each request on the control topic, `report_retry` with status
`accepted` or `early` (with the seconds still to wait, which does not spend the
regeneration), and announces `closed` at expiry or when no key can ever answer;
a duplicate request during
regeneration gets no answer. The browser waits at most 145 seconds from its
request or the acceptance, whichever came last, so neither a reconnect nor a
lost answer cuts off a regeneration still in progress. The terminal failure
determines eligibility, so a transient burst followed by a schema failure
regenerates as a schema failure. Reloads and process restarts cannot recover
the inputs.
regeneration gets no answer. The browser waits at most 175 seconds from its
request or the acceptance, whichever came last. This covers the configured
generation deadline, one final rejoin grace and every delivery attempt. This
browser bound does not bound arbitrary SDK publish or host scheduling stalls.
The terminal failure determines eligibility, so a transient burst followed by
a schema failure regenerates as a schema failure. Reloads and process
restarts cannot recover the inputs.

Every report packet, provisional, regenerated or final, is delivered the same
way. The agent publishes those same bytes at most three times, each with a
Expand Down Expand Up @@ -511,7 +512,11 @@ completed interviews awaiting recovery; admission still refuses excess
starts rather than exceeding this bound. A fixed local room refuses a new
start while its existing agent is finalizing or awaiting report recovery, as
`dispatch_refused ... reason=finalizing` rather than `at_capacity`; it cannot
reuse that agent as an interviewer for another candidate. Provider projects are
reuse that agent as an interviewer for another candidate. A server that has
begun to stop refuses every start that would admit a new room as
`reason=closed`, and the candidate is told it is shutting down; admitted rooms
keep running, and a start for a room still being assessed joins it; a live
room otherwise gets the refusal it had before the stop. Provider projects are
rotated and their LiveKit connection-minute quota is refreshed in the
background, and known-exhausted projects are skipped.

Expand Down
Loading
Loading