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
151 changes: 151 additions & 0 deletions docs/code-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Pull request reviews

Code Review keeps one task per `(repository ID, PR number, profile)`. The task is
open until GitHub confirms closure or merge. A removed team request, a missing
search result or a failed HTTP request never closes it. Known closed PRs are also
polled, so reopening restores the same task and history. Drafts receive no new
task or review; tracked drafts retain history and only their state is observed.

Configure the service in the shareable `config/settings.yaml` layer:

```yaml
code_review:
board_name: Tasks Code Review
profiles:
- id: example
repository: alice/example
query: team-review-requested:alice/reviewers
enabled: false
schedule: '0 */2 * * *'
timezone: UTC
version_budget_usd: 5
daily_budget_usd: 30
max_attempts: 3
```

All fields in `code_review` require a restart. The service reads GitHub through
`gh api --method GET` using the operator's existing authentication. No credentials
belong in this section. Search always adds repository, open PR and `draft:false`
constraints, checks completeness and caps, and then fetches PRs directly.
Repository data belongs in configuration; no organization is built into the app.

The authenticated `/tasks-code-review` page shows the actual registered schedule,
last successful poll, next poll, observation errors, queue states and budget.
Its active and archive columns each paginate over the full set, ordered by last
content change and stable PR identity. Poll heartbeats do not reorder cards.
The pause button persistently stops the profile and only its owned workflow runs;
tasks and immutable artifacts remain. Resume performs one catch-up and keeps the
existing budget ledger. A configured disabled profile cannot be resumed from the
UI and never starts.
One startup catch-up replaces missed launches. Normal GitHub polling uses no LLM.

## Versions and audit

Full head/base/merge-base SHAs, PR text, comments/reviews, changed file paths and CI
are captured. Head, base, requirements and new failure context create input
versions. CI-only transitions update evidence without repeating the audit. Full
changed files at the recorded SHAs are supplied to the worker; it must inspect
needed callers, contracts and tests using read-only GitHub requests pinned to SHA.
Truncated file/search pagination fails visibly. Source acquisition has a 100 MB
bound and does not silently substitute a truncated API patch.

Workers use three session-bound MCP tools, without shell execution:
`code_review_input` supplies the snapshot and prior report;
`code_review_read_source` reads numbered, paginated source at the stored head,
base or merge-base SHA (including unchanged files); `code_review_checkpoint`
saves the four report stages in order and validates the final verdict. These
tools reject non-review sessions, paused/disabled profiles and settled or stopped
attempts. They accept no repository, run ID, arbitrary SHA or local output path.
Source reads are GET-only; report writes use directory descriptors and refuse
symlink traversal. The restricted Codex sandbox remains enabled. This also works
in containers where nested Linux namespaces prevent shell sandbox startup.
No command execution capability is exposed by these tools.

Each workflow writes description, before/after sequence diagrams and code review
in order. The instructions cover bugs, adjacent behavior, compatibility, enablement
and invariants. New behavior uses Mermaid `rect rgb(220, 252, 231)` and the text
«новое»; diagrams also retain their copyable source. Mermaid renders locally with
strict security, sanitization, source configuration/link restrictions and a size
cap. On a sequence-note syntax error, the renderer retries literal semicolons in
note labels and discloses the display correction; the immutable source is kept.
Other invalid diagrams fall back to source. No diagram rendering service is used.
See [Mermaid sequence highlighting](https://mermaid.js.org/syntax/sequenceDiagram.html#background-highlighting)
and [security configuration](https://mermaid.js.org/config/schema-docs/config.html).

Reviews currently do **not** run repository code or tests: there is no provisioned
secret-free isolated test runner. Workers must disclose this and propose focused
tests. Test execution requires a separately implemented isolated runner, never a
shell command in the authenticated Nerve environment. No GitHub comments, approvals,
branch changes or merges are part of this service.

Artifacts live under `workspace/memory/code-reviews/<PR>/<version>/`. Each attempt
has a working output directory and a write-once `result.json`; snapshots and final
artifacts are read back before index completion. Failed/interrupted attempts keep
partial output. SQLite stores identities, queue, ledger, events and artifact paths.
The ordinary workspace backup includes `memory/`; retain the SQLite backup together
with it. A workflow journal is not the only copy. History is served through normal
authenticated API/page routes, including inside the task detail.

A new SHA during review cannot make the old report current. Superseded queued
versions remain in history. Draft is rechecked before dispatch and before admission.
A workflow waiting for a global slot has a dispatch guard, so it checks again when
it actually starts. Per-profile concurrency is one. The queue keeps a PR-level waiting timestamp
across superseded versions and moves a claimed PR to the back, so frequent base
changes cannot repeatedly prioritize the same PRs. Failed attempts back off and
are capped. Observation errors do not starve other PRs and are retried at the next
poll.

## Budget semantics and recovery

Reservations are committed before workflow creation. A unique run ID links the
outbox to Nerve's workflow row. A controller lease excludes competing daemons;
SQLite `BEGIN IMMEDIATE` makes reservations safe across connections. Startup first
reconciles workflow recovery, then polls and dispatches. No retry resets a version's
spend, and every profile shares the daily cap. All profiles must use the same daily
cap and time zone. Active reservations carry across midnight. A run spanning dates
is conservatively charged in full to each date it touches (the UI calls this
committed budget); per-attempt actual spend is shown separately. This can reduce
throughput but cannot create capacity by crossing midnight.

A finished report whose final GitHub observation fails waits in a separate
read-only admission queue with backoff. Its paid run is settled and other PRs can
proceed. Recovery does not pay to recreate an already valid report.

A terminal workflow row is not sufficient to release a reservation: the executor,
kill and teardown tasks must have ended and recorded cost is reconciled. Native
Codex parent token usage is priced during the active turn, even when no child
workflow journals exist. Observed parent token cost survives an interrupted final
usage event, and late meter samples cannot overwrite a terminal run. Metering
is delayed, so actual provider billing may overshoot. The UI displays recorded
spend and excess; new runs stop when committed budget reaches the cap. If a run
actually started but was interrupted by a daemon restart, its unreported remainder
stays reserved as uncertainty, separately from measured spend. Pending runs that
never started are not charged. A per-version limit preserves the partial review
and never completes the PR task. Daily exhaustion keeps versions queued while
polling continues.

Notifications combine discoveries and lifecycle changes; ordinary results
accumulate in a ten-minute digest. Unchanged serious findings are deduplicated.
Sustained observation errors and budget exhaustion produce notifications;
unchanged polls remain silent.

## Deployment and verification

1. Install the application change through the normal reviewed application path.
Back up the database/workspace, run migrations and the frontend build. Migration
v062 is idempotent and adds tables without rewriting existing task data.
2. Review the disabled workspace profile through the configuration PR path. Keep
other cron jobs/model settings unchanged. Synchronize explicitly when automatic
workspace sync is disabled. Validate with the newly installed application version.
3. Enable the reviewed profile and restart. Check `/api/code-review/status` and the
board for the actual `code-review-<profile>` job, time zone, last success and next
run. Check that search was complete and unique task count matches eligible PRs.
4. Inspect one complete workflow (or an explicit budget queue), its SHA, all three
stages and previous-version selector. Repeated unchanged polling must preserve
board order; a new SHA must produce a version. Check draft, close and reopen.
5. Pause the profile to stop its work without deleting the archive. Disable it in
reviewed configuration before the next installation if shutdown is permanent.

Local regression checks: `pytest tests/test_code_review.py`, migration numbering
check, full backend tests, and frontend test/lint/build. Tests use synthetic PRs
and temporary databases/workspaces; they never launch paid review workflows.
9 changes: 9 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ reload cannot inspect, and are documented here only.
|--------|-----|
| `gateway.host`, `.port`, `.ssl.*` | the socket is already bound |
| `gateway.loop` | the event loop is chosen when uvicorn starts. `asyncio` (the default) spawns subprocesses with `vfork()`; `uvloop` (also what `auto` picks when it is installed) uses a full `fork()`, which blocks the loop for seconds per spawn once the process is large |
| `code_review` | profiles, schedules and budgets are captured at startup; pausing from the board is immediate and persistent |
| `timezone` | the cron scheduler and every trigger built from it carry the old zone |
| `agent.max_concurrent` | its semaphore cannot be resized under in-flight turns |
| `workspace` | the skill manager, the tool context, the memory bridges and each session's working directory all captured it at startup. Following it in one of them and not the others would be worse than not following it at all |
Expand Down Expand Up @@ -2488,3 +2489,11 @@ restart. See [Platform activity reporting](platform-activity.md).
| `platform.activity_socket` | path | `/run/nerve/guestd.sock` | The platform's local activity socket. `""` switches reporting off |
| `platform.activity_interval_seconds` | int | `30` | Seconds between reports when nothing changes (held to 5–300) |
| `platform.lease_ttl_seconds` | int | `120` | Lifetime of a keep-awake lease, renewed every half lifetime (held to 10–3600) |

### Pull request Code Review

`code_review` is a shareable settings section (restart required), with `board_name`
and a `profiles` list. Each profile supplies `id`, `repository`, `query`, `enabled`
(default false), `schedule`, `timezone`, `version_budget_usd`, `daily_budget_usd` and
`max_attempts`. See [Code Review](code-review.md) for defaults, budget accounting,
artifact retention, pause behavior and installation checks.
7 changes: 7 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,10 @@ Full-page markdown editor for task content:
- **Back button** — returns to the task list

Saving writes the full markdown file to disk and re-syncs the title and deadline to SQLite.

## Code Review board

[Code Review](code-review.md) adds an independent `/tasks-code-review` board for
configured pull request review profiles. It keeps task lifecycle tied to PR closure,
immutable review versions and content-based ordering separate from the ordinary
manually ordered task board.
15 changes: 15 additions & 0 deletions nerve/agent/backends/codex/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ def __init__(self, backend: CodexBackend, spec: SessionSpec):
self._items: dict[str, dict] = {}
# Latest thread/tokenUsage/updated for the active turn.
self._turn_usage: dict | None = None
self._turn_usage_completed = False
# Cumulative `total` at the end of the previous turn on this client,
# kept across turns; the baseline for the next turn's delta when known.
self._prev_total_usage: dict | None = None
Expand Down Expand Up @@ -1483,6 +1484,7 @@ async def _snapshot_pre_image(self, path: str, change: dict) -> None:
logger.warning("Snapshot failed for %s: %s", path, e)

def _map_turn_completed(self, params: dict) -> ev.TurnCompleted:
self._turn_usage_completed = True
turn = params.get("turn") or {}
status = str(turn.get("status") or "completed").lower()
if status not in ("completed", "interrupted", "failed"):
Expand Down Expand Up @@ -1580,9 +1582,22 @@ def _reset_turn_usage_accounting(self) -> None:
"""Clear per-turn token-accounting state (latest tokenUsage payload,
turn-start cumulative baseline, response count) at turn start."""
self._turn_usage = None
self._turn_usage_completed = False
self._turn_total_base = None
self._turn_response_count = 0

def live_parent_cost(self) -> float:
"""Current parent turn's API-equivalent cost, excluding child journals.

Use the same delta and pricing as final accounting, without advancing
its baseline. Completion hands the charge to persisted turn usage.
"""
if self._turn_usage_completed:
return 0.0
baseline = self._prev_total_usage if self._prev_total_usage is not None else self._turn_total_base
usage = self._normalize_usage(self._turn_usage, baseline)
return compute_cost(self.model, usage, self._backend.codex.pricing) or 0.0

def _capture_turn_total_base(self, usage: dict) -> None:
"""Record the fallback turn-start baseline from the turn's first
tokenUsage notification, used only when no previous-turn total is
Expand Down
8 changes: 8 additions & 0 deletions nerve/agent/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2654,6 +2654,14 @@ async def _broadcast_session_running(
"background_count": self.agents.live_count(session_id),
})

def get_live_codex_parent_cost(self, session_id: str) -> float:
"""Token-priced active Codex parent usage, before its turn is persisted."""
if not self.is_session_running(session_id):
return 0.0
client = self.sessions.get_client(session_id)
getter = getattr(client, "live_parent_cost", None)
return getter() if callable(getter) else 0.0

def get_codex_ultracode_run_ids(self, session_id: str) -> set[str] | None:
"""Ultracode run ids the session's live Codex client attributed to
its current turn, or None when unavailable (no client / claude
Expand Down
2 changes: 2 additions & 0 deletions nerve/agent/tools/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from nerve.agent.tools.registry import ToolRegistry

from nerve.agent.tools.handlers.code_review import CODE_REVIEW_SPECS
from nerve.agent.tools.handlers.config_pr import CONFIG_PR_SPECS
from nerve.agent.tools.handlers.forms import FORM_SPECS
from nerve.agent.tools.handlers.hoa import HOA_SPECS
Expand Down Expand Up @@ -47,6 +48,7 @@ def build_default_registry() -> ToolRegistry:
*CONFIG_PR_SPECS,
*WAKEUP_SPECS,
*WORKFLOW_RUN_SPECS,
*CODE_REVIEW_SPECS,
*REVIEW_LOOP_SPECS,
*HOA_SPECS,
):
Expand Down
Loading