# Marathon Dynamic-Workflow (codex seats) Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Drive the entire spec-gap marathon (audit→fix→review→gate→integrate every not-yet-shipped spec to production-ready) through one resumable `run-plan.js` dynamic workflow whose seats are codex/GPT only.

**Architecture:** `run-plan.js` already has a `seat:'codex'` mode that routes ALL implement+fix through the tested `cx-implement.sh` wrapper (never cursor/north) and all review through `cx.sh`, with seat models read from the session-JSONL `meta`. Three surgical, meta-gated edits add (1) per-risk review-model split, (2) a codex-only review floor, (3) a money-gate toggle. A generator script emits one coarse audit-and-fix task per remaining spec, waved by the registry's existing dependency graph. The orchestrator (haiku glue) runs it wave-by-wave; codex writes in isolated worktrees and lands via PR.

**Tech Stack:** `~/.claude/workflows/run-plan.js` (+ `lib/run-plan-lib.sh`, `lib/cx-implement.sh`, `lib/cx.sh`), `~/.claude/skills/multi-orchestrator/risk-router.sh`, session-state/v1 JSONL, `docs/plans/registry.json`, Node (built-ins only).

---

## Seat configuration (USER-DECIDED — literal, GPT-only)

| Seat | Model | Effort | Source |
|------|-------|--------|--------|
| Implement + fix | `gpt-5.4` | low | `meta.codex_impl_model` / `codex_impl_effort` |
| Review — money/security (auth, money, tax, tenant-isolation) | `gpt-5.5` | low | `meta.codex_review_model_money` (NEW) |
| Review — regular | `gpt-5.3-codex-spark` | low | `meta.codex_review_model_regular` (NEW) |
| Glue / runners (load, reconcile, lease, dispatch, gate0, risk, metagate, commit) | `haiku` | — | `meta.glue_model` |

**Floor:** codex-only — NO opus/sonnet fallback anywhere. **ALREADY hardcoded** in `CHAINS` (every chain `['codex']`, run-plan.js ~240–246); the `meta.review_floor='codex-only'` key is a declarative marker only. T2's residual is the exhausted-vs-reject parking the floor-drop left unhandled.
**Opus money-logic gate:** DISABLED via `meta.money_gate=false` (T3). Currently the `financeGate` (measured finance detector, opus k=1, via tested script) runs unconditionally on money diffs; T3 adds the skip toggle.
**Banned models:** cursor, grok, north, sonnet, opus. The `seat:'codex'` path already skips cursor/north classification; the codex-only `CHAINS` already removed opus/sonnet review tiers; T3 removes the residual opus-k=1 finance detector.

**Money routing is SPEC-derived, not diff-derived (review-pass fix #2).** `risk-router.sh` classifies a diff by scanning its changed `+/-` lines for money/auth sinks — it runs AFTER codex writes, so a money fix that under-trips the sink regex gets a false `LOW` and (with `money_gate=false`) silently routes to the *weak* `gpt-5.3-codex-spark` reviewer with no backstop. run-plan.js's own comment (~:731) warns a confident-wrong LOW skips both the security review and the money gate. Mitigation: the generator (T5) knows each spec's slug a priori and stamps `force_risk:"HIGH"` + `force_money:true` on every spec in the money/auth/tax/tenant-isolation cluster (T4b); the engine (T1) ORs these into the router result, so those specs ALWAYS hit `gpt-5.5` review regardless of what the diff-classifier decides. Spec-identity, not a post-hoc diff scan, decides whether money code gets money review.

> **Accepted residual risk (recorded):** even with forced `gpt-5.5` review on the money cluster, codex/GPT both authors AND reviews the money/security path — cross-model independence is lost, and the opus-backed money-logic detector is off. The `gpt-5.4`-writes / `gpt-5.5`-reviews split + the spec-derived forced routing + the live-dev smoke gate (T8b) + human PR merge are the only safety on the money path. User chose this over a one-opus-gate or double-GPT-review alternative.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 0 | T1 (per-risk review model + spec-derived force_risk/force_money), T2 (exhausted-vs-reject parking — codex-only floor already shipped), T3 (money-gate toggle) | `~/.claude/workflows/run-plan.js` (disjoint regions) | ⚠️ same file — run sequentially T1→T2→T3 |
| 1 | T4 (scope skip-set), T4b (money cluster list), T5 (JSONL generator) | `docs/plans/marathon/skip-set.json`, `docs/plans/marathon/money-cluster.json`, `docs/plans/marathon/gen-marathon-jsonl.mjs` | ✅ no overlap |
| 2 | T6 (generate + validate JSONL) | `docs/plans/2026-06-30-marathon-dynwf.jsonl` | single task |
| 3 | T7 (engine dry-run smoke on a 1-spec subset) | (read-only run) | single task |
| 4 | T8 (execute marathon) + T8b (per-spec live-dev smoke before merge) | per-spec worktrees + dev deploy + PRs | engine-internal dag-parallel; migration-bearing tasks serialized (T5) |

Wave 0's edits touch the same file (`run-plan.js`) → NOT parallelizable; sequential T1→T2→T3. All later waves are single-task gates consuming the prior wave's artifact.

**Commit target for Wave-0 edits:** `~/.claude/workflows/run-plan.js` is tracked in the **`~/.claude` git repo** (verified — `git -C ~/.claude rev-parse` = `/home/user/.claude`), NOT a separate workflows repo and NOT the zync repo. Wave-0 commits land in `~/.claude`. (Earlier draft's "its own repo" note was wrong.)

**`meta.scheduler` for THIS plan's own JSONL:** `sequential` (Wave 0 is single-file-serial; Waves 1–4 chain). The MARATHON's generated JSONL is `dag-parallel` (its per-spec tasks are file-disjoint within a registry wave).

---

## File Structure

- `~/.claude/workflows/run-plan.js` — MODIFY: 3 meta-gated knobs (review-model-by-risk, codex-only floor, money-gate off). Defaults preserved → other workflows unaffected.
- `docs/plans/marathon/skip-set.json` — CREATE: explicit list of already-shipped spec slugs (live-e2e verified this marathon) to exclude.
- `docs/plans/marathon/money-cluster.json` — CREATE: explicit list of money/auth/tax/tenant-isolation spec slugs to force `gpt-5.5` review + migration-serialize (T4b).
- `docs/plans/marathon/gen-marathon-jsonl.mjs` — CREATE: reads `registry.json` + `skip-set.json` + `money-cluster.json` → emits the marathon session JSONL.
- `docs/plans/2026-06-30-marathon-dynwf.jsonl` — GENERATED: session-state/v1 graph (meta + one task per remaining spec).

---

## Task 1: Per-risk codex review-model split

**Wave:** 0 · **Blocks:** T8 · **Blocked by:** —

**Files:**
- Modify `~/.claude/workflows/run-plan.js` — the codex review dispatch (the `--model ${_reviewModel}` call sites, currently lines ~485 + ~494, fed from `meta.codex_review_model` at ~564) and the review-chain selection by risk.

**Contract (pin EXACTLY):**
- New meta reads (after load, alongside existing `_reviewModel`): `_reviewModelMoney = m.meta.codex_review_model_money || _reviewModel`, `_reviewModelRegular = m.meta.codex_review_model_regular || _reviewModel`.
- **Spec-derived risk floor (review-pass fix #2):** the risk used for review-model selection MUST be `effHigh = high || task.force_risk==='HIGH'` and `effMoney = (r && r.money) || task.force_money===true`, where `task.force_risk`/`force_money` come from the task record (set by the generator for the money cluster). The diff-router can only ESCALATE, never DOWNGRADE, a spec the generator marked money. (`high`/`r.money` are the existing router signals at run-plan.js ~:733-735.)
- At each task's review/integrate dispatch, the codex `--model` passed to `cx.sh` MUST be: `(effHigh || effMoney) ? _reviewModelMoney : _reviewModelRegular`.
- Back-compat: when neither new meta key nor `force_*` is set, behavior is byte-identical to today (both resolve to `_reviewModel`).

**Behavior:** money/security specs (generator-stamped `force_risk:HIGH`/`force_money`, OR diff-router HIGH/money) reviewed by `gpt-5.5`; all others by `gpt-5.3-codex-spark`. A money spec whose fix under-trips the diff sink-scan still gets `gpt-5.5` because its spec identity forced HIGH.

**Acceptance (one executable check):**
- Add a focused unit (in `~/.claude/workflows/test-run-plan-scheduler.mjs` style) loading a meta with both keys + (a) a diff-HIGH task, (b) a diff-LOW task, (c) a diff-LOW task carrying `force_money:true`.
- Expected: PASS — (a)→`gpt-5.5`; (b)→`gpt-5.3-codex-spark`; (c)→`gpt-5.5` (spec floor overrides diff-LOW); absent-meta+absent-force → unchanged `_reviewModel`.

- [ ] Write the test covering the four cases above.
- [ ] Implement the meta reads + spec-floor + per-risk model selection at the review/integrate dispatch.
- [ ] Run acceptance → expected output above.
- [ ] Commit (in the `~/.claude` repo): `git -C ~/.claude add workflows/run-plan.js workflows/test-run-plan-scheduler.mjs && git -C ~/.claude commit -m "feat(run-plan): per-risk codex review-model split + spec-derived force_risk/force_money floor"`

---

## Task 2: Exhausted-vs-reject parking (codex-only floor already in place)

**Wave:** 0 · **Blocks:** T8 · **Blocked by:** T1

> **State-of-code note (verified 2026-06-30):** the codex-only floor is ALREADY shipped — `CHAINS` (run-plan.js ~240–246) is `['codex']` on every chain (`reviewHighSec`/`reviewHigh`/`reviewLow`/`integrate`/`commit`); the opus/sonnet floor tiers were already stripped. So the `review_floor` meta + `effectiveChain` pure-selector from the original draft are DEAD WORK — dropped. The ONLY residual is the failure-mode split that the floor-drop left unhandled, plus stale comments that still describe the removed floors.

**Files:**
- Modify `~/.claude/workflows/run-plan.js` — the review caller's terminal branches (~783–784: `if (rv && rv.clean)…` / `if (!rv) return bad('review errored')` / ~791 `if (!reviewed) return bad('review not clean')`) and the integrate dispatch (~494). Fix the stale floor comments at ~761/768/776.

**Contract (pin EXACTLY):**
- `runChain` returns the last link's result; with a single `['codex']` chain that is the codex verdict, or `null`/an `exhausted`-flagged object when codex produced NO verdict (quota/crash — `cx.sh exhausted:true`, run-plan.js ~:117).
- The review (and integrate) handler MUST distinguish two terminal outcomes that today both collapse into `bad()` (terminal fail):
  - **Exhausted** (`rv == null` OR `rv.exhausted === true` — codex gave no verdict): task PARKS as a retryable `BLOCKED` status (re-leasable on the next `run-plan.js` resume), NOT a terminal fail. Use the existing status-write path (the same one that sets `BLOCKED` from the status_enum); do NOT invent a new state.
  - **Reject** (`rv.clean === false` with real `findings` — codex reviewed and rejected): unchanged from today — runs the bounded fixer up to `MAX_REVIEW`, then `bad('review not clean')` fail-closed. NO floor rescue (correct — there is none to rescue with).
- Rationale (load-bearing at scale): codex quota exhaustion across ~161 tasks is near-certain; collapsing exhausted into terminal-fail would stall the fleet on the first rate-limit instead of parking-and-resuming. A real `clean:false` reject must still stop (no silent pass).
- One-line comment at each changed site. Fix the three stale `→ opus …floor` / `→ sonnet …floor` comments (they describe tiers no longer in `CHAINS`).

**Behavior:** an exhausted codex review parks the task `BLOCKED` for retry; a genuine `clean:false` verdict (after `MAX_REVIEW` fix attempts) fails the task closed. No behavior change to clean reviews.

**Acceptance:**
- Run: a focused unit (`~/.claude/workflows/test-run-plan-scheduler.mjs` style) feeding the review-outcome handler (1) `null`, (2) `{exhausted:true}`, (3) `{clean:false, findings:[…]}` after MAX_REVIEW, (4) `{clean:true}`.
- Expected: PASS — (1)&(2) → task status `BLOCKED` (retryable, not terminal `bad`); (3) → terminal fail-closed; (4) → proceeds. Plus: `grep -c "opus.*floor\|sonnet.*floor" run-plan.js` == 0 (stale comments gone).

- [ ] Write the test (four outcomes + stale-comment grep).
- [ ] Implement the exhausted→BLOCKED-park vs reject→fail-closed split at the review + integrate sites; delete the stale floor comments.
- [ ] Run acceptance → expected.
- [ ] Commit (in `~/.claude`): `git -C ~/.claude add workflows/run-plan.js workflows/test-run-plan-scheduler.mjs && git -C ~/.claude commit -m "fix(run-plan): park exhausted codex review as retryable BLOCKED (not terminal); drop stale floor comments"`

---

## Task 3: Money-logic gate toggle

**Wave:** 0 · **Blocks:** T8 · **Blocked by:** T2

**Files:**
- Modify `~/.claude/workflows/run-plan.js` — the dedicated opus money-logic gate (`P.moneyGate` / its dispatch, ~lines 462–465).

**Contract (pin EXACTLY):**
- New meta read: `_moneyGate = (m.meta.money_gate !== false)` (default ON — preserves today).
- When `_moneyGate === false`, the opus money-logic gate step is SKIPPED entirely (not dispatched), and a `log()` line records `money-logic gate: DISABLED by meta.money_gate=false` so the skip is visible, never silent.

**Behavior:** under the marathon meta (`money_gate:false`) the opus detector never runs; money correctness rests solely on the `gpt-5.5` money/security review seat (T1). The skip is logged (No-ignored-signals: a disabled safety gate is announced).

**Acceptance:**
- Run: unit asserting the money-gate dispatch is absent from the planned step list when `money_gate:false`, present when absent/true.
- Expected: PASS.

- [ ] Write the test.
- [ ] Implement the meta-gated skip + log line.
- [ ] Run acceptance → expected.
- [ ] Commit (in `~/.claude`): `git -C ~/.claude add workflows/run-plan.js && git -C ~/.claude commit -m "feat(run-plan): meta.money_gate toggle to disable opus money-logic gate"`

---

## Task 4: Scope skip-set (already-shipped specs)

**Wave:** 1 · **Blocks:** T5 · **Blocked by:** —

**Files:**
- Create `docs/plans/marathon/skip-set.json` — `{ "skip": [ "<slug>", ... ], "reason": "merged+deployed+live-e2e this marathon — re-verify only on regression" }`

**Contract (pin EXACTLY):**
- JSON shape: `{ skip: string[], reason: string }`. Every entry MUST be a real `registry.json` task `slug` (validated by T5).
- Seed `skip` from the shipped clusters (cross-check each against `registry.json` slugs; drop any that don't match a slug): `foundation-auth-rbac`, `session-security`, `onboarding`, `time-management`, `expenses-module`, `contractor-portal`, `crm-support-center`, `reports-analytics`, `accountant-export`, `israeli-tax-reports`, `invoices-core`, `invoice-receipt-document`, `settings-module`, `kb-module`, `tenant-portals`, `recurring-invoices`.

**Behavior:** these specs are excluded from the generated graph because they were brought to production-ready and live-e2e-verified earlier this marathon (see project memory). Tenant-isolation FK/IDOR were cross-cutting sweeps, not single specs — NOT skip entries; their host specs still get a task (the sweep already hardened them, so codex should find little). Anything uncertain → DO NOT skip (re-audit-first default).

**Acceptance:**
- Run: `node -e "const s=require('./docs/plans/marathon/skip-set.json'); const r=require('./docs/plans/registry.json'); const slugs=new Set(r.tasks.map(t=>t.slug)); const bad=s.skip.filter(x=>!slugs.has(x)); if(bad.length){console.error('UNKNOWN SLUGS',bad);process.exit(1)} console.log('skip='+s.skip.length+' all-valid')"`
- Expected: PASS — `skip=<N> all-valid`, exit 0.

- [ ] Author `skip-set.json` from the seed list.
- [ ] Run acceptance (validates every slug exists) → fix any non-matching entry.
- [ ] Commit: `chore(marathon): scope skip-set of shipped specs` *(zync repo; `docs/` is spec/plan content — confirm `docs/plans/` is in the project git allow-list before `git add`)*

---

## Task 4b: Money/security forced-routing cluster

**Wave:** 1 · **Blocks:** T5 · **Blocked by:** —

**Files:**
- Create `docs/plans/marathon/money-cluster.json` — `{ "force_high": [ "<slug>", ... ], "reason": "money/auth/tax/tenant-isolation specs — review by gpt-5.5 + migration-serialize regardless of per-diff risk-router signal" }`

**Contract (pin EXACTLY):**
- JSON shape: `{ force_high: string[], reason: string }`. Every entry MUST be a real `registry.json` task `slug` (validated by T5, same gate as skip-set).
- Seed `force_high` with the in-graph (NOT skipped) specs whose subject is money, auth, tax, billing/ledger, or cross-tenant isolation — the classes where a confident-wrong diff-LOW would silently skip the money review AND (money-gate being off) every money-logic check. Cross-check each against `registry.json` slugs; drop non-matches. Candidates to evaluate against the live registry: `invoices-adapters` (Morning/payment gateway), `payment-link`, `e-invoice`, `recurring-invoices` (if not already shipped/skipped), `invoice-approval-workflow`, `billing`/`platform-billing`, `ledger`/`accounting`, any `tax`/`vat` spec, any `tenant-isolation`/`rbac`/`permissions` spec not in skip-set.
- A slug present in BOTH skip-set and money-cluster is an ERROR (skipped specs generate no task to stamp) — T5 validator rejects the overlap.

**Behavior:** the generator (T5) reads this file and stamps `force_risk:"HIGH"` + `force_money:true` on each listed task, so its review routes to `gpt-5.5` (T1 spec-floor) and it is migration-serialized (T5) even if the codex diff under-trips the router's sink-scan. SPEC-derived, not diff-derived — identity of the spec decides money-criticality, the diff only escalates.

**Acceptance:**
- Run: `node -e "const m=require('./docs/plans/marathon/money-cluster.json'); const sk=require('./docs/plans/marathon/skip-set.json'); const r=require('./docs/plans/registry.json'); const slugs=new Set(r.tasks.map(t=>t.slug)); const bad=m.force_high.filter(x=>!slugs.has(x)); const overlap=m.force_high.filter(x=>sk.skip.includes(x)); if(bad.length){console.error('UNKNOWN',bad);process.exit(1)} if(overlap.length){console.error('SKIP/MONEY OVERLAP',overlap);process.exit(1)} console.log('force_high='+m.force_high.length+' all-valid no-overlap')"`
- Expected: PASS — `force_high=<N> all-valid no-overlap`, exit 0.

- [ ] Author `money-cluster.json` against the live registry slug list.
- [ ] Run acceptance (valid slugs + no skip overlap) → fix any mismatch.
- [ ] Commit: `chore(marathon): money/security forced-routing cluster` *(zync repo; confirm `docs/plans/` allow-listed)*

---

## Task 5: Marathon JSONL generator

**Wave:** 1 · **Blocks:** T6 · **Blocked by:** T4, T4b

**Files:**
- Create `docs/plans/marathon/gen-marathon-jsonl.mjs` — reads `registry.json` + `skip-set.json` + `money-cluster.json`, writes the session JSONL to stdout (or `--out PATH`).

**Contract (pin EXACTLY):**
- Invocation: `node docs/plans/marathon/gen-marathon-jsonl.mjs --out docs/plans/2026-06-30-marathon-dynwf.jsonl`
- Line 1 = `meta` record (session-state/v1) with EXACTLY these seat keys:
  ```json
  {"type":"meta","schema":"session-state/v1","slug":"marathon-dynwf","topic":"Spec-gap marathon — audit+fix every remaining spec to production-ready (codex seats)","scheduler":"dag-parallel","seat":"codex","exec_mode":"ship","land_mode":"pr","base_branch":"master","base_ref":"head","glue_model":"haiku","codex_impl_model":"gpt-5.4","codex_impl_effort":"low","codex_review_model_money":"gpt-5.5","codex_review_model_regular":"gpt-5.3-codex-spark","codex_review_effort":"low","review_floor":"codex-only","money_gate":false,"status_enum":["BLOCKED","PENDING","WORKING","WRITTEN","REVIEWED","COMMITTED"],"truth_rule":"this file is INTENT; git is DONE-NESS — reconcile statuses against git","report_rule":"at run end emit done/blocked/deferred/canceled"}
  ```
  `base_ref:"head"` (not `origin/master`) — isolation worktrees MUST branch from LOCAL master, which leads origin by the Wave-0 + prior-wave commits ([[feedback_worktree_base_staleness]]); a `fresh`/origin base would silently drop them. Also set the repo-side guard `.claude/settings.local.json` `{"worktree":{"baseRef":"head"}}` (gitignored, already set in this repo) — belt-and-suspenders so the engine and the harness agree.
- One `task` record per registry task whose `slug` is NOT in `skip-set.skip`. Each task record fields: `{type:'task', id:'P<NNN>', slug, title, wave:<registry wave>, depends_on:[<registry deps, MINUS any skipped slug, PLUS any migration-serialize edge>], status:'PENDING', spec_file, force_risk?:'HIGH', force_money?:true, migration?:true, requires_decision?:'<gated-id>'}`.
- **Money-cluster stamping (fix #2):** for every task whose `slug` ∈ `money-cluster.force_high`, set `force_risk:"HIGH"` and `force_money:true` on the record. (T1 reads these to force the `gpt-5.5` review seat; the diff-router can only escalate, never downgrade them.)
- **Migration serialization (fix #3):** detect migration-bearing tasks (spec file contains a ```` ```sql ```` block with `ALTER TABLE`/`CREATE TABLE`, OR slug ∈ money-cluster). Set `migration:true` on each. To prevent two migration-bearing tasks running in parallel (colliding on the next migration number + journal file — a real merge hazard at prod), CHAIN them: order the migration-bearing tasks by registry index and add a `depends_on` edge from each to the previous migration-bearing task. They still interleave with non-migration tasks; only the migration writers are serialized. Document this added edge in a one-line `serialize_reason` field on the record so the edge is not mistaken for a real data dependency.
- Task `contract` text (verbatim template, `${slug}`/`${spec_file}` interpolated): *"Bring spec `${slug}` to production-ready. Read `${spec_file}` as source of truth and the implementing code it governs. Find EVERY divergence between spec and implementation; fix the CODE to match the spec. NEVER edit the spec. If you judge the implementation is correct for the real need and the SPEC is what's wrong (do not bend correct code to a bad spec), STOP that item — do NOT change the code to match a spec you believe is wrong — and record it in `spec_doubts[]` with the file/line and why; the high-reasoning layer decides. Add/repair tests for each fixed behavior. Acceptance: `pnpm -w typecheck` and the spec's owning package build both green; new/changed tests pass. Touch only files this spec governs. Your FINAL message MUST be JSON: `{ divergences_found:[{area,detail}], fixed:[{area,files,test}], deferred:[{area,why}], spec_doubts:[{file,line,why}] }` — this ledger is how the review seat and orchestrator judge coverage; an empty `fixed` with non-empty `divergences_found` and no `deferred`/`spec_doubts` is a FAIL (work claimed but not done)."*
- **Review prompt augmentation (fix #4):** for these marathon tasks the review seat (T1 dispatch) MUST receive the task's `spec_file` path + the implementer's returned ledger JSON, and be told to judge whether `divergences_found` is plausibly complete and every entry is either in `fixed` (with a test) or justified in `deferred`/`spec_doubts` — not just whether the diff is internally clean. An unfalsifiable "done" (clean diff, empty ledger, but obvious spec gaps) is a `clean:false`.
- Emit a `gated` record (`status:"OPEN"`, `category:"irreversible"`, `blast_radius`, `options:["proceed","abort"]`) for the prod-DB/migration concern, and link it via `requires_decision` from every task with `migration:true`. One shared gate id `g-prod-migrations` is acceptable (migrations apply to prod only at ship).
- Deterministic: no `Date.now()`/`Math.random()`; stable ordering by registry index. `try/catch`, handle missing files, Node + Bun compatible.

**Behavior:** produces a resumable dag-parallel graph that reuses the registry's proven wave/dependency topology, minus shipped specs, with the codex seat config baked into meta and money/migration risk surfaced as a gate.

**Acceptance:**
- Run: `node docs/plans/marathon/gen-marathon-jsonl.mjs --out $CLAUDE_JOB_DIR/tmp/m.jsonl && node -e "const fs=require('fs');const L=fs.readFileSync(process.env.CLAUDE_JOB_DIR+'/tmp/m.jsonl','utf8').trim().split('\n').map(JSON.parse);const meta=L[0];if(meta.type!=='meta'||meta.seat!=='codex'||meta.codex_impl_model!=='gpt-5.4'||meta.glue_model!=='haiku'||meta.review_floor!=='codex-only'||meta.money_gate!==false||meta.base_ref!=='head')throw new Error('bad meta');const tasks=L.filter(r=>r.type==='task');const slugs=new Set(tasks.map(t=>t.slug));const sk=require('./docs/plans/marathon/skip-set.json');for(const t of tasks)for(const d of t.depends_on)if(!slugs.has(d)&&!sk.skip.includes(d))throw new Error('dangling dep '+d+' in '+t.slug);const mc=require('./docs/plans/marathon/money-cluster.json');for(const s of mc.force_high){const t=tasks.find(x=>x.slug===s);if(t&&(t.force_money!==true||t.force_risk!=='HIGH'))throw new Error('unstamped money task '+s)}const mig=tasks.filter(t=>t.migration);for(let i=1;i<mig.length;i++)if(!mig[i].depends_on.includes(mig[i-1].slug))throw new Error('migration not serialized at '+mig[i].slug);console.log('tasks='+tasks.length+' meta-ok deps-ok money-stamped='+mc.force_high.length+' mig-serialized='+mig.length)"`
- Expected: PASS — `tasks=<N> meta-ok deps-ok money-stamped=<M> mig-serialized=<K>`. (Dangling-dep check: every dependency is another in-graph task or an intentionally-skipped shipped spec. Money tasks all carry `force_risk/force_money`; migration tasks form a single dependency chain.)

- [ ] Write the generator per the contract.
- [ ] Write the acceptance check (above) and run it.
- [ ] Commit: `feat(marathon): session-JSONL generator from registry + skip-set`

---

## Task 6: Generate + validate the marathon JSONL

**Wave:** 2 · **Blocks:** T7 · **Blocked by:** T5, T4

**Files:**
- Generated: `docs/plans/2026-06-30-marathon-dynwf.jsonl`

**Contract:** run T5's generator to the real output path; the file MUST parse as one JSON object per line, meta first, and pass T5's acceptance check against the committed `skip-set.json`.

**Acceptance:**
- Run: `node docs/plans/marathon/gen-marathon-jsonl.mjs --out docs/plans/2026-06-30-marathon-dynwf.jsonl` then T5's validator pointed at it.
- Expected: PASS — `tasks=<N> meta-ok deps-ok`.

- [ ] Generate the JSONL.
- [ ] Validate (meta keys + no dangling deps).
- [ ] Commit: `chore(marathon): generate marathon-dynwf session JSONL`

---

## Task 7: Engine dry-run smoke (1-spec subset)

**Wave:** 3 · **Blocks:** T8 · **Blocked by:** T6, T3

**Files:** none written (read-only / disposable worktree).

**Contract:** prove the wired engine consumes the meta correctly END-TO-END on ONE low-risk spec before committing the full fleet. Build a 1-task copy of the JSONL (the lowest-risk un-shipped leaf spec, e.g. a static/UI spec with no money/auth), run `run-plan.js` against it, and confirm from logs: implement dispatched via `cx-implement.sh --model gpt-5.4 --effort low`; review via `cx.sh --model gpt-5.3-codex-spark`; NO cursor/north/opus/sonnet/grok process spawned; glue seats ran on haiku.

**Acceptance:**
- Run the engine on the 1-task subset; `grep` the run log.
- Expected: log shows `gpt-5.4` implement + `gpt-5.3-codex-spark` review + zero matches for `cursor|north|opus|sonnet|grok` in dispatch lines; task reaches `COMMITTED` (or a clean fail-closed stop with a real verdict).

- [ ] Build 1-task subset JSONL (lowest-risk leaf spec).
- [ ] Run `run-plan.js`; capture log.
- [ ] Assert seat models + no banned models + terminal status.
- [ ] (No commit — validation run; revert any worktree.)

---

## Task 8: Execute the marathon

**Wave:** 4 · **Blocks:** — · **Blocked by:** T7

**Files:** per-spec isolated worktrees → per-spec PRs to `master` (land_mode=pr).

**Contract:** run `run-plan.js` against `docs/plans/2026-06-30-marathon-dynwf.jsonl` to completion, wave by wave, resumable. The engine handles lease/reconcile/gate0/review/integrate per task. The orchestrator (haiku glue) babysits; the human merges PRs. Honor every `requires_decision` gate (`g-prod-migrations`): migrations apply to prod ONLY at ship, after the gate is answered `proceed`.

**Behavior:** money/security specs auto-route (risk-router) to the `gpt-5.5` review seat; the rest to `gpt-5.3-codex-spark`. No opus/sonnet anywhere; codex-only floor means an exhausted review fails the task closed (no silent rescue) — those tasks surface for manual attention. `spec_doubts[]` from any task escalate to the orchestrator/user (spec edits are never made by the implement seat).

**Acceptance (per task, engine-enforced):** `pnpm -w typecheck` + owning-package build green; review verdict `clean:true`; gate0 no-warn clean; integrated on the plan branch; PR opened.
- Overall: every in-graph task reaches `COMMITTED` or is explicitly `BLOCKED`/`deferred` in the end-of-run report.

- [ ] Launch `run-plan.js` on the marathon JSONL.
- [ ] Answer `g-prod-migrations` gate per migration-bearing spec at ship.
- [ ] Review the end-of-run brief; re-dispatch any `BLOCKED` task (smaller scope) or escalate `spec_doubts`.
- [ ] Per-spec PRs land by human merge (NOT auto).

---

## Task 8b: Live-dev smoke before merge (per migration/money spec)

**Wave:** 4 · **Blocks:** — · **Blocked by:** T8

**Files:** none written (verification + report only; orchestrator-run, not a codex seat).

**Contract:** the code gate (typecheck + build + codex review) CANNOT see prod schema, prod secrets, or runtime 500s — EVERY prior marathon wave's load-bearing bugs (partial-index ON CONFLICT predicate, JS-Date→timestamptz, phantom columns, presigned-PUT 400, fire-and-forget killed post-response) were caught ONLY by live e2e, never by the gate ([[project_invoices_core_wave]], [[project_israeli_tax_reports_wave]], [[project_settings_profile_wave]]). So for every task with `migration:true` OR `force_money:true`, a PR is NOT merge-ready on a green code gate alone. Before human merge: deploy the branch to dev (api + app), apply its migration to the dev DB (= prod Neon data), and run a live smoke of the spec's core path against `app.dev.zync.is`; capture the result (`N/N PASS` + worker version) in the PR. The orchestrator (opus) performs deploy/migration/e2e — NOT the haiku glue, NOT a codex seat (matches the IRON LAW: orchestrator owns deploy/migration/e2e).

**Behavior:** non-migration, non-money specs (pure UI/static/read-only) merge on the green code gate + review (no live smoke required — nothing prod-schema or money-path to surprise). Migration/money specs get the live gate. A live smoke FAIL re-opens the task as `BLOCKED` (the gate passed but reality didn't) and the divergence is fixed before re-deploy. This is a HUMAN-merge gate, not engine-automated: the engine opens the PR; the orchestrator runs the smoke and posts PASS/FAIL; the human merges only PASS.

**Acceptance:**
- For each migration/money PR: branch deployed to dev, migration applied to dev DB, live smoke run against `app.dev.zync.is`.
- Expected: `N/N PASS` recorded in the PR before merge; any FAIL re-blocks the task. Non-migration/non-money PRs: code gate + review green is sufficient (state this explicitly in the PR so the skip is visible, not silent).

- [ ] Classify each opened PR: migration/money → live gate; else → code-gate-only (note in PR).
- [ ] For live-gate PRs: deploy dev, apply migration, run spec smoke, post `N/N PASS`.
- [ ] Re-block + fix any live FAIL; re-smoke after fix.
- [ ] Hand merge-ready (live-PASS or code-gate-only) PRs to the human.

---

## Gated Decisions

- **`g-prod-migrations`** — `category:irreversible`, `blast_radius:"prod Neon zync-prod schema (purple-haze-75517629) — applied migrations are forward-only; a bad DDL corrupts live tenant/money data"`, `needs:"human go/no-go before any spec's migration is applied to prod at ship"`, `options:["proceed","abort"]`, `source:"author"`. Linked from every migration-bearing spec task. Resolved at SHIP, per spec, never at author time.
- **Seat/floor policy** — RESOLVED at plan time by the user (GPT-only, single review, codex-only floor, money-gate off). Recorded under "Seat configuration" with the accepted-risk note. No run-level gate needed; the choice is baked into `meta`.

---

## Self-Review

**1. Spec coverage (of the request "a dynwf for the entire marathon, haiku runners, only the 3 GPT seats"):**
- Implement seat gpt-5.4/low → meta `codex_impl_model`/`codex_impl_effort` (T6 meta). ✓
- Money/security review gpt-5.5/low → T1 per-risk split + risk-router. ✓
- Regular review gpt-5.3-codex-spark/low → T1. ✓
- Haiku runners → meta `glue_model:haiku` (already honored; T6). ✓
- No cursor/grok/north → `seat:'codex'` skips classification; no grok/north code path. ✓
- No sonnet/opus → codex-only `CHAINS` ALREADY shipped (T2 reduced to exhausted-parking + stale-comment cleanup) + T3 disables the opus-k=1 finance detector. ✓
- "Entire marathon" → T4 skip-set + T5 generator over all 177 registry specs minus shipped. ✓

**2. Vagueness/body-bloat scan:** Wave-0 tasks are contract-only (implementer writes the diff + test). No literal bodies pasted (each edit > its contract). No "handle edge cases" without naming — the codex-only exhaustion behavior and the money-gate skip-log are named explicitly. Generator contract pins the exact meta line + task shape verbatim.

**3. Contract/seam consistency:** meta keys are identical across T1/T3 (`codex_review_model_money`, `codex_review_model_regular`, `money_gate`) and the T5 generator's emitted meta and the T6/T7 assertions. `review_floor` is emitted as a declarative marker only (the codex-only `CHAINS` are hardcoded, not meta-driven — T2 reads nothing from it). risk-router signal (`RISK`/`MONEY`) is the single source feeding both chain-selection (existing) and review-model selection (T1).

**4. Wave plan check:** Wave 0's three tasks share `run-plan.js` → serialized T1→T2→T3 (flagged, not parallel). Wave 1 = T4/T4b/T5 (skip-set + money-cluster are file-disjoint authoring; T5 reads both → after them). Waves 2–4 chain on artifacts (T8b after T8). This plan's own `meta.scheduler=sequential`; the GENERATED marathon JSONL is `dag-parallel` (per-spec tasks file-disjoint within a registry wave — the engine's lease/worktree isolation enforces it), EXCEPT migration-bearing tasks which are chained (T5 serialize edge) to avoid migration-number/journal collision.

**Caveats surfaced (not silently passed):** (a) coarse per-spec unit — codex low-effort auditing a whole spec is larger than the proven bounded-embed seam; T7 smoke + the codex-only fail-closed floor + the ledger-coverage review (T5 fix #4) are the guards, and any `BLOCKED` task is re-dispatched at smaller scope. (b) Editing the global `run-plan.js` — all three edits are meta-gated with byte-identical defaults, so other workflows are unaffected; their commits land in the `~/.claude` repo, not zync. (c) GPT writes AND reviews the money path (user-chosen, opus money-gate OFF) — the gpt-5.4-write / gpt-5.5-review split is the sole independence boundary; the T8b live-dev smoke on every money/migration spec is the real backstop, since prior waves' money bugs were caught ONLY live, never by any code reviewer. (d) Spec-wrong handling — the contract forbids the implement seat from bending correct code to a bad spec or editing the spec; it STOPS and reports `spec_doubts[]`, the high-reasoning layer decides ([[feedback_spec_is_source_of_truth]]).
