PLAN.md — Open Question (OQ) Escalation Channel
PLAN.md — Open Question (OQ) Escalation Channel
Section titled “PLAN.md — Open Question (OQ) Escalation Channel”Decomposition companion to PRODUCT.md (33 invariants
OQ-INV-1..OQ-INV-33) and TECH.md (file-per-record transport,
atomic-publish primitive, record schemas, write/poll/restart protocols). This
document slices the ratified spec pair into ordered, independently-testable
implementation Subtasks (ID-43.4 onward), records the dependency graph and
wave structure, assigns disjoint file-ownership sets, states which slices
parallelise vs serialise, flags any dependency escalations, and proves every
OQ-INV-N is delivered by at least one Subtask.
No production code is authored here. This is the PLAN-phase artefact only.
Overview
Section titled “Overview”The OQ-escalation channel is a durable file-per-record mailbox living under
the existing cmux substrate at .claude/cmux-events/<session-id>/oq/. A worker
emits immutable OQ files; a parent writes immutable decision files; both sides
re-derive their entire view from disk with zero in-memory state. The unit of
work is shell helpers + a Python checksum/canonical-JSON helper, sited
beside session-driver-cmux’s existing five scripts — not application code (no
migration, no MCP tool, no React/TanStack surface). Decomposition follows
TECH’s own recommendation: build the atomic-publish primitive first
(everything writes through it), then the record schemas + oq_id derivation
(the data contract every other slice consumes), then layer the worker-emit,
parent-decide, worker-poll/state, and crash/restart behaviours on
that contract, then the live-cmux integration, with test kinds woven
into the slices that own each behaviour plus a dedicated crash-injection /
latency / restart harness slice. The chain is deliberately bottom-up: each slice
delivers a complete, independently-testable behaviour and leaves the channel in
a working state.
Architecture decisions inherited from TECH (non-negotiable for Executors)
Section titled “Architecture decisions inherited from TECH (non-negotiable for Executors)”Every Subtask below MUST honour these. They are restated once here so each
Subtask details can reference “the inherited decisions” rather than re-deriving
them:
- Transport = file-per-record, never an append-log. OQ = file
oq/questions/<oq_id>.json; decision = fileoq/decisions/<oq_id>.json; worker lifecycle = single fileoq/oq-state.json. State is the directory listing plus the records. No index, nooq.jsonl, no SQLite, no FIFO. (TECH “Transport decision”.) - The single write primitive is
atomic-publish(dir, name, payload): write canonical-JSON (withchecksum) to a same-directory dotfile<dir>/.<name>.tmp.<pid>.<rand>→fsync(file)→fsync(dir)→rename(tmp, <dir>/<name>)→fsync(dir).rename(2)is the commit point; tmp and target share the directory so the rename is always same-filesystem atomic (OQ-INV-3).fsync-before-renameis what lets emit return success with OQ-INV-5 durability. No other code path may write a record. (TECH “Write protocol”.) oq_id = "oq-" + sha256_hex(task_id "|" phase "|" content_hash)[0:16]wherecontent_hash = sha256_hex(normalised(question) "|" json(context_ref)).worker_id,emitted_at,seqare excluded from the derivation so a relaunched worker (same identity, OQ-INV-29) and a crash-then-re-emit (OQ-INV-12) both re-derive the same id.normalised(question)trims and collapses internal whitespace. (TECH “oq_idderivation”.)- Every record carries
schema_version: 1and achecksum(SHA-256 over canonical-JSON of all other fields). Every read recomputes the checksum and validates the schema; on mismatch / missing required field / malformed timestamp / unknown enum the reader fails closed — reports a channel error and refuses to advance past the bad record (never silently skips or drops). (TECH “Integrity / fail-closed”, OQ-INV-27.) - Readers enumerate published
*.jsonnames only, excluding dotfiles, so an in-flight tmp record is never visible. (OQ-INV-3.) seqis a monotonic per-worker integer derived from disk (max overquestions/*.json+ 1), never an in-memory counter — crash-safe (OQ-INV-29). FIFO (OQ-INV-4) is a read-time sort over the immutable set, not a writer property.- Two-state protocol only (OQ-Q1 ratified NO): emitted → decided. No
receivedack, noacks/directory. The worker infers “seen” solely from the presence ofdecisions/<oq_id>.json. - The channel imposes NO timeout (OQ-Q2 ratified): auto-abort is a
workflow-orchestrationpolicy enacted via an ordinaryoutcome: abort_taskdecision. No daemon, no timer. - No cap on non-blocking OQs (OQ-Q3 ratified, deferred): must be recorded as a backlog-revisit trigger, not silently dropped.
directiveis structured advisory data, never executable code — noeval/exec path may consume it (OQ-INV-19).checkpoint_refcontents are opaque to the channel — its schema is aworkflow-orchestrationconcern; channel Subtasks must NOT over-specify it.send-promptis a latency nudge only, never correctness-bearing. The authoritative decision is always the file; the prompt only wakes the poll. Tests must prove correctness via polling alone with the nudge dropped.- KH conventions that DO apply: UK English throughout (comments, docs,
error strings); worktree-isolation rules (shared filesystem, relative paths
in sub-agent-facing scripts); dependency surface limited to
jq+ coreutils +sha256sum(shell) or Python stdlib — no new npm/pip package. KH database/auth/TanStack gotchas do not apply (TECH “Context”).
Slice structure and rationale
Section titled “Slice structure and rationale”Eight implementation Subtasks, 43.4 through 43.11. (43.1 PRODUCT, 43.2 TECH, 43.3 this PLAN are complete.) The cut follows two principles from the loaded skill: vertical slices that each deliver a complete testable behaviour, and dependency order bottom-up so foundations land first and high-risk slices fail fast.
| Slice | Title | Delivers | Why it sits here |
|---|---|---|---|
| 43.4 | atomic-publish primitive + integrity helper | The single write primitive (atomic-publish), canonical-JSON, checksum compute/verify, schema-version + fail-closed read guard | Foundation. Everything writes through it. Highest-risk (POSIX rename/fsync correctness) ⇒ first, fail fast. Owns the shell-vs-Python helper-language decision. |
| 43.5 | record schemas + oq_id derivation + seq | OQ-record / decision-record / oq-state schema constructors + validators; derive_oq_id; disk-derived seq | The data contract. Every behaviour slice consumes it. Depends only on 43.4 (uses its checksum/canonical-JSON). |
| 43.6 | worker emit + cancel + idempotency short-circuit | oq_emit, oq_cancel; emit step-2 short-circuit; immutability enforcement; oq-state.json flip to awaiting-decision on blocking:true | First worker-side behaviour. Needs the primitive (43.4) and the schemas/id (43.5). |
| 43.7 | parent decide + delivery + decide-once guard | oq_decide; decide-once existence guard; addressed single-slot decision; optional send-prompt nudge (non-correctness) | First parent-side behaviour. Writes decision files the worker-poll slice consumes. Needs 43.4 + 43.5; consumes OQ files produced by 43.6 (test fixtures). |
| 43.8 | worker poll loop + latency + awaiting-decision state | oq_poll_decision loop at POLL_INTERVAL=2 s; at-least-once + idempotent apply; oq-state.json reset to working; 10 s budget | Closes the worker round-trip. Needs the decision writer (43.7) to have something to poll, and the emit/state machinery (43.6). |
| 43.9 | crash/restart re-derivation + fail-closed (worker + parent) | oq_restart_classify (worker); oq_scan_fleet / oq_list_open parent enumeration + set-difference; stateless re-derivation both sides; integrity fail-closed wired through every read | The reliability layer. Sits on top of all read/write surfaces (43.4–43.8) because it re-derives over every record type. |
| 43.10 | crash-injection / latency / restart test harness | The crash-injection rig (interpose between tmp-write and rename), the latency-budget assertion, the restart-seeding harness, provenance-independence (mtime-blanking) tests | The test kinds that cannot be folded into a single behaviour slice because they span primitives + state + restart. Depends on the surfaces they exercise. |
| 43.11 | live-cmux integration + brief fragment + SKILL cross-reference | End-to-end against a live cmux daemon; the worker-facing OQ brief fragment; the session-driver-cmux SKILL.md pointer to the helpers | Phase-B integration. Touches an ID-27-owned surface (session-driver-cmux SKILL.md + --brief convention) ⇒ see Dependency escalations. Last because it needs the whole channel working. |
Why behaviour-owning slices carry their own schema/round-trip tests. Per the loaded skill’s “each task leaves the system in a working state”: 43.6 lands with emit/cancel/immutability unit tests; 43.7 with addressing/decide-once tests; 43.8 with at-least-once/idempotent-apply tests; 43.9 with restart-classification tests. 43.10 is reserved only for the cross-cutting test kinds (crash injection, wall-clock latency, multi-worker restart seeding, provenance independence) that exercise several slices at once and need a shared rig — it is not “all the tests”, it is the tests no single behaviour slice can own.
Helper-language decision (resolves a TECH gap). TECH leaves “shell vs small
Python helper” to PLAN. Decision: shell helpers for the channel surface
(oq-channel.sh exposing the worker/parent functions, consistent with the
existing launch-worker.sh et al.), with a thin Python helper
(oq-canonical.py, stdlib-only) for the two operations shell does badly:
canonical-JSON serialisation and SHA-256 checksum compute/verify. Rationale:
sha256sum over a shell-built JSON string cannot guarantee canonical key
ordering / whitespace normalisation (needed for stable checksum and stable
content_hash), whereas Python json.dumps(obj, sort_keys=True, separators=(",", ":")) + hashlib.sha256 is deterministic in three lines and
uses only stdlib (no new pip package — honours TECH’s dependency-surface
bar). The shell surface owns the filesystem dance (dotfile, fsync, rename,
enumeration); the Python helper owns canonicalisation + checksum only. This split
is fixed in 43.4 so all downstream slices inherit one canonicalisation
implementation.
Dependency graph
Section titled “Dependency graph”All dependencies are sibling-only (within ID-43). Integer ids below are the
Subtask-local ids (4..11) used in the dependencies arrays.
43.4 atomic-publish + integrity helper (id 4) [FOUNDATION] │ ▼ 43.5 record schemas + oq_id + seq (id 5) deps [4] │ ┌──────┴───────┐ ▼ ▼ 43.6 emit 43.7 decide (id 6 deps [5]; id 7 deps [5]) │ │ └──────┬───────┘ ▼ 43.8 worker poll + latency + state (id 8) deps [6,7] │ ▼ 43.9 crash/restart re-derivation (id 9) deps [6,7,8] │ ▼ 43.10 crash/latency/restart test harness (id 10) deps [8,9] │ ▼ 43.11 live-cmux integration + brief (id 11) deps [9] (+ ID-27 — escalated)- 43.4 → 43.5 strictly serial: 43.5’s validators call 43.4’s checksum/canonical-JSON helper.
- 43.6 and 43.7 are parallel (both depend only on 43.5; disjoint files — see ownership map). They are the parallelisation sweet-spot of this Task.
- 43.8 depends on both 43.6 and 43.7 because the poll loop needs the emit/ state machinery (43.6) and something to poll (decision files written via 43.7’s writer, used as test fixtures).
- 43.9 depends on 43.6/43.7/43.8 because restart re-classification is a pure function over all three record types (questions, decisions, oq-state) and must re-derive the poll/awaiting-decision state.
- 43.10 depends on 43.8 and 43.9 — the cross-cutting rig exercises the poll loop (latency), the primitive (crash injection mid-rename), and restart seeding.
- 43.11 depends on 43.9 (the whole channel must classify correctly before a live end-to-end run) and additionally couples to ID-27 (escalated).
Wave / parallelisation plan
Section titled “Wave / parallelisation plan”| Wave | Slices | Mode | Why |
|---|---|---|---|
| W1 | 43.4 | solo | Foundation primitive; highest risk; nothing else can start. |
| W2 | 43.5 | solo | Single data-contract slice; everything downstream consumes it; must land before the parallel pair. |
| W3 | 43.6 ∥ 43.7 | parallel | Disjoint file ownership, both depend only on 43.5. Worker-emit and parent-decide are independent subsystems wired by a shared schema already fixed in 43.5. |
| W4 | 43.8 | solo | Joins 43.6 + 43.7; shared oq-state.json write surface with 43.6 forces serialisation (see ownership map). |
| W5 | 43.9 | solo | Reads/derives over every prior record type; touches both worker and parent read surfaces. |
| W6 | 43.10 ∥ 43.11 | parallel-with-caveat | 43.10 (tests) and 43.11 (live integration + docs) own disjoint files and can run in parallel once 43.9 is merged; 43.11 additionally blocks on the ID-27 escalation being resolved. If the escalation forces a Task-split, 43.11 moves out of ID-43 entirely (see escalations). |
Net parallelism: two genuine parallel opportunities (W3 emit∥decide, W6 tests∥integration). The 43.4→43.5→…→43.9 spine is an irreducible chain because each link consumes the prior link’s produced interface (primitive → schema → state → re-derivation). This matches TECH’s “chain-dependent slices” recommendation; the chain is not artificial, it is the data-flow.
File-ownership map
Section titled “File-ownership map”All new artefacts live under
.claude/skills/session-driver-cmux/scripts/ (channel helpers, beside the
existing five scripts) and a test dir. The OQ records themselves are runtime
artefacts under .claude/cmux-events/<sid>/oq/ and are not “owned” by any slice
(they are written by the helpers at runtime / by test fixtures).
| Slice | Owns (creates/edits) | Shared-file serialisation note |
|---|---|---|
| 43.4 | scripts/oq-channel.sh (creates file; atomic_publish, verify_record, enumeration helpers); scripts/oq-canonical.py (creates; canonical-JSON + checksum) | Creates oq-channel.sh. Later slices append functions to it ⇒ they serialise on this file (see below). |
| 43.5 | oq-channel.sh (+derive_oq_id, next_seq, schema constructors/validators); scripts/tests/oq/test_schema.bats (or .py) | Edits oq-channel.sh — must run after 43.4 (already serial). |
| 43.6 | oq-channel.sh (+oq_emit, oq_cancel); scripts/tests/oq/test_emit_cancel.* | Edits oq-channel.sh. Shares the file with 43.7 ⇒ if run in parallel, the two emit/decide function blocks must be added to disjoint regions and merged by cherry-pick; the Orchestrator either (a) serialises 43.6 before 43.7 on the file, or (b) splits the channel surface into oq-worker.sh + oq-parent.sh at 43.4 time to make W3 truly file-disjoint — recommended (see note). |
| 43.7 | oq-channel.sh (+oq_decide, decide-once guard, send-prompt nudge call); scripts/tests/oq/test_decide.* | Same shared-file caveat as 43.6. |
| 43.8 | oq-channel.sh (+oq_poll_decision); scripts/tests/oq/test_poll_latency.* | Edits oq-channel.sh; also writes oq-state.json reset — shares the oq-state.json write semantics with 43.6 (which writes the awaiting-decision flip). Logical-not-file conflict: both must agree the marker schema (fixed in 43.5) ⇒ serial after 43.6 anyway. |
| 43.9 | oq-channel.sh (+oq_restart_classify, oq_scan_fleet, oq_list_open); scripts/tests/oq/test_restart.* | Edits oq-channel.sh; serial after 43.8. |
| 43.10 | scripts/tests/oq/test_crash_injection.*, test_latency_budget.*, test_restart_seed.*, test_provenance.*; a crash-injection shim (scripts/tests/oq/crash-shim.sh) | Test files only — disjoint from 43.11. Does not edit oq-channel.sh. |
| 43.11 | .claude/skills/session-driver-cmux/oq-brief-fragment.md (creates); scripts/tests/oq/test_integration_live.*; edit .claude/skills/session-driver-cmux/SKILL.md (cross-reference) | Edits an ID-27-owned file (SKILL.md) ⇒ escalation. The brief fragment + integration test are new/disjoint. |
Recommended file split to unlock W3 parallelism (decision for 43.4). Because
43.6 (worker) and 43.7 (parent) are the one true parallel pair but both would
append to a single oq-channel.sh, 43.4 should create the surface as a small
sourced core plus two thin facades:
oq-core.sh—atomic_publish,verify_record, enumeration,derive_oq_id,next_seq, schema validators (owned by 43.4 + 43.5).oq-worker.sh— sourcesoq-core.sh;oq_emit,oq_cancel,oq_poll_decision,oq_restart_classify(owned by 43.6/43.8/43.9 worker funcs).oq-parent.sh— sourcesoq-core.sh;oq_decide,oq_scan_fleet,oq_list_open(owned by 43.7/43.9 parent funcs).
With this split, 43.6 (→oq-worker.sh) and 43.7 (→oq-parent.sh) touch
disjoint files and W3 is cleanly parallel. 43.9 still touches both facades, so
it is serial (as the graph already has it). The Subtask details below assume
this three-file split.
Subtask detail
Section titled “Subtask detail”Each subtask below mirrors the TM-shape record at the foot of this document. The
details here is the Executor’s full dispatch brief — an Executor should not
need to re-read PRODUCT.md/TECH.md to implement it.
43.4 — atomic-publish primitive + integrity helper
Section titled “43.4 — atomic-publish primitive + integrity helper”Delivers/verifies: OQ-INV-3, OQ-INV-5, OQ-INV-25, OQ-INV-27 (mechanism), plus the foundation for OQ-INV-14. Owns the helper-language split.
Build: scripts/oq-core.sh exposing atomic_publish "$dir" "$name" "$payload_json" implementing the exact TECH sequence: write canonical-JSON to
same-directory dotfile "$dir/.$name.tmp.$$.${RANDOM}", fsync the file,
fsync the directory fd, rename tmp→"$dir/$name", fsync the directory fd
again. The dotfile MUST be derived from the target $dir (never a scratch dir)
so tmp+target are same-filesystem. Add verify_record "$file": recompute
checksum via the Python helper, compare to the record’s checksum, validate
schema_version == 1; on any mismatch return non-zero and emit a channel-error
message — fail closed, never skip. Add list_records "$dir": enumerate
"$dir"/*.json excluding dotfiles. Create scripts/oq-canonical.py (stdlib
only): canonicalise = json.dumps(obj, sort_keys=True, separators=(",",":"), ensure_ascii=False); checksum = hashlib.sha256(canonical.encode()).hexdigest();
a verify subcommand. Helper-language decision is fixed here: shell surface +
Python canonical/checksum helper, no new pip/npm package. UK English in all
comments/errors. fsync(dir) note: ordinary fsync meets the crash/reboot bar
(OQ-INV-5); F_FULLFSYNC on Darwin only if power-loss durability is later
required (flag to Liam, not now).
43.5 — record schemas + oq_id derivation + seq
Section titled “43.5 — record schemas + oq_id derivation + seq”Delivers/verifies: OQ-INV-1, OQ-INV-4, OQ-INV-12 (id half), OQ-INV-31; the schema half of OQ-INV-10, OQ-INV-20/21/22.
Build (in oq-core.sh, after 43.4): derive_oq_id task_id phase question context_ref_json → "oq-" + sha256_hex(task_id "|" phase "|" content_hash)[0:16]
where content_hash = sha256_hex(normalised(question) "|" canonicalise(context_ref));
normalised trims + collapses internal whitespace; exclude
worker_id/emitted_at/seq from the id. next_seq "$questions_dir" →
max seq across questions/*.json + 1 (disk-derived, crash-safe), 0 if empty.
Schema constructors + validators for the three records, each stamping
schema_version:1 and a checksum (via 43.4’s helper):
- OQ record —
oq_id, worker_id, seq, emitted_at (UTC ISO-8601), question, urgency∈{low,normal,high}, blocking(bool), context_ref(obj), status∈{open, cancelled}, supersedes(string|null), schema_version, checksum. - Decision record —
oq_id, decided_at, decider_id, outcome∈{answered, deferred,cancelled,abort_task}, answer, directive(obj|null), schema_version, checksum. - oq-state —
worker_id, lifecycle_state∈{working,awaiting-decision}, blocked_on(string|null), checkpoint_ref(obj|null — opaque to channel), updated_at, schema_version, checksum. Validators enforce enums + required fields and feed the fail-closed path. Do NOT specifycheckpoint_refinternal shape (OQ-INV-21 — opaque). Unit tests:oq_iddeterminism (same inputs ⇒ same id across calls/processes; changed question ⇒ different id; changed worker_id/emitted_at/seq ⇒ same id); provenance derives from in-recordseq/emitted_at, not mtime.
43.6 — worker emit + cancel + idempotency short-circuit
Section titled “43.6 — worker emit + cancel + idempotency short-circuit”Delivers/verifies: OQ-INV-2, OQ-INV-6, OQ-INV-7, OQ-INV-8, OQ-INV-9, OQ-INV-12 (emit half), OQ-INV-13, OQ-INV-26 (writer half).
Build (in oq-worker.sh, sources oq-core.sh):
oq_emit— computeoq_id+next_seq; short-circuit (OQ-INV-12): ifquestions/<oq_id>.jsonexists ANDdecisions/<oq_id>.jsondoes not → skip write, return success (idempotent); if decision exists → signal “already resolved, apply decision” rather than re-emit. Elseatomic_publish questions/ <oq_id>.json <record>. Ifblocking:true, after the OQ commits,atomic_publish oq/ oq-state.json {lifecycle_state: 'awaiting-decision', blocked_on:<oq_id>, checkpoint_ref:…}(ordering matters — parent must never seeawaiting-decisionpointing at an unreadable OQ). Ifblocking:false, leave stateworking.oq_cancel—atomic_publish questions/ <oq_id>.json {…,status:'cancelled', supersedes:<oq_id>}overwriting the slot (terminal-state record, originalquestion/emitted_atpreserved); resetoq-state.jsontoworkingif blocked on it. Never delete a record (OQ-INV-6).- Immutability guard (OQ-INV-2): a second
status:openwrite to an existingoq_idis rejected by the short-circuit; the only legal overwrite isstatus:cancelled. Tests: emit→read-back round-trip; immutability rejection; cancel flips slot + parent open-set excludes it; re-emit N times ⇒ one file (oq_iddedup). Touchesoq-worker.shonly — parallel-safe with 43.7.
43.7 — parent decide + delivery + decide-once guard
Section titled “43.7 — parent decide + delivery + decide-once guard”Delivers/verifies: OQ-INV-10 (writer), OQ-INV-11, OQ-INV-14, OQ-INV-15, OQ-INV-17 (writer half), OQ-INV-19, OQ-INV-33 (decide-once half).
Build (in oq-parent.sh, sources oq-core.sh):
oq_list_open "$worker_dir"—{questions/*.json} − {decisions/*.json}, dropstatus:cancelled, sort byseq(FIFO).oq_decide "$worker_dir" "$oq_id" <decision>— decide-once guard (OQ-INV-33): ifdecisions/<oq_id>.jsonexists, refuse (never re-decide); for a cancelled OQ, SHOULD NOT write at all (OQ-INV-13). Elseatomic_publish decisions/ <oq_id>.json <decision>. Decision schema validated (43.5).directiveis data only — noeval/exec path (OQ-INV-19).- Optional non-correctness
send-promptnudge after the decision commits: call the existingsend-prompt.sh“decision ready for <oq_id>”. The file is authoritative; the nudge only wakes the poll. Noreceived/acksartefact is ever written (OQ-INV-11, OQ-Q1=NO). Tests: addressing (X’s worker reads only X’s decision); decide-once refuses a second write;directiveparsed as data; assert noreceived/acksfile on the happy path. Touchesoq-parent.shonly — parallel-safe with 43.6.
43.8 — worker poll loop + latency + awaiting-decision state
Section titled “43.8 — worker poll loop + latency + awaiting-decision state”Delivers/verifies: OQ-INV-16, OQ-INV-17 (apply half), OQ-INV-18, OQ-INV-22, OQ-INV-24 (state half), closes OQ-INV-8 round-trip, OQ-INV-23 (read order it honours).
Build (in oq-worker.sh, after 43.6): oq_poll_decision "$worker_dir" "$blocked_on" — while lifecycle_state == awaiting-decision, every
POLL_INTERVAL (default 2 s): if decisions/<blocked_on>.json exists →
verify_record (checksum/schema, fail-closed) → apply exactly once
(idempotent: applying twice == once, OQ-INV-16/17) → atomic_publish oq/ oq-state.json {lifecycle_state:'working', blocked_on:null} → break. Latency
budget 10 s wall-clock (OQ-INV-18) is the testable behaviour; 2 s cadence is
the knob. Non-blocking OQ decisions are checked opportunistically at phase
boundaries, not in a loop, and applied-if-relevant / discarded (OQ-INV-9/22) —
no latency budget. Tolerate observing the same decision twice (crash after apply
before state reset). Tests: write decision, assert unblock ≤ 10 s; deliver same
decision twice, assert state after two applies == one; assert non-blocking OQ
keeps state working. Shares oq-state.json write semantics with 43.6 ⇒
serial after it.
43.9 — crash/restart re-derivation + fail-closed (worker + parent)
Section titled “43.9 — crash/restart re-derivation + fail-closed (worker + parent)”Delivers/verifies: OQ-INV-20 (parent enumeration), OQ-INV-21, OQ-INV-23, OQ-INV-28, OQ-INV-29, OQ-INV-30, OQ-INV-32, OQ-INV-33 (restart half); fail-closed OQ-INV-27 wired through every read.
Build:
- Worker (
oq-worker.sh):oq_restart_classify "$worker_dir"— for eachquestions/<oq_id>.jsonsorted byseq:status==cancelled⇒ RESOLVED;decisions/<oq_id>.jsonexists ⇒ DECIDED (apply if not yet applied); else UNRESOLVED. Readoq-state.json; ifawaiting-decisionandblocked_onis UNRESOLVED, re-enterawaiting-decisionand resume polling without re-running the OQ-producing work (usecheckpoint_ref, OQ-INV-21). Pure function, no parent involvement (OQ-INV-29/32). - Parent (
oq-parent.sh):oq_scan_fleet— for each<sid>/under.claude/cmux-events/: read only<sid>/oq/oq-state.jsonto answer “which workers are blocked” (OQ-INV-20, no stream read); for blocked ones,oq_list_opengives FIFO order (OQ-INV-23). Open-awaiting set =⋃ {questions/*.json − decisions/*.json − cancelled} filtered blocking:true. Stateless re-derivation — identical for fresh vs long-lived parent (OQ-INV-30). Decide-once guard is disk-derived (“have I decided?” == “doesdecisions/<oq_id>.jsonexist?”, OQ-INV-33). Per-worker isolation (OQ-INV-28) is the directory boundary; cross-worker is the sibling-dir scan. Tests: seed open/decided/cancelled mix, relaunch worker, assert classification matches seeded truth with no parent; seed multiple worker dirs, “restart” parent (fresh process), assert re-derived open-awaiting set == union set-difference. Touches both facades ⇒ serial after 43.8.
43.10 — crash-injection / latency / restart test harness
Section titled “43.10 — crash-injection / latency / restart test harness”Delivers/verifies: hardens OQ-INV-3 (no partial visible), OQ-INV-5/25 (durability across crash), OQ-INV-16/17 (duplicate delivery), OQ-INV-18 (wall clock), OQ-INV-31 (provenance independence), OQ-INV-29/30 (multi-worker restart).
Build (test files only — scripts/tests/oq/):
crash-shim.sh— interpose between the tmp-dotfile write and therename(and a >4 KB record path); a concurrent reader must see either nothing or the complete record, never a truncated one; assert the dotfile tmp is never enumerated.test_crash_injection.*—kill -9after emit returns, re-read, assert OQ present (no loss); deliver same decision twice, assert idempotent apply.test_latency_budget.*— poll loop at default cadence, write decision, assert unblock ≤ 10 s wall-clock; assert thesend-promptnudge shortens never lengthens observed latency, and that dropping the nudge still meets the budget via polling alone (proves nudge is non-correctness-bearing).test_restart_seed.*— multi-worker seeded dirs, fresh parent + fresh worker, assert re-derivation equals a never-crashed view.test_provenance.*— blank/scramble mtimes, assert ordering+provenance derive from in-recordseq/emitted_at(OQ-INV-31). Run as plain filesystem tests — no live cmux daemon. Disjoint test files — parallel-safe with 43.11.
43.11 — live-cmux integration + brief fragment + SKILL cross-reference
Section titled “43.11 — live-cmux integration + brief fragment + SKILL cross-reference”Delivers/verifies: OQ-INV-7/8/18/20/23/24 end-to-end against a live daemon; wires the channel into the orchestration workflow.
Build:
.claude/skills/session-driver-cmux/oq-brief-fragment.md(new) — the short “OQ-escalation” section the parent appends to a sub-orchestrator brief (mirrors the--brief/final_report.yamlconventions): load the channel, emit OQs here, poll for decisions, the two-state contract.scripts/tests/oq/test_integration_live.*— a real sub-orchestrator emits ablocking:trueOQ; the parent’sstop-monitor loop scans (oq_scan_fleet), decides (oq_decide), the worker unblocks — exercising the real<sid>/oq/layout +send-promptnudge +wait-for-fleetcadence together.- Edit
.claude/skills/session-driver-cmux/SKILL.md— add a pointer from the existing “Escalation” section to the helper scripts (the carry-forward “known limitations”/Escalation table is the home). No behavioural change to the five existing scripts. ⚠ This slice edits an ID-27-owned file (SKILL.md) and depends on cmux launch/ send/stop behaviour — see Dependency escalations. It must be the last slice and may be split out of ID-43 if the Orchestrator rules the ID-27 coupling a Task-boundary problem.
Invariant coverage matrix
Section titled “Invariant coverage matrix”Every OQ-INV-N is delivered by at least one slice and verified by a
test in that slice or in the 43.10 cross-cutting harness. “Delivered” = the
mechanism is implemented; “Verified” = a test asserts it.
| Invariant | Delivered by | Verified by |
|---|---|---|
| OQ-INV-1 (record fields) | 43.5 | 43.5 (field presence/round-trip) |
| OQ-INV-2 (immutable; refine via new OQ) | 43.6 | 43.6 (immutability rejection) |
| OQ-INV-3 (atomic emit) | 43.4 | 43.4 + 43.10 (crash injection, no partial visible) |
| OQ-INV-4 (per-worker FIFO) | 43.5 (seq) | 43.5 / 43.7 (oq_list_open sort) |
| OQ-INV-5 (durable) | 43.4 (fsync+rename) | 43.10 (kill -9 then re-read) |
| OQ-INV-6 (append-only / no retract) | 43.6 (cancel = state, no delete) | 43.6 |
| OQ-INV-7 (emit at any phase) | 43.6 (phase-agnostic emit) | 43.11 (live, emit mid-phase) |
| OQ-INV-8 (blocking ⇒ no dependent progress) | 43.6 (state flip) + 43.8 (poll gate) | 43.6 + 43.8 |
| OQ-INV-9 (non-blocking ⇒ continue) | 43.6 (stays working) | 43.6 / 43.8 |
| OQ-INV-10 (≤1 decision; schema) | 43.5 (schema) + 43.7 (single slot) | 43.7 (decide-once) |
| OQ-INV-11 (two-state ack = decision) | 43.7 (no received) | 43.7 (assert no acks artefact) |
| OQ-INV-12 (idempotent emit) | 43.5 (oq_id) + 43.6 (short-circuit) | 43.5 (id determinism) + 43.6 (re-emit dedup) |
| OQ-INV-13 (cancellation) | 43.6 (oq_cancel) | 43.6 + 43.7 (parent skips, stray decision ignored) |
| OQ-INV-14 (decisions durable/atomic) | 43.4 (primitive) + 43.7 | 43.10 (shares crash-injection) |
| OQ-INV-15 (decisions addressed) | 43.7 (filename=oq_id) | 43.7 (addressing test) |
| OQ-INV-16 (at-least-once delivery) | 43.8 (poll until observed) | 43.8 + 43.10 (duplicate delivery) |
| OQ-INV-17 (in-order-per-OQ; first wins) | 43.7 (writer) + 43.8 (apply once) | 43.8 / 43.7 (forced double-decide no-op) |
| OQ-INV-18 (10 s latency budget) | 43.8 (poll cadence) | 43.10 (wall-clock budget) |
| OQ-INV-19 (no executable code in decisions) | 43.7 (directive=data) | 43.7 (no eval path) |
| OQ-INV-20 (parent-observable awaiting-decision) | 43.5 (marker) + 43.9 (scan reads only marker) | 43.9 |
| OQ-INV-21 (persist resume state; checkpoint) | 43.6 (writes checkpoint_ref) + 43.9 (resume) | 43.9 (restart-while-blocked) |
| OQ-INV-22 (non-blocking ⇒ stays working) | 43.6 + 43.8 | 43.8 |
| OQ-INV-23 (deterministic open-OQ listing) | 43.7 (oq_list_open) + 43.9 (scan) | 43.9 / 43.10 (FIFO) |
OQ-INV-24 (stop ⊥ OQ; no stop while blocking-open) | 43.8 (state) + 43.11 (live stop orthogonality) | 43.11 |
| OQ-INV-25 (no loss) | 43.4 (durability) | 43.10 (crash-then-read) |
| OQ-INV-26 (no duplication per parent) | 43.5 (oq_id files) + 43.6 (dedup) | 43.6 (re-emit once) |
| OQ-INV-27 (no silent corruption; fail closed) | 43.4 (verify_record) wired into every read by 43.6/43.7/43.8/43.9 | 43.4 + each consumer (corrupt checksum/field/enum) |
| OQ-INV-28 (per-worker isolation) | 43.9 (directory boundary / sibling scan) | 43.9 (B can’t see A; parent scan does) |
| OQ-INV-29 (worker restart safety) | 43.9 (oq_restart_classify) | 43.9 + 43.10 (seeded relaunch) |
| OQ-INV-30 (parent restart safety, zero in-memory) | 43.9 (oq_scan_fleet set-diff) | 43.9 + 43.10 (fresh parent == long-lived) |
| OQ-INV-31 (observable provenance) | 43.5 (in-record ids/timestamps) | 43.5 + 43.10 (mtime independence) |
| OQ-INV-32 (worker contract) | 43.6 + 43.8 + 43.9 (emit→poll→restart-reclassify) | 43.9 |
| OQ-INV-33 (parent contract) | 43.7 (decide-once) + 43.9 (disk-derived guard) | 43.7 + 43.9 |
Coverage statement: all 33 invariants (OQ-INV-1 … OQ-INV-33) are delivered by
at least one Subtask and have a named verifying test. No invariant is
unmapped. The three ratified open questions (OQ-Q1/Q2/Q3) require no
implementation slice — Q1 (no received) and Q2 (no timeout) are absence
properties verified by 43.7 (no acks artefact) and by the lack of any timer in
43.8/43.9; Q3 (no cap) is a backlog-revisit trigger recorded in “Risks carried
into implementation”.
Dependency escalations
Section titled “Dependency escalations”All inter-Subtask dependencies within ID-43 are sibling-only (verified: every
dependencies array in the record block references only ids 4–11). One
cross-Task coupling surfaces in slice 43.11 and is flagged here rather than
silently encoded:
ESCALATION — potential cross-Task coupling (ID-43.11 ↔ ID-27)
INTENT: Slice 43.11 (live-cmux integration + brief fragment) must (a) EDIT .claude/skills/session-driver-cmux/SKILL.md to add a pointer from its existing "Escalation" section (lines ~498–507, which already reference docs/specs/id-43-oq-escalation/PRODUCT.md) to the new OQ helper scripts; and (b) rely at runtime on ID-27-owned behaviour: launch-worker.sh --brief (copies the brief into the worktree as .cmux-brief.md), send-prompt.sh (the decision nudge), and the parent stop-monitor / wait-for-fleet loop. This is a dependency on session-driver-cmux (Task ID-27) internals/surface, NOT a sibling Subtask of ID-43.
WHY IT IS NOT A SIBLING DEP: SKILL.md and the five scripts are authored/owned by ID-27. ID-43 cannot express "43.11 depends on ID-27.<x>" as a sibling dep without violating the sibling-only constraint.
RECOMMENDATION (Orchestrator's call — do NOT bend the constraint): Option A (preferred — additive, low-risk): treat the SKILL.md edit + brief fragment as a small ADDITIVE cross-reference that ID-27 has already anticipated (its Escalation section explicitly says the OQ channel is "authored in parallel" and points at PRODUCT.md). Land 43.11 as an ID-43 slice but record an explicit Task-level dependency ID-43 → ID-27 in Task.dependencies[], so the ordering lives at the Task level where cross-Task deps belong — NOT as a Subtask dep. The runtime reliance on launch/send/stop is read-only use of stable ID-27 scripts (no edit), which is ordinary consumption, not a code dependency. Option B (if the Orchestrator judges the SKILL.md edit too invasive for an ID-43 slice): SPLIT 43.11 — keep the brief fragment + integration test in ID-43 (they are new/disjoint files), and move the SKILL.md cross-reference edit into ID-27 as a one-line ID-27 Subtask, sequenced after ID-43's helpers land. The Task-level dep then becomes ID-27 → ID-43 for that doc edit.
DECISION REQUIRED BEFORE 43.11 DISPATCH. Slices 43.4–43.10 are unaffected and may proceed regardless of how this resolves. Nothing is blocked except 43.11.No other cross-Task coupling exists. The channel helpers (43.4–43.9) and the
test harness (43.10) are fully self-contained within scripts/ + scripts/tests/
and depend only on POSIX primitives, jq/sha256sum/coreutils, and Python
stdlib — none of which is an ID-N Task.
Risks carried into implementation
Section titled “Risks carried into implementation”From TECH “Risks and mitigations”, plus PLAN-phase additions. Each is assigned to the slice that must encode the mitigation.
| Risk | Impact | Mitigation | Owning slice |
|---|---|---|---|
rename(2) atomicity assumes same filesystem | High (breaks OQ-INV-3) | Helper derives tmp path from the target dir (same-dir dotfile), never a scratch dir; a test asserts tmp+target share a device (stat -f/stat -c %d). | 43.4 (impl) + 43.10 (device test) |
fsync on dir entry platform-sensitive (macOS vs Linux CI) | Medium | Durability target is crash/reboot (OQ-INV-5 wording), which ordinary fsync covers on both; document the macOS power-loss limit. Add F_FULLFSYNC on Darwin only if power-loss durability becomes a requirement — flag to Liam, not now. | 43.4 |
seq derivation is O(n) over questions/*.json per emit | Low (cost not correctness) | Fine for the expected handful of OQs/worker; covered by OQ-Q3 deferral. If runaway emission bites, cache max seq in oq-state.json (still disk-derivable). | 43.5 (note) |
Clock skew on emitted_at/decided_at | Low | Worker + parent share one host/clock; FIFO uses seq not timestamps. Only matters cross-host (out of scope — multi-parent Non-goal). | n/a (documented) |
send-prompt nudge mistaken for source of truth | High (silent correctness dependency on lossy transport) | Contract is explicit: file authoritative, prompt only wakes poll. Test drops the nudge and asserts correctness via polling alone + a code comment. | 43.7 (comment) + 43.10 (test) |
| Two writers to one slot (protocol violation) | Low | Single-parent rule (one orchestrator per worker) + emit short-circuit + idempotent oq_id. OQ-INV-17 first-wins + conflict-as-new-OQ is the backstop; no locking. | 43.7 + 43.8 |
| OQ-Q3 deferral (non-blocking cap) must not be silently dropped | Low now, latent | Recorded here as a backlog-revisit trigger: if the first live integration (43.11) shows runaway non-blocking emission, return OQ-Q3 to the backlog as a real design question (max_open_nonblocking parent-side count). The Checker should confirm this trigger is recorded, not solved. | 43.11 (observe) + ledger |
Pre-ratification external-API check: not applicable. Per TECH “Gaps / open
items” and the Planner empirical-verification rule, this feature cites no
third-party / non-pinned library symbols — it uses POSIX rename/fsync (OS
primitives), jq/sha256sum (already-assumed CLI), Python stdlib (json,
hashlib), and in-repo session-driver-cmux scripts (internal, indexed by
gitnexus/ast-dataflow). OS primitives + standard/already-present tooling are out
of scope for the import-and-call check. Verification result: N/A — no
external/non-pinned symbols cited.
Checkpoints
Section titled “Checkpoints”Per the loaded skill’s checkpoint discipline:
- After 43.5 (data contract frozen):
oq-canonical.pyround-trips;oq_iddeterminism tests green; schema validators reject bad enums. Review before the W3 parallel pair — the schema is the contract both 43.6 and 43.7 build on, so a late schema change would force rework in both. - After 43.8 (worker round-trip closed): a worker can emit blocking, a parent can decide, the worker unblocks within budget — the core happy path works end-to-end at the filesystem level.
- After 43.9 (reliability layer): crash/restart re-derivation green on both sides; this is the last slice before the cross-cutting harness and live integration.
- After 43.10 + 43.11 (complete): all 33 invariants verified; live cmux end-to-end passes; ID-27 escalation resolved and recorded.
Subtask records (TM-shape, for task-list.json ID-43.subtasks)
Section titled “Subtask records (TM-shape, for task-list.json ID-43.subtasks)”The Orchestrator transcribes these into docs/reference/task-list.json under
ID-43’s subtasks array (ids local to Task 43). All dependencies are
sibling-only integers (4–11). All start pending.
[ { "id": 4, "title": "atomic-publish primitive + integrity helper", "description": "Build the single write primitive every record flows through, plus canonical-JSON + checksum + fail-closed read verification. Fixes the helper-language split: shell surface (oq-core.sh) + Python stdlib helper (oq-canonical.py) for canonicalisation/checksum only — no new pip/npm package.", "details": "Mechanism: TECH 'Write protocol' atomic-publish + 'Integrity/fail-closed'. Satisfies OQ-INV-3, OQ-INV-5, OQ-INV-25, OQ-INV-27(mechanism), foundation for OQ-INV-14. OWNS: scripts/oq-core.sh (create), scripts/oq-canonical.py (create). Implement atomic_publish dir name payload: write canonical-JSON to SAME-DIR dotfile \"$dir/.$name.tmp.$$.$RANDOM\" -> fsync(file) -> fsync(dirfd) -> rename(tmp,\"$dir/$name\") -> fsync(dirfd). Dotfile MUST derive from target $dir (never scratch) so tmp+target same-fs => rename atomic. verify_record file: recompute checksum via oq-canonical.py, compare record.checksum, assert schema_version==1; on mismatch/missing-field/bad-enum return non-zero + channel-error msg (FAIL CLOSED, never skip). list_records dir: enumerate \"$dir\"/*.json excluding dotfiles. oq-canonical.py (stdlib only): canonicalise=json.dumps(obj,sort_keys=True,separators=(',',':'),ensure_ascii=False); checksum=hashlib.sha256(canonical.encode()).hexdigest(); plus a verify subcommand. fsync(dir) note: ordinary fsync meets crash/reboot bar (OQ-INV-5); F_FULLFSYNC on Darwin only if power-loss durability later required (flag Liam, not now). UK English in all comments/errors. No new dependency.", "testStrategy": "atomic_publish leaves either nothing or a complete *.json (never a dotfile/truncated file) visible to a concurrent reader; verify_record fails closed (non-zero) on corrupted checksum, missing field, and bad schema_version.", "status": "pending", "dependencies": [] }, { "id": 5, "title": "record schemas + oq_id derivation + seq", "description": "Define and validate the three on-disk records (OQ, decision, oq-state), the deterministic oq_id derivation, and the disk-derived monotonic seq. This is the data contract every behaviour slice consumes.", "details": "Mechanism: TECH 'Record schemas and on-disk layout' + 'oq_id derivation'. Satisfies OQ-INV-1, OQ-INV-4, OQ-INV-12(id half), OQ-INV-31, OQ-INV-10(schema half), OQ-INV-20(marker schema), OQ-INV-21(oq-state schema), OQ-INV-22(lifecycle enum), OQ-INV-26(structural: oq_id is the filename key, one file per oq_id guarantees no duplication). OWNS: oq-core.sh (+derive_oq_id, next_seq, schema constructors+validators), scripts/tests/oq/test_schema.* (create). derive_oq_id task_id phase question context_ref_json = 'oq-'+sha256_hex(task_id '|' phase '|' content_hash)[0:16]; content_hash=sha256_hex(normalised(question) '|' canonicalise(context_ref)); normalised=trim+collapse-internal-whitespace; EXCLUDE worker_id/emitted_at/seq from id (so relaunch+crash-re-emit re-derive same id, OQ-INV-12/29). next_seq questions_dir = max seq across questions/*.json +1 (disk-derived, crash-safe), 0 if empty. Three records, each stamping schema_version:1 + checksum via id-4 helper. OQ: oq_id,worker_id,seq,emitted_at(UTC ISO-8601),question,urgency in{low,normal,high},blocking(bool),context_ref(obj),status in{open,cancelled},supersedes(string|null),schema_version,checksum. Decision: oq_id,decided_at,decider_id,outcome in{answered,deferred,cancelled,abort_task},answer,directive(obj|null),schema_version,checksum. oq-state: worker_id,lifecycle_state in{working,awaiting-decision},blocked_on(string|null),checkpoint_ref(obj|null OPAQUE — do NOT specify shape, OQ-INV-21),updated_at,schema_version,checksum. Validators enforce enums+required fields feeding fail-closed path. UK English.", "testStrategy": "Same (task_id,phase,question,context_ref) yields identical oq_id across calls/processes; changed question yields a different oq_id; changed worker_id/emitted_at/seq yields the SAME oq_id; ordering/provenance derive from in-record seq/emitted_at not mtime.", "status": "pending", "dependencies": [4] }, { "id": 6, "title": "worker emit + cancel + idempotency short-circuit", "description": "First worker-side behaviour: emit an OQ atomically, flip oq-state to awaiting-decision on blocking, cancel via a terminal-state record, and short-circuit re-emission so a crash-then-re-emit is idempotent. Touches oq-worker.sh only (parallel-safe with 43.7).", "details": "Mechanism: TECH 'Worker emit' + 'Cancellation'. Satisfies OQ-INV-2, OQ-INV-6, OQ-INV-7, OQ-INV-8(emit half), OQ-INV-9, OQ-INV-12(emit half), OQ-INV-13, OQ-INV-21(checkpoint_ref write), OQ-INV-22(non-blocking stays working), OQ-INV-26(writer half). OWNS: scripts/oq-worker.sh (create; sources oq-core.sh), scripts/tests/oq/test_emit_cancel.* (create). oq_emit: compute oq_id+next_seq; SHORT-CIRCUIT (OQ-INV-12): if questions/<oq_id>.json exists AND decisions/<oq_id>.json absent => skip write,return success(idempotent); if decision exists => signal 'already resolved, apply decision' not re-emit; else atomic_publish questions/ <oq_id>.json <record>. If blocking:true, AFTER the OQ commits, atomic_publish oq/ oq-state.json {lifecycle_state:'awaiting-decision',blocked_on:<oq_id>,checkpoint_ref:...} (ORDERING: parent must never see awaiting-decision pointing at an unreadable OQ). If blocking:false leave state 'working'. oq_cancel: atomic_publish questions/ <oq_id>.json {...,status:'cancelled',supersedes:<oq_id>} overwriting slot (terminal record; preserve original question/emitted_at); reset oq-state to working if blocked on it; NEVER delete (OQ-INV-6). Immutability (OQ-INV-2): second status:open write to existing oq_id rejected by short-circuit; only legal overwrite is status:cancelled. checkpoint_ref opaque. UK English.", "testStrategy": "Emit then read-back is identical; a second status:open write to an existing oq_id is rejected; cancel flips the slot to status:cancelled and the parent open-set excludes it; re-emitting the same OQ N times yields exactly one questions/<oq_id>.json.", "status": "pending", "dependencies": [5] }, { "id": 7, "title": "parent decide + delivery + decide-once guard", "description": "First parent-side behaviour: list a worker's open OQs in FIFO order, write exactly one addressed decision per oq_id with a decide-once existence guard, and optionally fire a non-correctness send-prompt nudge. Touches oq-parent.sh only (parallel-safe with 43.6).", "details": "Mechanism: TECH 'Parent decide'. Satisfies OQ-INV-10(writer), OQ-INV-11, OQ-INV-14, OQ-INV-15, OQ-INV-17(writer half), OQ-INV-19, OQ-INV-33(decide-once half). OWNS: scripts/oq-parent.sh (create; sources oq-core.sh), scripts/tests/oq/test_decide.* (create). oq_list_open worker_dir: {questions/*.json} minus {decisions/*.json}, drop status:cancelled, sort by seq (FIFO, OQ-INV-4/23). oq_decide worker_dir oq_id <decision>: DECIDE-ONCE GUARD (OQ-INV-33): if decisions/<oq_id>.json exists, refuse(never re-decide); for a cancelled OQ SHOULD NOT write at all (OQ-INV-13); else atomic_publish decisions/ <oq_id>.json <decision>. Validate decision schema (id 5). directive is DATA ONLY — no eval/exec path (OQ-INV-19). Optional NON-CORRECTNESS send-prompt nudge AFTER commit: call existing scripts/send-prompt.sh 'decision ready for <oq_id>'; file is authoritative, nudge only wakes poll. NEVER write a received/acks artefact (OQ-INV-11, OQ-Q1=NO). UK English. NOTE: reading send-prompt.sh is read-only use of an ID-27 script (ordinary consumption, not a code dep).", "testStrategy": "A decision for OQ-X is read only by X's worker (never another OQ's); a second oq_decide for the same oq_id is refused by the existence guard; directive is parsed as data with no eval path; no received/acks file appears on the happy path.", "status": "pending", "dependencies": [5] }, { "id": 8, "title": "worker poll loop + latency + awaiting-decision state", "description": "Close the worker round-trip: poll the addressed decision file on a fixed cadence, apply it exactly once (idempotent), reset oq-state to working, and unblock within the 10 s wall-clock budget. Serial after 43.6 (shares oq-state write semantics).", "details": "Mechanism: TECH 'Worker decision-poll loop'. Satisfies OQ-INV-16, OQ-INV-17(apply half), OQ-INV-18, OQ-INV-22, OQ-INV-24(state half), closes OQ-INV-8 round-trip, honours OQ-INV-23 read order. OWNS: oq-worker.sh (+oq_poll_decision), scripts/tests/oq/test_poll_latency.* (create). oq_poll_decision worker_dir blocked_on: while lifecycle_state==awaiting-decision, every POLL_INTERVAL (default 2s): if decisions/<blocked_on>.json exists => verify_record (checksum/schema FAIL CLOSED) => APPLY EXACTLY ONCE (idempotent: twice==once, OQ-INV-16/17) => atomic_publish oq/ oq-state.json {lifecycle_state:'working',blocked_on:null} => break. Latency budget 10s wall-clock (OQ-INV-18) is the testable behaviour; 2s cadence is the knob (may later tune per urgency without changing contract). Non-blocking OQ decisions checked OPPORTUNISTICALLY at phase boundaries (not a loop), applied-if-relevant/discarded (OQ-INV-9/22), no latency budget. Tolerate observing same decision twice (crash after apply before state reset). Shares oq-state.json write semantics with id-6 => serial after it. UK English.", "testStrategy": "After the parent writes a decision the worker observes it and resets oq-state to working within 10 s wall-clock; delivering the same decision twice leaves worker state identical to delivering once; a blocking:false OQ leaves lifecycle_state at working.", "status": "pending", "dependencies": [6, 7] }, { "id": 9, "title": "crash/restart re-derivation + fail-closed (worker + parent)", "description": "The reliability layer: worker re-classifies every OQ purely from disk on restart and resumes a blocked op without re-running it; parent enumerates fleet state from oq-state markers and re-derives the open-awaiting set as a stateless set-difference. Touches both facades (serial after 43.8).", "details": "Mechanism: TECH 'Crash/restart re-derivation' (worker + parent) + 'Parent enumeration scan'. Satisfies OQ-INV-20, OQ-INV-21, OQ-INV-23, OQ-INV-28, OQ-INV-29, OQ-INV-30, OQ-INV-32, OQ-INV-33(restart half); wires fail-closed OQ-INV-27 through every read. OWNS: oq-worker.sh (+oq_restart_classify), oq-parent.sh (+oq_scan_fleet,oq_list_open used by scan), scripts/tests/oq/test_restart.* (create). WORKER oq_restart_classify worker_dir: for each questions/<oq_id>.json sorted by seq: status==cancelled=>RESOLVED; decisions/<oq_id>.json exists=>DECIDED(apply if not yet applied); else UNRESOLVED. Read oq-state.json; if awaiting-decision and blocked_on UNRESOLVED, re-enter awaiting-decision + resume polling WITHOUT re-running OQ-producing work (use checkpoint_ref, OQ-INV-21). Pure function, NO parent involvement (OQ-INV-29/32). PARENT oq_scan_fleet: for each <sid>/ under .claude/cmux-events/: read ONLY <sid>/oq/oq-state.json to answer 'which workers blocked' (OQ-INV-20, no stream read); for blocked ones oq_list_open gives FIFO (OQ-INV-23). open_awaiting = union {questions/*.json minus decisions/*.json minus cancelled} filtered blocking:true. STATELESS re-derivation identical fresh-vs-long-lived (OQ-INV-30). Decide-once guard disk-derived (OQ-INV-33). Per-worker isolation = dir boundary (OQ-INV-28); cross-worker = sibling-dir scan. UK English.", "testStrategy": "Seeded open/decided/cancelled OQs on disk are classified correctly by a relaunched worker with no parent involvement; a fresh parent (no memory) re-derives the same open-awaiting set as a never-crashed parent; worker B's enumeration never sees worker A's OQs while the parent sibling-scan does.", "status": "pending", "dependencies": [6, 7, 8] }, { "id": 10, "title": "crash-injection / latency / restart test harness", "description": "The cross-cutting test kinds no single behaviour slice can own: crash-injection between tmp-write and rename, wall-clock latency budget, multi-worker restart seeding, and provenance-independence (mtime blanking). Test files only — disjoint from 43.11, parallel-safe.", "details": "Mechanism: TECH 'Testing and validation' (Atomicity/durability crash injection, Latency test, Restart re-derivation tests, Provenance independence). Hardens OQ-INV-3, OQ-INV-5, OQ-INV-25, OQ-INV-16, OQ-INV-17, OQ-INV-18, OQ-INV-31, OQ-INV-29, OQ-INV-30. OWNS (test files only): scripts/tests/oq/crash-shim.sh, test_crash_injection.*, test_latency_budget.*, test_restart_seed.*, test_provenance.* (all create). DOES NOT edit oq-core/worker/parent.sh. crash-shim.sh: interpose between tmp-dotfile write and rename (+a >4KB record path); concurrent reader sees either nothing or the complete record, never truncated; assert dotfile tmp never enumerated; assert tmp+target share a device (stat -f %d / stat -c %d). test_crash_injection: kill -9 after emit returns, re-read, assert OQ present (no loss); deliver same decision twice, assert idempotent apply. test_latency_budget: poll loop default cadence, write decision, assert unblock <=10s wall-clock; assert send-prompt nudge SHORTENS NEVER LENGTHENS observed latency, and dropping the nudge still meets budget via polling alone (proves nudge non-correctness-bearing). test_restart_seed: multi-worker seeded dirs, fresh parent+worker, assert re-derivation equals never-crashed view. test_provenance: blank/scramble mtimes, assert ordering+provenance from in-record seq/emitted_at (OQ-INV-31). Plain filesystem tests, NO live cmux daemon. Run via the project's test runner; bash tests via bats or python harness — match scripts/tests/ convention. UK English.", "testStrategy": "Crash injected between tmp-write and rename never exposes a partial record and the tmp dotfile is never enumerated; the worker unblocks within 10 s by polling alone (nudge dropped); seeded multi-worker dirs re-derive identically on fresh worker and fresh parent.", "status": "pending", "dependencies": [8, 9] }, { "id": 11, "title": "live-cmux integration + brief fragment + SKILL cross-reference", "description": "Phase-B end-to-end against a live cmux daemon, plus the worker-facing OQ brief fragment and a pointer from session-driver-cmux SKILL.md to the helpers. EDITS an ID-27-owned file (SKILL.md) and relies on ID-27 launch/send/stop behaviour — see Dependency escalations; gate dispatch on the Orchestrator's escalation decision.", "details": "Mechanism: TECH 'Proposed changes' items 2+3 + 'Integration (live cmux — Phase B)'. Satisfies OQ-INV-7, OQ-INV-8, OQ-INV-18, OQ-INV-20, OQ-INV-23, OQ-INV-24(stop orthogonality: worker stays in awaiting-decision and does not /exit while a blocking OQ is undecided) end-to-end. OWNS (new/disjoint): .claude/skills/session-driver-cmux/oq-brief-fragment.md (create), scripts/tests/oq/test_integration_live.* (create). EDITS (ID-27-owned): .claude/skills/session-driver-cmux/SKILL.md — add pointer from existing Escalation section (~lines 498-507, already references docs/specs/id-43-oq-escalation/PRODUCT.md) to the new helper scripts; NO behavioural change to the five existing scripts. oq-brief-fragment.md: short 'OQ-escalation' section the parent appends to a sub-orchestrator brief (mirrors --brief/final_report.yaml conventions): load the channel, emit OQs here, poll for decisions, the two-state contract. test_integration_live: a real sub-orchestrator emits blocking:true OQ; parent stop-monitor loop scans (oq_scan_fleet), decides (oq_decide), worker unblocks — exercises real <sid>/oq/ layout + send-prompt nudge + wait-for-fleet cadence. ESCALATION: SKILL.md edit + runtime reliance on launch-worker.sh --brief / send-prompt.sh / stop-monitor is a cross-Task coupling to ID-27 — do NOT encode as a sibling Subtask dep; the Orchestrator records an ID-43->ID-27 Task-level dep (Option A) or splits the SKILL.md edit into an ID-27 Subtask (Option B). Also: observe whether runaway non-blocking emission appears (OQ-Q3 backlog-revisit trigger). UK English. MUST be last; gated on escalation resolution.", "testStrategy": "Against a live cmux daemon a real sub-orchestrator's blocking OQ is scanned, decided, and unblocked end-to-end through the real <sid>/oq/ layout, and the SKILL.md cross-reference resolves to the shipped helper scripts.", "status": "pending", "dependencies": [9] }]