# Pre-flight Decision Gate 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:** Surface every foreseeable human decision in a run-plan BEFORE the Workflow launches, record answers as durable pre-authorizations in the plan JSONL, and have the engine fail-closed HALT on any unresolved decision regardless of launch path.

**Architecture:** No new record type — enrich the existing `gated` record with a lifecycle (`status`/`answer`/`options`/`category`/`blast_radius`) and link it from tasks via `task.requires_decision` (per-task binding) or from `meta` via `gated.binds_meta` (run-level binding). The main-thread launcher (`run-plan/SKILL.md`) presents one consolidated `AskUserQuestion` sheet and writes answers back; the engine (`run-plan.js`) enforces via a shared `decisionGate(t)` predicate + load-time validation. Planner (`plan`/`brainstorm`) authors decisions up front.

**Tech Stack:** Node.js (run-plan.js Workflow controller + `.mjs` test harness), agent-facing Markdown SKILLs.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1, Task 2, Task 3, Task 4 | `workflows/run-plan.js` + `workflows/lib/test-run-plan-scheduler.mjs` (T1); `skills/brainstorm/SKILL.md` (T2); `skills/plan/SKILL.md` (T3); `skills/run-plan/SKILL.md` (T4) | ✅ four disjoint file sets, zero semantic import edge |

**Execution Strategy:** `dag-parallel` — wave 1 holds 4 independent file-disjoint tasks (a real parallel round). Stamp `meta.scheduler="dag-parallel"` in the session JSONL. (Dogfoods the parallel executor.)

> Coupling note: the four tasks share the spec as source of truth but have NO runtime/import dependency and NO file overlap — engine tests use mocks, docs point at the spec, never at each other. Per wave rules → same wave.

---

## File Structure

- `workflows/run-plan.js` — engine. Extend `S_MANIFEST` (decision fields survive validation), `P.load` (parse `gated`), add `decisionGate(t)` + load validation + answer injection. Trust boundary.
- `workflows/lib/test-run-plan-scheduler.mjs` — engine test harness. Add decision-gate scenarios + `scenario.gated` mock support.
- `skills/brainstorm/SKILL.md` — session-state/v1 schema doc (the record vocabulary).
- `skills/plan/SKILL.md` — planner decision-enumeration pass.
- `skills/run-plan/SKILL.md` — main-thread pre-flight gate procedure.

---

### Task 1: Engine enforcement + tests (`run-plan.js` + scheduler harness)

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

**Files:**
- Modify `workflows/run-plan.js`:
  - `S_MANIFEST` (lines 21-50) — add decision fields so the load agent's output is not stripped by `additionalProperties: false`.
  - `P.load` (lines 135-142) — instruct it to parse `type:gated` lines.
  - After `const m = await sub(P.load()…)` (line 315) — load-time decision validation.
  - Near `depsReady` (line 445) — add shared `decisionGate(t)`.
  - `runSequential` (before lease, line 474) and `runDagParallel` (before lease, line 502) — call `decisionGate`.
  - `P.implement` (line 200) and `P.implementNorth` (line 168) — inject the resolved answer.
- Modify `workflows/lib/test-run-plan-scheduler.mjs` — `scenario.gated` mock + decision cases.

**Contract (pin EXACTLY):**

`S_MANIFEST` gains:
- top-level property `gated` (array, OPTIONAL — absent ⇒ legacy plan):
  ```
  gated: { type: 'array', items: { type: 'object', additionalProperties: false,
    required: ['id','status'],
    properties: {
      id:{type:'string'}, category:{type:'string'}, needs:{type:'string'}, why:{type:'string'},
      blast_radius:{type:'string'}, options:{type:'array',items:{type:'string'}},
      default:{type:['string','null']}, status:{type:'string'}, answer:{type:['string','null']},
      resolved_by:{type:['string','null']}, source:{type:'string'}, binds_meta:{type:'string'} } } }
  ```
- task item property `requires_decision: { type: ['string','null'] }` (added to the existing task `properties`).

`P.load` prompt — add a numbered step: *"parse every `type:gated` line into `gated:[...]` (copy all fields verbatim); a plan with no gated lines ⇒ omit the field."*

`decisionGate(t)` — module-scope, alongside `depsReady`:
- Signature: `const decisionGate = t => <string|null>` (returns a HALT reason, or `null` when clear).
- Behavior: `t.requires_decision` absent ⇒ `null`. Else look it up in a `Map` of `m.gated` by id: missing ⇒ reason `task ${t.id} decision ${id} unresolved (no record)`; `status !== 'RESOLVED'` ⇒ reason `… unresolved`; `answer === 'abort'` ⇒ reason `… = abort (user stopped run)`; otherwise `null`.

Load-time validation block (after load, deterministic, `return halt('Preflight', why, ctx)` on any failure):
- build `gMap = new Map((m.gated||[]).map(g => [g.id, g]))`; a duplicate id ⇒ HALT.
- any `gated` with non-null `answer` AND non-empty `options` where `answer ∉ options` ⇒ HALT.
- any `task.requires_decision` not present in `gMap` ⇒ HALT.
- any `gated` with `binds_meta` and `status !== 'RESOLVED'` ⇒ HALT (run-level decision left OPEN).

Scheduler integration — in BOTH `runSequential` (after the deps/BLOCKED checks, line 472-473) and `runDagParallel` (in the A1 lease loop, before `P.lease`, line 501-502): `const dh = decisionGate(t); if (dh) return halt('Preflight', dh, t)`.

Answer injection — in `P.implement` and `P.implementNorth`, when `t.requires_decision` resolves to a `gated` in `m.gated`, append one line to the dispatched prompt: `PRE-AUTHORIZED DECISION: <needs> → answer="<answer>". Apply this choice.` (Convert the arrow fn to a block body if needed to look the record up; do NOT thread new params.)

**Behavior:**
- Legacy plan (no `gated`, no `requires_decision`) ⇒ every check is vacuous; behavior byte-identical to today.
- Per-task OPEN/missing/abort decision ⇒ HALT before lease, in either scheduler.
- Structural corruption (dup id, `answer ∉ options`, dangling ref, OPEN `binds_meta`) ⇒ HALT at load, before any task runs.
- All-resolved plan ⇒ no HALT; resolved answer reaches the implementer prompt.

**Acceptance (one executable check):**
- Run: `node ~/.claude/workflows/lib/test-run-plan-scheduler.mjs`
- Expected: PASS — `N passed, 0 failed`, exit 0, including the new decision cases below.

New test cases the harness MUST gain (extend `ok.manifest` + `defaultResponder` so `scenario.gated` becomes `man.gated`):
- requires_decision → OPEN gated ⇒ HALT, no `lease:` call — assert in BOTH `sequential` and `dag-parallel`.
- requires_decision → RESOLVED (answer e.g. `"proceed"`) ⇒ run completes, task committed.
- requires_decision → RESOLVED answer `"abort"` ⇒ HALT, reason matches `/abort/`.
- requires_decision → id absent from `gated` ⇒ HALT at load (no `lease:`).
- duplicate `gated.id` ⇒ HALT at load.
- `answer ∉ options` ⇒ HALT at load.
- `binds_meta` gated with `status:"OPEN"` ⇒ HALT at load.
- all-resolved plan ⇒ full run completes (idempotent-resume proof).

- [ ] Write the test cases above (implementer writes the test code + mock changes)
- [ ] Implement the engine contract to satisfy them (implementer writes the bodies)
- [ ] Run acceptance check → `N passed, 0 failed`
- [ ] Commit: `git add workflows/run-plan.js workflows/lib/test-run-plan-scheduler.mjs && git commit -m "preflight gate: engine enforces resolved decisions, fail-closed + tests"`

---

### Task 2: Schema doc — enriched `gated` + `requires_decision` (`brainstorm/SKILL.md`)

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

**Files:**
- Modify `skills/brainstorm/SKILL.md` — the "Compact-Proof Session File" → Schema section (the `session-state/v1` record vocabulary, ~lines 220-234).

**Contract (pin EXACTLY — these are the literal schema seams the doc must show):**
- Enriched `gated` record line (additive; absent fields ⇒ today's behavior):
  `{"type":"gated","id":"g1","category":"irreversible|fork|input|policy|architecture","needs":"…","why":"…","blast_radius":"…","options":["proceed","abort"],"default":null,"status":"OPEN|RESOLVED","answer":null,"resolved_by":null,"source":"author|scan","binds_meta":null}`
- Task field: `requires_decision` — OPTIONAL string, the `gated.id` a task is gated on; `deps` stays strictly task→task.
- A short unified-model note: three old ways to say "needs a human" collapse to one — `deps` (task→task), `requires_decision` (per-task human decision), `gated.binds_meta` (run-level decision into a meta field). NO new `decision` record type.
- One pointer line: planner authors these at plan time — see `[[plan]]` (decision-enumeration pass). Do NOT restate the enumeration rule here.

**Behavior:** Document fields + the model; additive/back-compat (`status` absent ⇒ OPEN). Follow agent-doc-authoring register (caveman prose; HARD FLOOR: keywords/identifiers/JSON verbatim). Point, never re-inline plan's rule.

**Acceptance:**
- Run: `grep -c 'requires_decision\|binds_meta\|"status":"OPEN' skills/brainstorm/SKILL.md`
- Expected: ≥1 each; the enriched `gated` JSON line and `requires_decision` field both present.

- [ ] Invoke agent-doc-authoring, then edit the schema section per the contract
- [ ] Run acceptance grep → fields present
- [ ] Commit: `git add skills/brainstorm/SKILL.md && git commit -m "preflight gate: session-state schema gains gated lifecycle + requires_decision"`

---

### Task 3: Planner decision-enumeration pass (`plan/SKILL.md`)

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

**Files:**
- Modify `skills/plan/SKILL.md` — add a new section (near "Execution Strategy Selection") defining the decision-enumeration pass.

**Contract (pin the procedure — prose, this is the canonical source other docs point to):**
- After waves are assigned, for EVERY task ask: would it need a human? Source of truth for "what needs a human" = the project's `## Pre-flight gate policy` section in its `CLAUDE.md` if present, ELSE the baseline categories `irreversible | fork | input | policy | architecture`. Baseline is the floor; the project section is additive and can never silence an irreversible destructive op.
- For a per-task decision: author a `gated` record (`status:"OPEN"`, `source:"author"`, `category`, `needs`, `why`, `blast_radius` MANDATORY when `irreversible`, `options`) and link it from the task via `requires_decision`.
- For a run-level landing/policy decision (push-vs-PR, base branch, auto-merge): author a `gated` with `binds_meta:"<meta field>"` (e.g. `base_branch`, `exec_mode`), NO task link.
- Forks: a **param-only** fork (answer changes a value, graph unchanged) ⇒ ONE task `requires_decision` + the answer injected as input. A **graph-changing** fork (answer would create different tasks/deps) ⇒ resolve it HERE, at plan time (ask the user during `plan`); NEVER defer to the gate.

**Behavior:** Decision rule first (BLUF), rationale after. agent-doc-authoring register. Cross-reference `[[brainstorm]]` schema for the record shape; do NOT restate field definitions.

**Acceptance:**
- Run: `grep -c 'Pre-flight gate policy\|requires_decision\|binds_meta' skills/plan/SKILL.md`
- Expected: ≥1 each; the enumeration pass section present with the per-project policy source named.

- [ ] Invoke agent-doc-authoring, then add the enumeration-pass section per the contract
- [ ] Run acceptance grep → present
- [ ] Commit: `git add skills/plan/SKILL.md && git commit -m "preflight gate: planner enumerates human decisions at plan time"`

---

### Task 4: Main-thread pre-flight gate procedure (`run-plan/SKILL.md`)

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

**Files:**
- Modify `skills/run-plan/SKILL.md` — insert a pre-flight decision-gate step between the existing "Pre-flight (cheap, MUST pass)" file-existence check (step 3) and the template-and-launch step.

**Contract (pin the procedure — main-thread, runs BEFORE the `Workflow()` call):**
1. Native Read the JSONL; collect all `gated` with `status:"OPEN"`.
2. Scan backstop — run ONLY if `meta.preflight.task_graph_hash` is absent or differs from the current task-graph hash. Spawn ONE `sonnet` agent; input = task list (descriptions + each task's `files[]` hints) + the project's `## Pre-flight gate policy` (or baseline). NOTE in the doc: no diffs exist pre-execution — the scan reasons over task descriptions, so catchability is bounded by description fidelity. It appends decision points not already covered as `gated … "source":"scan","status":"OPEN"`. Bias: over-flag.
3. If any OPEN decisions: present ONE consolidated `AskUserQuestion` sheet — each shows needs / why / blast_radius / options, recommended option first.
4. Write each answer back to its `gated` line: `status:"RESOLVED"`, `answer`, `resolved_by:"user"`. For a `binds_meta` decision, ALSO write the answer into the named `meta` field.
5. Stamp `meta.preflight = {"task_graph_hash": <hash of task ids+descs+requires_decision>}`.
6. Proceed to template-and-launch.
- State plainly in the doc: the sheet is UX; the engine (`run-plan.js`) is the trust boundary. The gate removes halts for foreseeable decisions ONLY — NOT failure-driven correctness halts (merge conflict, gate red, MAX_FIX/MAX_REVIEW), and NOT truly-unforeseen decisions (no `gated` record ⇒ engine can't HALT on them).
- Idempotency note: an LLM scan is non-deterministic; gating it behind the graph hash keeps headless/cron resume clean (unchanged graph ⇒ scan skipped ⇒ all-resolved ⇒ gate is a no-op).

**Behavior:** Numbered ladder, BLUF. agent-doc-authoring register. Note resume-plan routes every multi-wave plan through this launcher → single entry point.

**Acceptance:**
- Run: `grep -c 'pre-flight decision gate\|AskUserQuestion\|task_graph_hash\|binds_meta' skills/run-plan/SKILL.md` (case-insensitive: add `-i`)
- Expected: ≥1 each; the gate step present with scan-idempotency + the UX-vs-trust-boundary honesty note.

- [ ] Invoke agent-doc-authoring, then insert the gate step per the contract
- [ ] Run acceptance grep → present
- [ ] Commit: `git add skills/run-plan/SKILL.md && git commit -m "preflight gate: launcher surfaces decisions up front, writes pre-authorizations"`

---

## Self-Review

**1. Spec coverage:**
- Enriched `gated` lifecycle + `requires_decision` + `binds_meta` → T1 (schema/engine) + T2 (doc). ✅
- Per-project policy seam (`## Pre-flight gate policy`) → T3 + T4. ✅
- Planner enumeration pass → T3. ✅
- Pre-flight scan backstop + consolidated sheet + write-back + idempotency hash → T4. ✅
- Engine fail-closed enforcement (per-task `decisionGate` + load validation + answer injection) → T1. ✅
- Fork boundary (param-inject vs plan-time re-plan), no branch-skip → T3 (authoring) + T1 (single-task injection). ✅
- Scope honesty (failure-driven halts out of scope) → T4 doc note. ✅
- No gaps.

**2. Vagueness + body-bloat scan:** No "TBD"/"handle edge cases". Engine bodies left to implementer (contract only); schema fragments and the literal `gated` JSON line are seams, not bodies. Doc tasks pin literal record shapes (required-format strings, verbatim per plan rules). No dispatched task carries a full body. ✅

**3. Contract/seam consistency:** `requires_decision`, `gated.id`, `binds_meta`, `status`/`answer`/`options`/`category`/`blast_radius`, `meta.preflight.task_graph_hash`, `decisionGate(t)` — names identical across T1-T4 and match the spec. `S_MANIFEST` decision fields (T1) match the `gated` JSON line (T2). ✅

**4. Wave plan check:** All 4 tasks Wave 1, Blocks/Blocked-by all `—`. File sets disjoint (run-plan.js+test / brainstorm / plan / run-plan SKILL — four distinct paths, no overlap). `meta.scheduler="dag-parallel"` (wave 1 has 4 disjoint tasks). ✅
