# planner_maker `split` Option (Dispatch Waves) Implementation Plan

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

**Goal:** Add an opt-in `split: true` parameter to `planner_maker` that makes the final synthesized plan include a **Dispatch plan** section — tasks grouped into parallel dispatch waves computed from the dependency data the pipeline already produces — so plan output can be handed directly to `superpowers:dispatching-parallel-agents` (or `run_coding_agent` workers) without hand-deriving the waves.

**Architecture:** No new pipeline step and no new tool. The `decompose_kimi` step (Phase 2c) already emits a dependency DAG, and the `judge_final` synthesis prompt already mandates per-task `Dependencies: Blocked by/Blocks/Parallel/Complexity` lines (`planner-tools.ts:919,935`). The only missing piece is (a) threading the flag into the synthesis prompt and (b) instructing the final judge to roll the DAG up into waves with a per-task executor suggestion. The flag flows through a new optional `flags` argument on `buildParams` — NOT via the module-level `_uxOverride`-style globals, because those are reset immediately after `getActiveWorkflow()` (lines 1061–1063) and would be stale (and race-prone across awaits) by the time `buildParams` runs at line 1229.

**Tech Stack:** TypeScript ESM, Zod schemas, `defineModelTool` pattern, Jest (`node --experimental-vm-modules`; goldens via `npm run test:golden` with `UPDATE_GOLDEN=1`).

**Spec:** Self-contained. Motivating context: the 2026-08-29 tachi-agent hardening plan hand-built exactly this wave structure (Wave A = 4 parallel module tasks → Wave B = integration tasks); `split` makes planner_maker emit that shape natively.

**Execution note (why this plan has NO waves of its own):** every code task edits `src/tools/planner-tools.ts` — same file, so by this feature's own core rule ("two tasks that modify the same file never share a wave") the plan is **strictly sequential: dispatch ONE fable agent** to run Tasks 1→4 in order, or execute inline.

## Global Constraints

- **Param design rule (repo-critical):** the tool's ONE required string stays `task`; `split` MUST be `z.boolean().optional().default(false)`. Never a required enum/flag.
- **Adding a param ≠ adding a tool:** do NOT touch `src/tools/registry.ts`, `provider-catalog.ts`, or any `src/profiles/*` — those steps apply only to new tools. The golden tool-contract snapshot DOES change and must be regenerated deliberately.
- **`split` must be re-passed on every `continue` call** (the workflow/prompts are rebuilt from args each call, same as `debate`/`ux`) — the param description must say so.
- **Don't renumber or reorder existing prompt requirements** in `judge_final` — downstream consumers (blueprint skill, planner_runner parsing) key off the format; only append.
- Test commands: `npx jest test/tools/planner-split-waves.test.ts` (new file), `UPDATE_GOLDEN=1 npm run test:golden`, full `npm test`. Build: `npm run build` (tsc + profiles).
- Shell traps on this machine: `sed`→`sd`, `nc`→`nnn` — use Read/Edit tools, not stream editors.

---

## File Structure

- Modify: `src/tools/planner-tools.ts` — `WorkflowStep` type (+`StepFlags`), `judge_final` prompt, params schema, execute args + call site (line ~1229), tool description block.
- Create: `test/tools/planner-split-waves.test.ts` — style-matched to `test/tools/planner-qwen-params.test.ts`.
- Regenerate: `test/golden/__snapshots__/tool-contracts.json` (via `UPDATE_GOLDEN=1`).
- Modify (docs): `docs/TOOLS_REFERENCE.md` (planner_maker section, ~line 419), `skills/blueprint/SKILL.md`, `CHANGELOG.md`.

---

## Task 1: Thread a `flags` argument through the step pipeline

**Files:**
- Modify: `src/tools/planner-tools.ts` (WorkflowStep type ~line 460; call site ~line 1229)
- Test: covered by Task 3's test (type-level change; `npm run build` is the gate here)

**Interfaces:**
- Produces: `export interface StepFlags { split?: boolean }`; `buildParams` signature gains optional trailing `flags?: StepFlags`. Existing 12 step definitions need NO edits (they simply ignore the extra argument). Task 2 reads `flags?.split` in `judge_final`; Task 3's test passes flags explicitly.

- [ ] **Step 1: Extend the type**

In `src/tools/planner-tools.ts`, directly above the `WorkflowStep` type (~line 458), add:

```ts
/** Per-call flags threaded from planner_maker args into step prompt builders.
 *  Passed as an explicit argument (NOT a module-level override) because the
 *  _uxOverride-style globals are reset right after getActiveWorkflow() and
 *  would be stale by the time buildParams runs. */
export interface StepFlags {
  split?: boolean;
}
```

Then change the `buildParams` line inside the `WorkflowStep` type from:

```ts
  buildParams: (task: string, context: string, codeContext: string, answers: string, prior: Record<string, string>) => Record<string, any>;
```

to:

```ts
  buildParams: (task: string, context: string, codeContext: string, answers: string, prior: Record<string, string>, flags?: StepFlags) => Record<string, any>;
```

- [ ] **Step 2: Pass flags at the single call site**

At line ~1229 (`grep -n "workflowStep.buildParams" src/tools/planner-tools.ts` — there is exactly one call), change:

```ts
      params: workflowStep.buildParams(task, context, codeContext, answers, prior),
```

to:

```ts
      params: workflowStep.buildParams(task, context, codeContext, answers, prior, { split: args.split === true }),
```

(`args.split` does not exist yet — Task 2 Step 3 adds it; the build goes green at the end of Task 2. If you want a green build after every step, do Task 2 Step 3 first.)

- [ ] **Step 3: Verify types**

Run: `npm run build`
Expected after Task 2 lands: clean. (Mid-task: only the `args.split` reference may error — nothing else.)

---

## Task 2: The `split` parameter + wave-grouping synthesis prompt

**Files:**
- Modify: `src/tools/planner-tools.ts` — params schema (~line 994, after `debate`), execute args type (~line 1013), `judge_final` buildParams (~line 899) and its OUTPUT FORMAT prompt (~line 916–942), tool description block (~line 957)

**Interfaces:**
- Consumes: `StepFlags` from Task 1.
- Produces: the `split` zod param; the `Dispatch plan` output contract (section name, table columns) that Task 3's test asserts and Task 4's docs describe.

- [ ] **Step 1: Add the zod param**

In the `parameters: z.object({...})` block, immediately after the `debate` entry (~line 995), add:

```ts
    split: z.boolean().optional().default(false)
      .describe("Append a Dispatch plan: group tasks into parallel dispatch waves (from the Dependencies lines) with a suggested executor per task, ready for parallel subagents. Re-pass on every continue call, like debate/ux."),
```

- [ ] **Step 2: Extend the execute args type**

In the `execute: async (args: {...})` inline type (~line 1013, after `debate?: boolean;`), add:

```ts
    split?: boolean;
```

- [ ] **Step 3: Make `judge_final` read the flag**

Change the `judge_final` step's buildParams signature (~line 899) from:

```ts
    buildParams: (task, context, codeContext, answers, prior) => ({
```

to:

```ts
    buildParams: (task, context, codeContext, answers, prior, flags) => ({
```

- [ ] **Step 4: Append the wave section to the prompt**

Inside the `judge_final` template literal, make two insertions. First, append a 9th requirement after requirement 8 (line ~935, the "Preserve exact task IDs" line):

```ts
${flags?.split ? "9. Tasks that modify the SAME file must NEVER share a dispatch wave — push the later one to a following wave even when dependencies would allow overlap." : ""}
```

Second, insert the section spec right BEFORE the `**QUALITY ASSESSMENT (at the end):**` line (~line 937):

```ts
${flags?.split ? `
**DISPATCH PLAN (required section, after all tasks, before quality assessment):**
Roll the Dependencies lines up into parallel dispatch waves:
- Wave 1 = every task with no "Blocked by" entry (or only blockers outside this plan) AND "Parallel: yes".
- Wave N = tasks whose blockers ALL appear in earlier waves.
- A "Parallel: no" task occupies its wave alone.
- Tasks sharing a wave MUST touch disjoint files (requirement 9).

Output exactly this shape:

### Dispatch plan
| Wave | Task IDs | Executor per task | Files touched |
|---|---|---|---|
| 1 | T1, T3 | T1: subagent · T3: subagent | T1: src/a.ts · T3: src/b.ts |
| 2 | T2 | T2: coordinator | T2: src/a.ts, src/b.ts |

Executor rule: "subagent" for self-contained code tasks a fresh agent can do from the task text alone; "coordinator" for tasks needing cross-task context (final integration, docs sweeps, merge/verify steps).
End with one line: "Max parallelism: N agents in wave W."` : ""}
```

- [ ] **Step 5: Document in the tool description**

In the `description` template (~line 957, the COORDINATOR PATTERN block), append one line to the existing example section:

```
Optional: split: true → final plan ends with a Dispatch plan (parallel waves + executor per task) for handing tasks to parallel subagents.
```

- [ ] **Step 6: Build**

Run: `npm run build`
Expected: clean, including the Task 1 call site (all `args.split` / `flags` references now resolve).

---

## Task 3: Contract tests + golden snapshot

**Files:**
- Create: `test/tools/planner-split-waves.test.ts`
- Regenerate: `test/golden/__snapshots__/tool-contracts.json`

**Interfaces:**
- Consumes: `ALL_WORKFLOW_STEPS` (already exported for exactly this test pattern), `geminiAnalyzeTextTool` from `src/tools/gemini-tools.ts` (export confirmed at line 436), `withParamAliases` from `src/utils/param-aliases.js`.

- [ ] **Step 1: Write the failing test**

```ts
/**
 * planner_maker split:true → judge_final emits the Dispatch-plan wave section.
 *
 * The flag travels via buildParams' trailing StepFlags argument (NOT the
 * _uxOverride-style globals — those are reset before buildParams runs). This
 * test pins: (a) flag on → wave section present, (b) flag off/absent → absent,
 * (c) emitted params still satisfy the REAL registered gemini_analyze_text
 * schema (withParamAliases, as production applies it in server-main.ts).
 */
import { ALL_WORKFLOW_STEPS } from "../../src/tools/planner-tools.js";
import { geminiAnalyzeTextTool } from "../../src/tools/gemini-tools.js";
import { withParamAliases } from "../../src/utils/param-aliases.js";

const registeredSchema = withParamAliases(geminiAnalyzeTextTool).parameters;
const judgeFinal = ALL_WORKFLOW_STEPS.find((s: any) => s.id === "judge_final");

describe("planner_maker split flag → judge_final dispatch waves", () => {
  it("judge_final exists and targets gemini_analyze_text", () => {
    expect(judgeFinal).toBeDefined();
    expect(judgeFinal!.tool).toBe("gemini_analyze_text");
  });

  it("split:true injects the Dispatch plan section and the same-file wave rule", () => {
    const params = judgeFinal!.buildParams("add rate limiting", "", "", "", {}, { split: true });
    expect(params.text).toContain("### Dispatch plan");
    expect(params.text).toContain("DISPATCH PLAN (required section");
    expect(params.text).toMatch(/SAME file must NEVER share a dispatch wave/);
    expect(params.text).toContain("Max parallelism:");
  });

  it("split absent (legacy 5-arg call) omits the section entirely", () => {
    const params = judgeFinal!.buildParams("add rate limiting", "", "", "", {});
    expect(params.text).not.toContain("Dispatch plan");
    expect(params.text).not.toContain("DISPATCH PLAN");
  });

  it("split:false omits the section", () => {
    const params = judgeFinal!.buildParams("add rate limiting", "", "", "", {}, { split: false });
    expect(params.text).not.toContain("Dispatch plan");
  });

  it("split:true params still parse against the registered gemini_analyze_text schema", () => {
    const params = judgeFinal!.buildParams("add rate limiting", "", "", "", {}, { split: true });
    const parsed = registeredSchema.safeParse(params);
    if (!parsed.success) {
      throw new Error(`judge_final params failed the real schema: ${JSON.stringify(parsed.error.issues)}`);
    }
  });
});
```

- [ ] **Step 2: Run it — red first if Task 2 is not yet applied, green after**

Run: `npx jest test/tools/planner-split-waves.test.ts`
Expected: PASS (5/5) once Tasks 1–2 are in. (If executing this plan strictly TDD, write this file before Task 2 Step 4 and watch the two split:true assertions fail first.)

- [ ] **Step 3: Regenerate the golden tool contract**

Run: `UPDATE_GOLDEN=1 npm run test:golden`
Then: `git diff test/golden/__snapshots__/tool-contracts.json`
Expected: the diff touches ONLY `planner_maker` — one added `split` property (boolean, optional, default false). Any other tool changing = something else drifted; stop and investigate before committing.

- [ ] **Step 4: Full suite**

Run: `npm test`
Expected: exit 0. Known trap: jest scans `.worktrees/` — a sibling-branch failure there is NOT yours (see workspace memory `tachibot-npm-test-phantom-failures`); verify any failure's path before reacting.

- [ ] **Step 5: Commit**

```bash
git add src/tools/planner-tools.ts test/tools/planner-split-waves.test.ts test/golden/__snapshots__/tool-contracts.json
git commit -m "feat(planner): split option — dispatch-wave grouping in final plan output"
```

---

## Task 4: Docs sweep

**Files:**
- Modify: `docs/TOOLS_REFERENCE.md` (planner_maker section, ~line 419)
- Modify: `skills/blueprint/SKILL.md`
- Modify: `CHANGELOG.md`

- [ ] **Step 1: TOOLS_REFERENCE** — in the planner_maker section, add to the parameter list:

```md
- `split` (boolean, default false) — append a **Dispatch plan**: tasks grouped into parallel dispatch waves (derived from each task's Dependencies line) with a suggested executor per task and a max-parallelism figure. Feed the waves straight to parallel subagents. Re-pass on every `continue` call.
```

- [ ] **Step 2: blueprint skill** — in `skills/blueprint/SKILL.md`, where the planner_maker invocation is described, add one usage note:

```md
For a plan you intend to execute with parallel subagents (superpowers:dispatching-parallel-agents), pass `split: true` — the final plan then ends with a Dispatch plan table (waves → task IDs → executor → files) instead of you deriving the waves by hand.
```

- [ ] **Step 3: CHANGELOG** — add under the next unreleased version heading:

```md
- `planner_maker` gains `split: true`: the final plan appends a Dispatch plan — dependency-derived parallel waves with per-task executor suggestions and a same-file-never-shares-a-wave rule — ready for parallel subagent dispatch.
```

- [ ] **Step 4: Commit**

```bash
git add docs/TOOLS_REFERENCE.md skills/blueprint/SKILL.md CHANGELOG.md
git commit -m "docs(planner): document the split dispatch-waves option"
```

---

## Deferred (explicitly out of scope — do not expand this plan)

- **`planner_runner` `mode: "wave"`** — returning all currently-unblocked steps as a set and re-checkpointing per wave. Touches the runner state machine + checkpoint cadence; only worth it once split-plans are routinely executed by parallel agents.
- **Executor vocabulary beyond subagent/coordinator** (e.g. suggesting `run_coding_agent` codex/grok workers per task) — needs cost/capability routing, own spec.
- **Machine-readable wave output** (JSON block alongside the table) for programmatic dispatch — add when a consumer actually parses it.

---

## Self-Review

**Coverage:** flag plumbing (T1) ✓; schema + prompt + description (T2) ✓; behavioral + schema-contract tests and golden regeneration with exact-diff check (T3) ✓; all three doc surfaces + changelog (T4) ✓; the runner wave-mode consciously deferred, not dropped ✓.

**Placeholder scan:** all code steps carry real code; the only "verify" steps are command-with-expected-output gates.

**Type consistency:** `StepFlags` (T1) is the type read as `flags?.split` in T2 and passed as `{ split: true }` in T3's test; `buildParams(task, context, codeContext, answers, prior, flags?)` arity matches across T1's type, T1's call site, T2's judge_final, and T3's 5-arg legacy-call test. `geminiAnalyzeTextTool` import verified against the real export (`gemini-tools.ts:436`). Section name `### Dispatch plan` is identical in the prompt spec (T2), the test assertions (T3), and the docs (T4).

**Race-safety note carried into code comments:** the module-global override pattern was rejected for this flag on purpose — the T1 comment records why, so a future refactor doesn't "simplify" it back into a bug.
