Skip to content
Merged
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
1,274 changes: 1,274 additions & 0 deletions agents/__tests__/base2.test.ts

Large diffs are not rendered by default.

544 changes: 533 additions & 11 deletions agents/base2/base2.ts

Large diffs are not rendered by default.

168 changes: 168 additions & 0 deletions agents/base2/gate-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,117 @@ export type Base2ReviewReceipt = {
recordedAt: string
}

/**
* Gate-issued per-task validation receipt for one EXECUTE_PLAN plan task.
*
* This is the evidence `update_plan_status` verifies a `checkpoint.receiptIds`
* entry against before a PLAN.md task may move to `done`. The runtime's
* `validatePlanTransition` already refused a `done` transition without a passed
* validation checkpoint carrying at least one receipt ID, but those IDs were
* entirely model-supplied, so an invented string satisfied the rule. A receipt
* here is minted ONLY by base2's own fresh validation/reviewer gate pass, which
* is what ties task completion to real gate evidence.
*
* EVIDENCE KINDS. A plan task whose gate cycle produced no reviewable diff must
* still be completable, and its receipt must not claim content evidence it does
* not have, so `evidence` records exactly what the cycle covered:
* - `'reviewed-diff'`: the reviewable subset the reviewer attested; `files` is
* that subset.
* - `'unreviewed-scope'`: pending files existed but NONE of them were
* reviewable (docs-only / `.md` / `.agents/`), so the reviewer was skipped.
* `files` is the VALIDATED pending set and the receipt claims no content
* review. Without this kind the mint produced a receipt whose fingerprint
* was the hash of an EMPTY file list — a constant — while presenting as
* reviewed-diff evidence.
* - `'no-diff'`: the cycle had no pending files at all (work that is pure
* verification, or whose only output is a non-reviewable artifact). `files`
* is empty, so this fingerprint is a CONSTANT by construction.
*
* `files` is the gate-covered set this receipt attests — for
* `'unreviewed-scope'` that is the validated pending set, not a reviewed subset.
* The invariant that makes verification uniform across all three kinds is
* `snapshotFingerprint === hashGateSnapshotDetails(buildGateSnapshotDetails(files, ''))`:
* content only, with an EMPTY summary component. For `'reviewed-diff'` that is
* exactly the reviewable-set fingerprint base2 computed for the review.
*
* Why `receiptId` is GATE-COMPUTED rather than reviewer-reported: it embeds the
* prefix of the fingerprint base2 hashed itself, the same provenance rule that
* makes `Base2ReviewReceipt.gateId` trustworthy. A reviewer-REPORTED
* `snapshotFingerprint` is deliberately drift-tolerated by the attestation path,
* so deriving the receipt from it would let a reviewer (or a model quoting one)
* choose its own receipt ID and forge completion evidence. A non-attestable
* fingerprint (a stable `unreadable:*` marker) is an error string rather than
* content evidence and never mints a receipt, for any kind.
*
* LIFETIME. At most ONE receipt is live per `taskId`: a newly minted receipt
* REPLACES the task's previous one instead of appending, so the printed ID is
* unambiguous. Two complementary mechanisms retire a receipt that has stopped
* being true, keeping the published ledger to receipts that hold right now:
* 1. Content verification (`prunePlanTaskGateReceipts` in base2.ts, at turn
* start and immediately before the mint): recompute
* `hashGateSnapshotDetails(buildGateSnapshotDetails(files, ''))` and drop
* the receipt unless the recomputation is attestable AND equal to
* `snapshotFingerprint`. Structurally invalid entries are dropped too.
* 2. Change supersession (`supersedePlanTaskGateReceiptsForChangedFiles` in
* base2.ts, called from `recordChangedFiles` and from the credited-file
* eviction ledger): drop every receipt whose `files` intersect the changed
* paths, plus EVERY receipt whose `evidence` is not `'reviewed-diff'` —
* those have no verifiable content identity (a `'no-diff'` fingerprint is a
* constant and can never fail verification), so only supersession can
* retire them. Legacy receipts serialized before `evidence` existed are
* retired the same way (fail closed).
*
* BOUND ON THE GUARANTEE: the runtime handler reads the LIVE
* `agentState.base2ActiveWork` during a step, so the ledger it sees is whatever
* base2 wrote at the last gate pass. A model that edits files and marks the task
* done inside the SAME step is therefore still outside supersession's reach.
* The property is "this receipt was true as of the last gate pass", not an
* airtight proof at the moment of the transition.
*
* PRODUCTION READERS (no field here is documented-but-unread):
* - base2.ts `prunePlanTaskGateReceipts` reads `receiptId`, `taskId`, `files`,
* and `snapshotFingerprint`;
* - base2.ts `supersedePlanTaskGateReceiptsForChangedFiles` reads `evidence`
* and `files`;
* - base2.ts's gate-pass mint site reads `taskId` (one live receipt per task)
* and `receiptId` (an identical ID is the idempotent repeat pass and is left
* untouched rather than churning `recordedAt`);
* - base2.ts's gate-pass `add_message` reads `taskId`, `receiptId`,
* `evidence`, and `files.length` for the printed evidence sentence;
* - base2.ts `buildPinnedActiveWorkMessage` reads `receiptId`, `taskId`, and
* `evidence` for the durable recovery line (pinned state survives context
* compaction, which is what makes a superseded ID recoverable);
* - the runtime `update_plan_status` handler's
* `readGateIssuedPlanTaskReceipts` reads `receiptId` and `taskId`, and
* `validatePlanTransition` matches them against `checkpoint.receiptIds` and
* lists the live IDs for the task when it rejects.
* `validationSummary`, `reviewerVerdict`, and `recordedAt` are durable audit
* fields surfaced through gate state itself; no decision branches on them.
*/
export type Base2PlanTaskGateReceipt = {
/**
* Gate-issued receipt id, derived from the fingerprint base2 computed itself:
* `plan-gate:<taskId>:<fp16>` for `'reviewed-diff'`,
* `plan-gate:<taskId>:unreviewed-scope:<fp16>`, or
* `plan-gate:<taskId>:no-diff:<fp16>`, where `<fp16>` is the first 16 chars of
* `snapshotFingerprint`. The kind is part of the id for the two non-reviewed
* kinds so a receipt that claims no content review can never be mistaken for
* one that does.
*/
receiptId: string
/** Stable PLAN.md task ID this gate cycle covered. */
taskId: string
/** What the gate cycle actually covered; see the docblock above. */
evidence: 'reviewed-diff' | 'unreviewed-scope' | 'no-diff'
/** Always `hashGateSnapshotDetails(buildGateSnapshotDetails(files, ''))`. */
snapshotFingerprint: string
/** The gate-covered set this receipt attests (empty for `'no-diff'`). */
files: string[]
validationSummary: string
reviewerVerdict: string
recordedAt: string
}

// Typed runtime-owned gate state. Field names are kept identical to the
// historical Base2ActiveWorkState shape so existing serialized
// base2ActiveWork objects keep round-tripping. The new
Expand Down Expand Up @@ -348,6 +459,63 @@ export type Base2ActiveWorkState = Base2GateState & {
specialistNoVerdictCounts?: Record<string, number>
/** Compact source-backed receipts from successful reviewer passes. */
reviewReceipts?: Base2ReviewReceipt[]
/**
* Stable PLAN.md task ID currently claimed by the model, extracted from
* successful `update_plan_status` tool calls in message history (the
* `currentTask` pointer, else the last `updates` entry moved to
* `in_progress`). Normalized to its leading stable-ID token, so
* `"P2-T3 Implement the thing"` is stored as `"P2-T3"` — the same form
* `validatePlanTransition` matches a `currentTask` pointer against a task id.
* Cleared when a successful call empties `currentTask` or moves the claimed
* task to `done`/`cancelled`. "Successful" includes the handler's POINTER-only
* messages (`Current task -> "<task>".` / `Current task pointer cleared.`),
* which carry none of the shared success verbs and are opted in explicitly at
* the extraction site. Execution-tracking state, NOT gate credit,
* which is why it lives here and not on `Base2GateState`.
* Backward-compatible: older serialized state lacks it (treated as no claim).
*/
activePlanTaskId?: string
/**
* Gate-issued per-task validation receipts (see `Base2PlanTaskGateReceipt`),
* written only on base2's FRESH validation/reviewer gate-pass path while a
* plan task is claimed. MUST stay a plain JSON-serializable array (never a
* Map/Set) and is bounded to the most recent 24 entries at every write site,
* the same convention as `reviewReceipts`, so durable state cannot grow
* without bound across a long plan run. base2's hydration ENFORCES that
* shape: a present-but-non-array value (corrupt or hand-edited serialized
* state) is normalized to an EMPTY array instead of being left intact, so
* every reader fails closed instead of throwing a TypeError mid-turn while
* the key stays PRESENT and gate-issued verification stays active (see
* below).
*
* This is a ledger of receipts that are TRUE RIGHT NOW, not an append-only
* history: at most one receipt is live per task (a new mint replaces that
* task's previous one over the remaining entries), content verification drops
* any receipt whose covered bytes no longer hash to its `snapshotFingerprint`,
* and change supersession drops receipts a recorded change invalidated. Both
* mechanisms PRUNE the array; neither ever deletes the key, because presence
* is what keeps verification active (see below).
*
* Its PRESENCE is the signal that gate-issued verification is active: the
* `update_plan_status` handler forwards this array to
* `validatePlanTransition`, which then requires the checkpoint to cite a
* gate-issued receipt ID for the task being completed. A PRESENT array —
* including an EMPTY one — rejects (the gate is active but has issued no
* evidence yet). When the key is ABSENT the handler falls back to the
* pre-existing "any non-empty receiptIds" rule, which is what keeps non-base2
* agents and a base2 run with the validation gate disabled
* (`hasNoValidation` / plan-only, where no receipt could ever be minted) able
* to complete plan tasks. base2 therefore initializes this key only when the
* gate actually runs, and DELETES an inherited key on a gate-disabled turn:
* the invariant is "present ⇔ the gate is active for THIS run", not "present ⇔
* the gate ran at some point in this session". Without that deletion a session
* that published the key under EXECUTE_PLAN/base2 and later resumed through a
* gate-disabled variant would restore verification with no way to mint
* evidence, making every new plan task impossible to move to `done`. Dropping
* the stale ledger is safe: the next fresh gate pass re-mints a receipt for
* whatever task is claimed then.
*/
planTaskGateReceipts?: Base2PlanTaskGateReceipt[]
/**
* M3 (R1d) — snapshot of the pendingGateFiles used to detect that the
* pending gate file set has changed, so the three aux-gate done-flags above
Expand Down
34 changes: 34 additions & 0 deletions common/docs/update-plan-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ task requires a passed validation checkpoint with `receiptIds`.
| `summary` | string, optional | Short human-readable summary. |
| `receiptIds` | array of non-empty strings, optional | Supporting receipt IDs. An empty array is treated as absent. |

When the caller's agent state carries gate-issued receipts — base2 publishes
`planTaskGateReceipts` in `base2ActiveWork` while its automated validation/reviewer
gate is active — `receiptIds` must cite at least one of those receipts whose task ID
matches the task being completed. Gate-issued receipt IDs are printed in the gate-pass
message and come in three shapes, one per evidence kind:

- `plan-gate:<taskId>:<fingerprintPrefix>` — a reviewable diff was reviewed.
- `plan-gate:<taskId>:unreviewed-scope:<fingerprintPrefix>` — pending files existed but
none of them were reviewable (docs-only, `.md`, `.agents/`), so validation covered
them and the reviewer was skipped.
- `plan-gate:<taskId>:no-diff:<fingerprintPrefix>` — the cycle had no file changes at
all (verification-only work).

A task whose gate cycle had no reviewable diff is therefore still completable, via an
`unreviewed-scope` or `no-diff` receipt that records exactly what was covered instead
of implying a content review that never happened. Arbitrary strings are rejected, and
a present-but-empty receipt list means the gate has issued no evidence for any task
yet, so completion is refused.

Receipts are superseded rather than permanent. A receipt stops authorizing completion
once the files it covers change again, and an `unreviewed-scope` / `no-diff` receipt —
which has no verifiable content identity — is superseded as soon as any further change
is recorded. After more edits, let the validation/reviewer gate close again and cite
the NEW ID from the newest gate-pass message; the current live ID is also repeated in
the pinned harness state, which survives context compaction. When the check rejects,
the error lists the receipt IDs that are live for that task, or reports that none is.

A published ledger that is present but malformed (not an array, or entries without a
string `receiptId`/`taskId`) fails closed: verification stays active with no usable
evidence, so the completion is refused instead of falling back to the legacy rule.
Callers with no gate-issued receipts at all (a non-base2 agent, or a base2 run with the
gate disabled) keep the previous behavior: any non-empty `receiptIds` satisfies the
check.

## Usage example

```json
Expand Down
Loading
Loading