From 7a06c7e246c1adbf9180395d7d60cd3b1b4976c7 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 20 Jul 2026 01:40:30 -0500 Subject: [PATCH 1/3] spec(cave): define Phase 5 proposal lifecycle view Bind Cave actions to daemon-owned lifecycle metadata, preserve daemon labels, and fail closed on unavailable, stale, mismatched, or unknown scheduling state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/PHASE-4-CAVE-SURFACES.md | 78 +++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/specs/PHASE-4-CAVE-SURFACES.md b/specs/PHASE-4-CAVE-SURFACES.md index e4432f3..ed78af0 100644 --- a/specs/PHASE-4-CAVE-SURFACES.md +++ b/specs/PHASE-4-CAVE-SURFACES.md @@ -6,6 +6,12 @@ **Substrate:** `coven-threads-core` v0.1.2 (98 tests green) + coven daemon integration branch `feat/threads-gate-validator` (PR OpenCoven/coven#382, merge gated on `threads-986.19`) **Shape reference:** OpenTrust `docs/MEMORY-API-CONTRACT.md` / `docs/MEMORY-LAYER-STANDARD.md` — mirrored in *shape* (contract-first, evidence-over-summaries, freshness surfaced everywhere), **never in stack** (no Tauri, no Convex, no plugin architecture, no vendored code) +**Phase 5 amendment (2026-07-20, bead `threads-uqx.7`):** scheduled +proposal lifecycle metadata comes from the daemon read model added by +OpenCoven/coven#430. Cave may combine that metadata with staged-file contents +for inspection, but it never infers lifecycle transitions from clocks or local +files. + --- ## 0. Ground rules (normative, inherited — violating any of these is wrong by declaration) @@ -221,10 +227,58 @@ never dropped from the list silently (§4.R7). { "surface": "MEMORY.md", "contents": { "encoding": "utf8|base64", "data": "…full desired contents, never diffs…" } } ], "stagedAt": "…" - } + }, + "authority": { /* ProposalAuthorityView, below */ } } ``` +Phase 5 adds daemon-owned scheduling metadata without making Cave an approval +engine. The daemon adapter joins each parse-ok staged file to +`GET /api/v1/threads/proposals` by proposal id. Staged contents remain visible +for principal inspection; every lifecycle field and display label comes from +the daemon response. + +```jsonc +// Scheduled proposal whose daemon metadata is verified. +{ + "state": "verified", + "approvalPath": { + "variant": "auto-regression|familiar-coherence|human-approval|human-approval-with-rationale", + "label": "auto|familiar_review|human_review|human_required", // daemon text, rendered verbatim + "vetoDeadline": "RFC3339 | null" + }, + "lifecycle": "awaiting-human-approval|veto-window-open|ready-for-replay|blocked", + "blockedReason": "string | null", + "earliestClose": "RFC3339 | null", + "affectedRegions": ["tool_defaults"], + "availableDecisions": ["approve", "reject"] // deterministic mapping below +} + +// Pre-Phase-5 proposal. Compatibility only; no scheduled lifecycle is invented. +{ "state": "legacy", "reviewKind": "authority" } + +// Scheduled envelope exists, but daemon authority cannot be joined or verified. +{ + "state": "blocked", + "why": "daemon-unavailable|daemon-proposal-missing|daemon-unparseable|daemon-mismatch|unknown-lifecycle" +} +``` + +`availableDecisions` is a closed presentation mapping over verified daemon +fields, not local authorization: + +| Daemon lifecycle and path | Cave action | +|---|---| +| `awaiting-human-approval` + `human-approval` | approve or reject | +| `awaiting-human-approval` + `human-approval-with-rationale` | approve with a non-empty note, or reject | +| `veto-window-open` + a path carrying a veto deadline | reject, rendered as **Veto** | +| `ready-for-replay`, `blocked`, unknown combination, or blocked authority | no action | + +Cave MUST NOT compare its clock to `vetoDeadline` or `earliestClose` to change +the action set. Deadline expiry triggers daemon replay; only a fresh daemon +response may move lifecycle. Every POST remains a forwarder and the daemon +re-validates the decision. + ### 2.7 `DegradedFamiliarView` — a familiar whose ward config cannot be parsed Amendment 2026-07-18 (bead `threads-k9s`, follow-up to `threads-vmf`/`threads-v60`). @@ -282,6 +336,15 @@ never apply edits, never touch `pending/`, never write sqlite. - Proposal `parse: corrupt`: both actions disabled in the UI *and* the routes answer **HTTP 409** `{ "blocked": true, "why": "proposal-corrupt" }` if called anyway (§4.R6). +- Phase 5 scheduled proposals expose only `authority.availableDecisions`. + Unknown, stale, unavailable, mismatched, or unrecognized daemon lifecycle + metadata disables both actions. Cave never substitutes a local default. +- A veto uses the existing reject forwarder. The UI label changes to **Veto** + only when the verified daemon lifecycle is `veto-window-open`; the request is + still `POST /api/proposals/[id]/reject`. +- `human_required` approval requires a non-empty `note` before Cave enables the + approve button. The daemon remains authoritative and rejects missing + rationale even if the route is called directly. ### 3.8 Response envelope — freshness on every response @@ -334,6 +397,10 @@ approval actions disabled, evidence trace still reachable. | R10 | Snapped thread | `state: "snapped"` | Terminal treatment: read-only, "fresh authority ceremony required" copy; no repair affordance | | R11 | Unknown route/id (404s) | id not in source | 404 with envelope, `blocked: true`; UI renders not-found as blocked, not empty-healthy | | R12 | Familiar ward config unparseable | §2.7 `degraded` entry in weaves response | Blocked rail row named for the familiar, "ward unreadable — protection not verifiable" copy, sanitized error in trace, zero threads shown, all actions disabled; never silently absent (added 2026-07-18) | +| R13 | Scheduled proposal lacks daemon lifecycle metadata | staged file has Phase 5 schema but daemon join is absent/unavailable | Proposal remains inspectable with blocked authority; both actions disabled | +| R14 | Daemon proposal metadata disagrees with staged identity/targets | id, familiar, writer, staged time, or target set mismatch | Blocked `daemon-mismatch`; no locally preferred source | +| R15 | Unknown lifecycle/path combination | unrecognized enum value or invalid combination | Blocked `unknown-lifecycle`; never fall back to legacy actions | +| R16 | Scheduled proposal metadata stale | response is past `staleAfter` | Last-known lifecycle may render as trace only; both actions disabled until a fresh daemon response | Rollup arithmetic (R1, normative): `snapped > frayed > unknown > stale > holds`. @@ -371,6 +438,13 @@ need; Phase 5+. - [ ] Both adapters answer every route with the §3.8 envelope - [ ] All ten fixture states exist and are exercised - [ ] Every §4 row has a test proving its rendering -- [ ] No code path renders healthy from unverifiable input (R1–R11 sweep) +- [ ] No code path renders healthy from unverifiable input (R1–R16 sweep) - [ ] POST routes forward-only; grep-level check: no `fs.write`/`unlink` under the proposals routes, no sqlite writes anywhere in Cave API +- [ ] Phase 5 fixtures cover `awaiting-human-approval`, + `veto-window-open`, `ready-for-replay`, and `blocked`, plus unavailable, + mismatched, and unknown daemon metadata +- [ ] Cave renders daemon approval labels verbatim and never advances lifecycle + from its own clock +- [ ] `availableDecisions` follows the §2.6 closed mapping; rationale-required + approval stays disabled until a non-empty note exists - [ ] `pnpm typecheck`, `pnpm test:app`, `pnpm test:api` green From 67af4710a383a6cb31fe12712880b0ab42e64a8f Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 20 Jul 2026 01:54:06 -0500 Subject: [PATCH 2/3] spec(cave): bind decisions to inspected revisions Require daemon proposal revisions and canonical familiar UUIDs so Cave can fail closed on changed contents or mismatched identity before forwarding a decision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/PHASE-4-CAVE-SURFACES.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/specs/PHASE-4-CAVE-SURFACES.md b/specs/PHASE-4-CAVE-SURFACES.md index ed78af0..3fee536 100644 --- a/specs/PHASE-4-CAVE-SURFACES.md +++ b/specs/PHASE-4-CAVE-SURFACES.md @@ -236,12 +236,17 @@ Phase 5 adds daemon-owned scheduling metadata without making Cave an approval engine. The daemon adapter joins each parse-ok staged file to `GET /api/v1/threads/proposals` by proposal id. Staged contents remain visible for principal inspection; every lifecycle field and display label comes from -the daemon response. +the daemon response. `proposalRevision` commits the complete authority envelope, +including staged contents. `familiarUuid` is compared directly with the staged +payload; the daemon's human `familiarId` remains display data and is not used as +the identity join key. ```jsonc // Scheduled proposal whose daemon metadata is verified. { "state": "verified", + "proposalRevision": "64-char lowercase hex", // daemon commitment to the complete authority envelope + "familiarUuid": "familiar-uuid", // canonical join key for the staged payload "approvalPath": { "variant": "auto-regression|familiar-coherence|human-approval|human-approval-with-rationale", "label": "auto|familiar_review|human_review|human_required", // daemon text, rendered verbatim @@ -277,7 +282,11 @@ fields, not local authorization: Cave MUST NOT compare its clock to `vetoDeadline` or `earliestClose` to change the action set. Deadline expiry triggers daemon replay; only a fresh daemon response may move lifecycle. Every POST remains a forwarder and the daemon -re-validates the decision. +re-validates the decision. Approve/reject bodies carry +`expectedRevision: authority.proposalRevision`; the daemon rejects a changed or +missing revision before accepting a manual Phase 5 decision. This binds the +principal's action to the exact proposal contents inspected, not merely its id +and target list. ### 2.7 `DegradedFamiliarView` — a familiar whose ward config cannot be parsed @@ -323,7 +332,9 @@ never apply edits, never touch `pending/`, never write sqlite. ### 3.7 Approve/reject semantics (fail-closed) -- Body: `{ "note": "optional principal note" }`. `readsJson: true`, +- Body: + `{ "note": "optional principal note", "expectedRevision": "daemon-issued proposal revision" }`. + `expectedRevision` is mandatory for manual Phase 5 decisions. `readsJson: true`, `invalidJson: "guarded"`. - `daemon-present`: forward to the daemon socket; the daemon re-validates (staging is data, not authority — replay goes back through `validate`, @@ -398,7 +409,7 @@ approval actions disabled, evidence trace still reachable. | R11 | Unknown route/id (404s) | id not in source | 404 with envelope, `blocked: true`; UI renders not-found as blocked, not empty-healthy | | R12 | Familiar ward config unparseable | §2.7 `degraded` entry in weaves response | Blocked rail row named for the familiar, "ward unreadable — protection not verifiable" copy, sanitized error in trace, zero threads shown, all actions disabled; never silently absent (added 2026-07-18) | | R13 | Scheduled proposal lacks daemon lifecycle metadata | staged file has Phase 5 schema but daemon join is absent/unavailable | Proposal remains inspectable with blocked authority; both actions disabled | -| R14 | Daemon proposal metadata disagrees with staged identity/targets | id, familiar, writer, staged time, or target set mismatch | Blocked `daemon-mismatch`; no locally preferred source | +| R14 | Daemon proposal metadata disagrees with staged authority | id, canonical `familiarUuid`, writer, staged time, target set, or committed proposal contents mismatch | Blocked `daemon-mismatch`; no locally preferred source | | R15 | Unknown lifecycle/path combination | unrecognized enum value or invalid combination | Blocked `unknown-lifecycle`; never fall back to legacy actions | | R16 | Scheduled proposal metadata stale | response is past `staleAfter` | Last-known lifecycle may render as trace only; both actions disabled until a fresh daemon response | @@ -445,6 +456,8 @@ need; Phase 5+. mismatched, and unknown daemon metadata - [ ] Cave renders daemon approval labels verbatim and never advances lifecycle from its own clock +- [ ] Every manual Phase 5 decision forwards the daemon-issued + `proposalRevision`; missing or changed revisions fail closed - [ ] `availableDecisions` follows the §2.6 closed mapping; rationale-required approval stays disabled until a non-empty note exists - [ ] `pnpm typecheck`, `pnpm test:app`, `pnpm test:api` green From a00cf5768c9d288bc5c3af1a3bbe509ffd7d5de6 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 20 Jul 2026 01:57:22 -0500 Subject: [PATCH 3/3] spec(cave): define revision and wire normalization Specify canonical SHA-256 proposal revisions and the exact snake_case-to-view mapping so Cave can verify inspected contents without inventing lifecycle semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- specs/PHASE-4-CAVE-SURFACES.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/specs/PHASE-4-CAVE-SURFACES.md b/specs/PHASE-4-CAVE-SURFACES.md index 3fee536..8b273e7 100644 --- a/specs/PHASE-4-CAVE-SURFACES.md +++ b/specs/PHASE-4-CAVE-SURFACES.md @@ -241,6 +241,13 @@ including staged contents. `familiarUuid` is compared directly with the staged payload; the daemon's human `familiarId` remains display data and is not used as the identity join key. +Revision algorithm (normative): remove top-level procedural +`decisionRequest`/`decisionState`; recursively sort every JSON object by Unicode +code-point key order while preserving array order; serialize compact UTF-8 JSON; +then lowercase-hex SHA-256 the bytes. Cave computes this revision from the same +parsed envelope used to render edits and requires equality with the daemon's +`proposalRevision`. A mismatch blocks the card before any action is offered. + ```jsonc // Scheduled proposal whose daemon metadata is verified. { @@ -272,6 +279,24 @@ the identity join key. `availableDecisions` is a closed presentation mapping over verified daemon fields, not local authorization: +The daemon wire uses snake_case enum values and nested field names. The adapter +performs only this closed normalization before the mapping: + +| Daemon wire | Cave view | +|---|---| +| `auto_regression` | `auto-regression` | +| `familiar_coherence` | `familiar-coherence` | +| `human_approval` | `human-approval` | +| `human_approval_with_rationale` | `human-approval-with-rationale` | +| `awaiting_human_approval` | `awaiting-human-approval` | +| `veto_window_open` | `veto-window-open` | +| `ready_for_replay` | `ready-for-replay` | +| `blocked` | `blocked` | +| `approvalPath.veto_deadline` | `approvalPath.vetoDeadline` | + +Any value outside this table is `unknown-lifecycle`; no generic case conversion +or label synthesis is permitted. `approvalPath.label` is never normalized. + | Daemon lifecycle and path | Cave action | |---|---| | `awaiting-human-approval` + `human-approval` | approve or reject |