# Competitive Gap #3: Token-budgeted Goal loop primitive with independent verifier

> **Verdict: ADAPT A NEW PRIMITIVE, DO NOT VENDOR.** We have no open-ended goal loop with an
> independent completion-verifier. We already have every supporting primitive — `agent()`,
> `parallel()`, `retry()`, `gate()`, `checkpoint()`, `budget`, the disk journal, and the
> `TOKEN_BUDGET_EXHAUSTED` fail-closed path. The gap is a thin orchestration primitive,
> `goal()`, that composes them. Per [#137](../workflow-engine-review.md): read pi-maestro-flow
> as a *concept source*, adapt through a bounded issue, never merge wholesale.

---

## Problem (what we lack, with evidence from the competitive analysis)

pi-maestro-flow ships a `goal` tool (README §"Goal", `docs/USAGE.md` §2.3) that does three
things we cannot express today:

1. **Set a high-level objective and let the loop work toward it autonomously** — the worker
   agent is free to choose its own tool calls each round; it is not handed a fixed subtask.
2. **An independent verifier judges completion each round**, not the worker: on `pass` the
   loop completes; on `fail` the worker gets another round carrying the unmet requirements;
   on `inconclusive` the loop pauses for a human `/goal resume`. The worker never self-certifies.
3. **A token budget makes it "set it and forget it"** — the loop stops cleanly when the
   budget is exhausted, surfacing partial work rather than silently shipping.

What we have today (`src/workflow.ts`):

- `loopUntilDry` (lines 2476–2512): runs `round(i)` until rounds stop yielding *new items*.
  It is a **convergence/dedup** loop for discovery tasks ("keep finding until the well is
  dry"), **not** a goal-completion loop. It has no objective, no verifier, and no notion of
  "done" beyond an empty round.
- `retry` (2530–2541) / `gate` (2542–2557): bounded retry with an `until()`/`validator()`.
  These **are** verifier-gated loops, but the validator is a *predicate the caller writes*,
  not an independent agent judging a free-text objective, and the thunk is a single fixed
  function — not "let the worker choose what to do this round given last round's verdict".
- `verify` (2414–2436): adversarial post-hoc fact-checking of a single claim ("is this
  REAL/correct?"). It is **not** a completion judge ("did the worker satisfy the objective?").
- `completenessCheck` (2519–2523): a one-shot critic that lists what's *missing*. Close to a
  verifier, but it returns a missing-list, not a pass/fail/inconclusive verdict, and it is not
  wired into a loop.
- `checkpoint` (2564–2599): deterministic human gate. Good for a manual `/goal resume`, but
  there is no primitive that *drives* the loop between checkpoints.

**The concrete gap:** there is no single primitive where you write
`await goal('Implement JWT auth module', { verifier: 'adversarial-evidence-reviewer', tokenBudget: 100_000 })`
and the engine runs worker→verifier rounds until pass or budget exhaustion, persisting each
round to the disk journal so a crashed/resumed run replays from the last verdict.

Evidence: `src/workflow.ts` `runWorkflow`'s injected globals (lines 2601–2641) include
`agent, parallel, pipeline, dag, workflow, verify, judgePanel, loopUntilDry,
completenessCheck, retry, gate, checkpoint, log, phase, budget` — and nothing resembling a
goal/verifier loop. The peer's distinct mechanism is the **independent completion-verifier
driving the loop**, which none of our primitives compose today.

---

## Peer reference (pi-maestro-flow approach, with source URL)

- **Source (README):** <https://github.com/catlog22/pi-maestro-flow/blob/master/packages/pi-maestro-flow/README.md>
  (§"Goal", `goal({ action: "create", objective, tokenBudget? })`; lifecycle `/goal stop|resume|clear`).
- **Source (USAGE):** <https://github.com/catlog22/pi-maestro-flow/blob/master/docs/USAGE.md>
  (§2.3 `goal` — 长时目标生命周期; the verification mechanism; the Goal panel states).
- **npm:** <https://www.npmjs.com/package/pi-maestro-flow> (v0.20.0, MIT).

### Their mechanism (concept only — we read it, we do not take their code)

- A `goal` LLM tool with a deliberately small surface: `create`/`get`/`update`/`complete`,
  plus user-facing `/goal stop|resume|clear`. `tokenBudget` is **absent by default** and only
  applies when supplied explicitly — "set it and forget it" is opt-in, not the baseline.
- After the worker's normal `agent_end`, an **independent verifier** runs automatically.
  Verdict mapping: `pass` → complete + clear the Goal; `fail` → start another loop carrying
  the unmet requirements forward; `inconclusive` → hold the Goal active, wait for human
  `/goal resume`. Crucially, `turn_end` does **not** verify, and `session_shutdown` only
  persists state — verification is a deliberate, bounded event, not ambient.
- A persistent Goal panel renders states: ACTIVE / WAITING / VERIFYING / VERIFIED /
  STOPPED / BUDGET / BLOCKED / ERROR. Persistence is scoped to `sessionManager.getSessionId()`;
  new/forked sessions start without a Goal.
- They prefer acceptance *commands* declared by the objective; only when no commands are
  declared do they fall back to a built-in `verifier` role (strict read-only, structured
  fail-closed verdict).

### What we borrow conceptually

1. **The tri-state verdict** (`pass` / `fail` / `inconclusive`), with `inconclusive` pausing
   for a human rather than looping forever. This is a real improvement over a boolean
   `until()` predicate — it gives the loop a safe "I can't decide, ask a human" exit.
2. **Budget opt-in by default.** No budget = run until pass/maxRounds; explicit `tokenBudget`
   = bounded. This matches our existing `WorkflowRunOptions.tokenBudget` semantics exactly.
3. **The verifier is a separate agent call from the worker**, so the worker never
   self-certifies completion. This is the core anti-self-deception property and it composes
   cleanly with our existing read-only `agentType` roster (`adversarial-evidence-reviewer`).
4. **Carry-forward of the unmet-requirements feedback** into the next round — identical in
   spirit to our existing `gate()` feedback loop (line 2542), just with an independent judge.
5. **Bounded verification event, not ambient.** Verify on the round boundary, not every turn.
   Their "turn_end does not verify" discipline maps to ours: `goal()` verifies once per round,
   after the worker agent returns.

### What we reject (per #137 adapt-don't-merge)

- **Their session-scoped Goal persistence + Goal panel (UI).** That is a TUI/extension
  concern (the `task-panel`/`workflow-monitor-pane` layer), not a workflow-engine primitive.
  We persist to our **disk journal** (`JournalEntry`, lines 110–133) like every other
  `agent()` call — resume replays from the journaled verdict. No new persistence layer.
- **Their `action: "create|get|update|complete"` LLM-tool surface.** Our workflows are
  authored JS scripts; `goal()` is a function in the script, not an LLM tool the model
  invokes mid-turn. We do not need `get`/`update`/`complete` — the script author already has
  the goal string in scope.
- **Their `sessionManager.getSessionId()` ownership model.** We scope persistence to the
  run via `runId` + `callSeq` (the existing resume-key contract), not Pi session IDs.
- **Their acceptance-commands fallback.** We are a workflow engine, not an autonomous
  coding surface; the verifier is always an explicit `agentType`/`model` the script names.
- **Vendoring any of their source.** Per #137: read-only idea source, adapted through a
  bounded issue against our architecture. Zero imports, zero copy-paste.

---

## Our adaptation (how it fits OUR deterministic VM / disk journal / checkpoint() / herdr / role-based routing / Foundation fail-closed; concrete API/signature sketches)

### Determinism constraints we must honor

- **No `Date.now` / `Math.random` / `setTimeout` inside the loop body** — the determinism
  prelude (line 504, `DETERMINISM_PRELUDE`) already neuters these; `goal()` inherits that.
- **Every agent() and the verifier agent() run under the monotonic `callSeq`** (line 492),
  so each round's worker + verifier pair journals at stable indices and replays on resume
  via `firstMiss` longest-unchanged-prefix (line 498). `goal()` does not invent a new
  identity key — it is sugar over `agent()` + `gate()`, so resume keeps working for free.
- **Budget accrues through `shared.spent`** (line 143) the same way every agent does. The
  verifier call counts against the budget too — there is no free verification. Budget
  exhaustion throws `TOKEN_BUDGET_EXHAUSTED` (line 1115) inside `agent()`, and `goal()`
  catches it to return partial work, mirroring `loopUntilDry`'s catch at line 2497.

### Role-based routing

- The **worker** defaults to `tier: 'medium'` (conductor-class implementation) but the
  script can override with `opts.workerTier` / `opts.workerModel`.
- The **verifier** defaults to `agentType: 'adversarial-evidence-reviewer'` (the existing
  read-only adversarial reviewer) with `tier: 'big'` (advisor-class independent judgment) —
  this is exactly the role the routing policy reserves the big tier for ("independent
  Anthropic-family judgment, and any verdict that requires exploring to reach it"). The
  script can override `opts.verifierAgentType` / `opts.verifierModel`.
- Per the operator routing policy, retries **do not escalate tiers automatically**; the
  script encodes escalation explicitly if it wants it (e.g. `workerTier: 'small'` for
  early rounds, bump on repeated `fail`). The default is one tier per role for the whole
  loop — simplest correct behavior.

### Fail-closed (the most important property)

`goal()` **never silently ships on budget exhaustion or on `maxRounds` reached without a
pass.** Its return value carries an explicit `status`:

```ts
type GoalStatus = "passed" | "budget_exhausted" | "max_rounds" | "inconclusive" | "aborted";
interface GoalResult<T = unknown> {
  status: GoalStatus;
  rounds: number;
  finalArtifact: T | null;     // the worker's last output (may be partial)
  verdict: { pass: boolean; inconclusive: boolean; reason: string } | null; // last verifier verdict
  tokensUsed: number;
  history: Array<{ round: number; verdict: GoalVerdict; feedback?: string }>;
}
```

A script that calls `goal()` and then proceeds to ship **must check `result.status === 'passed'`**
first. Anything else is a fail-closed stop. This is the same posture `checkpoint()` takes
with `default: false` (test at `tests/checkpoint.test.ts` line ~"explicit conservative
default declines headlessly") — the safe default is "do not proceed", not "proceed".

### Composes with `dag` / `pipeline` / `workflow()`

`goal()` is just an async function in the script, so it drops into any composition:

```js
// A pipeline stage that runs a goal loop per item
await pipeline(items, async (item) => {
  return await goal(`Deliver: ${item}`, { verifier: 'adversarial-evidence-reviewer', tokenBudget: 20_000 });
});

// A DAG node whose dependents only run once the goal passes
await dag([
  { id: 'build',  run: async () => goal('Implement feature X', { /*...*/ }) },
  { id: 'verify', dependsOn: ['build'], run: async (r) => { /* r is the GoalResult */ } },
]);
```

Because each `goal()` round is real `agent()` calls, `dag`'s wave scheduler and `pipeline`'s
stage sequencing see them as ordinary async work — no special-case integration.

### Persistence / resume

Each round = 1 worker `agent()` + 1 verifier `agent()`, both journaled at stable `callSeq`
indices. On resume (`resumeJournal`), the journal replays cached worker+verifier pairs up to
`firstMiss`; from `firstMiss` onward the loop re-runs live. The `goal()` wrapper itself stores
**no** state across rounds beyond what the agent calls already journal — the round counter is
reconstructed by re-running the script (deterministic: same goal + same journal prefix →
same round count). This is identical to how `retry`/`gate` already work (comment at line
2528: "attempt N+1's call hash depends on N's live result, so a retry/gate chain
cache-miss-cascades on resume (correct)").

### Sketch — the implementation (≤ ~60 lines in `src/workflow.ts`)

Lives next to `loopUntilDry` (line 2476) / `completenessCheck` (line 2519). Injected into the
vm context at line 2601 alongside the other quality helpers.

```ts
const GOAL_VERDICT_SCHEMA = {
  type: "object",
  properties: {
    pass: { type: "boolean" },
    inconclusive: { type: "boolean" },
    reason: { type: "string" },
    unmet: { type: "array", items: { type: "string" } }, // carried into next round
  },
  required: ["pass", "inconclusive"],
};

const goal = async <T = unknown>(
  objective: string,
  opts: {
    workerTier?: "small" | "medium" | "big";
    workerModel?: string;
    workerAgentType?: string;
    verifierAgentType?: string;   // default 'adversarial-evidence-reviewer'
    verifierModel?: string;
    verifierTier?: "small" | "medium" | "big"; // default 'big'
    tokenBudget?: number;          // throws GoalBudgetExhausted; opts IN (not run-wide)
    maxRounds?: number;            // default 10
    roundBudget?: number;          // optional per-round ceiling (uses phase budget)
    schema?: unknown;              // shape of the worker artifact (structured output)
  } = {},
) => {
  if (typeof objective !== "string" || !objective.trim())
    throw new TypeError("goal() needs a non-empty objective string");
  const maxRounds = Math.max(1, opts.maxRounds ?? 10);
  const verifierType = opts.verifierAgentType ?? "adversarial-evidence-reviewer";
  let history: Array<{ round: number; verdict: any; feedback?: string }> = [];
  let finalArtifact: T | null = null;
  let lastVerdict: any = null;
  let rounds = 0;
  for (let r = 0; r < maxRounds; r++) {
    rounds = r + 1;
    const feedback = lastVerdict && !lastVerdict.pass
      ? `\n\nPrevious round did not pass. Verifier reason: ${lastVerdict.reason}\nUnmet requirements:\n${(lastVerdict.unmet || []).map((u: string) => `- ${u}`).join("\n")}`
      : "";
    try {
      // Worker — free to choose its own tools each round, carrying the feedback.
      finalArtifact = (await agent(
        `OBJECTIVE:\n${objective}${feedback}\n\nWork toward this objective. Return the artifact you produced.`,
        { label: `goal worker r${r + 1}`, tier: opts.workerTier, model: opts.workerModel,
          agentType: opts.workerAgentType, schema: opts.schema },
      )) as T;
    } catch (error) {
      const code = (error as any)?.code;
      if (code === WorkflowErrorCode.TOKEN_BUDGET_EXHAUSTED || code === WorkflowErrorCode.AGENT_LIMIT_EXCEEDED)
        return { status: "budget_exhausted", rounds, finalArtifact, verdict: lastVerdict,
                 tokensUsed: shared.spent, history };
      throw error;
    }
    // Independent verifier — separate agent(), read-only agentType, structured verdict.
    lastVerdict = await agent(
      `You are an INDEPENDENT verifier. Does the artifact satisfy the OBJECTIVE? Do not trust the worker; verify against the objective and any evidence in the artifact.\n\nOBJECTIVE:\n${objective}\n\nArtifact:\n${JSON.stringify(finalArtifact).slice(0, 8000)}\n\nVerdict: pass=true only if fully satisfied; inconclusive=true if you cannot decide (e.g. needs human input or more info); otherwise pass=false with the unmet requirements listed.`,
      { label: `goal verifier r${r + 1}`, agentType: verifierType,
        model: opts.verifierModel, tier: opts.verifierTier ?? "big",
        schema: GOAL_VERDICT_SCHEMA, tools: [], disallowedTools: ["edit","write","bash"] },
    );
    history.push({ round: r + 1, verdict: lastVerdict, feedback: lastVerdict.unmet?.join("; ") });
    if (lastVerdict.pass) return { status: "passed", rounds, finalArtifact, verdict: lastVerdict,
                                   tokensUsed: shared.spent, history };
    if (lastVerdict.inconclusive) {
      // Pause for a human — do NOT loop. In a headless/background run this fails closed.
      const resume = await checkpoint(
        `Goal "${objective}" inconclusive after ${r + 1} round(s): ${lastVerdict.reason}. Continue another round?`,
        { default: false, kind: "confirm" },
      );
      if (!resume) return { status: "inconclusive", rounds, finalArtifact, verdict: lastVerdict,
                            tokensUsed: shared.spent, history };
    }
    // pass=false → another round, feedback carried forward.
  }
  return { status: "max_rounds", rounds, finalArtifact, verdict: lastVerdict,
           tokensUsed: shared.spent, history };
};
```

Key adaptation points vs the peer:

- **No session-scoped Goal entity.** The objective is a script-local string; persistence is
  the journal. This is the #137-clean version of their `sessionManager.getSessionId()` model.
- **`inconclusive` routes to `checkpoint()`**, our deterministic human gate, with
  `default: false` so a headless/background run fails closed instead of looping forever.
  This is *our* mechanism for their `/goal resume` — and it already journals + replays.
- **The verifier is read-only by tool policy** (`tools: []`, `disallowedTools`, and the
  `adversarial-evidence-reviewer` agentType which is read-only by definition), not by prompt
  text alone — per the repo invariant "reviewers must not receive edit/write/bash mutation
  ability unless repair is explicit." `goal()` is a review-style loop, not a repair loop.
- **`tokenBudget` is opt-in and scoped to the goal** (charged through `shared.spent` like all
  agent work), matching the peer's "absent by default" design and our `budget` object.

### Optional: a `goalBudget` sub-ceiling

If a script wants the goal's budget walled off from the run total, `goal()` can carve a
per-round phase budget via the existing `phase()` sub-budget mechanism (line 965). This is
**optional** and out of scope for the first PR; the run-wide `tokenBudget` already bounds it.

---

## Scope (in-scope for the first PR)

1. Add `goal(objective, opts)` to the quality-stdlib in `src/workflow.ts`, injected into the
   vm context at the globals block (line 2601). Pure sugar over `agent()` + `checkpoint()` —
   no new runtime state, no new journal shape.
2. `GoalResult` / `GoalStatus` / `GoalVerdict` exported types (TypeBox or TS interface) in
   `src/workflow.ts`, mirrored in `src/conductor-types.ts` if the conductor needs to surface
   `status` in the task panel.
3. Tests in `tests/goal-loop.test.ts` (see Test plan).
4. One paragraph in `docs/workflow-engine-review.md` peer-watchlist crediting pi-maestro-flow
   for the concept (adapted, not vendored) — the #137 attribution requirement.
5. CHANGELOG entry: "Added `goal()` token-budgeted goal loop with independent verifier
   (concept adapted from pi-maestro-flow, per #137)."

## Non-goals (what NOT to build; per #137 adapt-don't-merge — do not vendor their implementation)

- **No Goal panel / TUI / statusline.** That is `task-panel.ts`/`workflow-monitor-pane.ts`
  territory and out of scope; the first PR surfaces `GoalResult.status` to the existing
  task-panel result delivery only.
- **No `goal` LLM tool / `/goal` slash command / `action: create|get|update|complete`
  surface.** Our workflows are authored JS; `goal()` is a script function.
- **No session-scoped Goal entity or `sessionManager.getSessionId()` ownership.** Persistence
  is the disk journal; ownership is `runId` + `callSeq`.
- **No acceptance-commands fallback / built-in verifier role substitution.** The verifier is
  always an explicit `agentType`/`model` named by the script.
- **No ambient verification on `turn_end`/`agent_end`.** `goal()` verifies on the round
  boundary it controls, not on host lifecycle events.
- **No new persistence layer / new file format.** Reuse `JournalEntry` verbatim.
- **No imports from `pi-maestro-flow`, no copy-paste of their source.** Concept only.
- **No auto-escalation of worker tier on `fail`.** Escalation is explicit in the script.

## File-touch list (exact files/symbols that change, with the role of each)

| File | Symbol | Role of change |
|------|--------|----------------|
| `src/workflow.ts` | `goal` (new, ~lines 2524–) | New quality-stdlib primitive: the verifier-gated goal loop. Pure sugar over `agent()` + `checkpoint()`, journaled via `callSeq`. |
| `src/workflow.ts` | `GoalResult` / `GoalStatus` / `GoalVerdict` (new interfaces, near `JournalEntry` block ~line 133) | Exported return-shape types so the conductor/task-panel can consume `status`. |
| `src/workflow.ts` | vm context object (line 2601) | Add `goal` to the injected globals alongside `loopUntilDry`/`completenessCheck`. |
| `src/conductor-types.ts` | (optional) `ConductorRunStatus` | If the task panel should show `goal_passed`/`goal_budget_exhausted`; otherwise the existing `status` string carries it. |
| `tests/goal-loop.test.ts` | (new file) | Tautology-free tests per the Test plan. |
| `docs/workflow-engine-review.md` | peer-watchlist + attribution | Crediting pi-maestro-flow for the *concept* (adapted, not vendored) per #137. |
| `CHANGELOG.md` | entry | Attribution + feature note per #137 ("record it in the CHANGELOG / release notes"). |

No changes to: `run-persistence.ts` (journal shape unchanged), `agent-registry.ts`,
`tool-requirements.ts`, `saved-commands.ts`, `workflow-lock.json` (no new saved workflow),
`package.json` (no new deps).

## Test plan (tautological-test warning: expected values from an independent oracle, not recomputed the same way)

Per the repo invariant: expected values must come from an independent source of truth, never
recomputed the same way as the code under test. The "oracle" here is the **scripted fake
agent's deterministic behavior** — the fake returns a fixed verdict the test asserts against,
so the test is not re-running the verifier logic.

1. **pass on round 1** — fake worker returns `{feature:"jwt"}`; fake verifier returns
   `{pass:true, inconclusive:false, reason:"all claims verified"}`. Assert `result.status ===
   "passed"`, `result.rounds === 1`, `result.finalArtifact.feature === "jwt"`. (Oracle: the
   hardcoded fake verdict, independent of `goal()`'s loop logic.)
2. **fail then pass** — verifier returns `pass:false` with `unmet:["refresh token"]` on round
   0, `pass:true` on round 1. Assert `rounds === 2`, `history[0].verdict.pass === false`,
   `history[0].feedback` contains "refresh token", `status === "passed"`. (Oracle: the
   per-round fake verdict array — the test does not recompute pass/fail.)
3. **budget_exhausted** — inject a fake agent that throws
   `{ code: 'TOKEN_BUDGET_EXHAUSTED' }` on round 2's worker. Assert `status ===
   "budget_exhausted"`, `rounds === 2`, `finalArtifact` is the round-1 artifact. **Fail-closed
   guard:** assert the result is *not* `passed` and `finalArtifact` is carried but never
   certified (`verdict` is the round-1 verdict, `pass:false`). (Oracle: the thrown error code,
   independent of `goal()`'s catch.)
4. **max_rounds without pass** — verifier always returns `pass:false`. Assert `status ===
   "max_rounds"`, `rounds === maxRounds`. **Fail-closed guard:** assert `status !==
   "passed"`.
5. **inconclusive → headless fails closed** — verifier returns
   `{pass:false, inconclusive:true}`; no `confirm` threaded. Assert `checkpoint` takes
   `default:false` → `status === "inconclusive"`, `rounds === 1`. (Oracle: the fake verdict +
   the known headless `default:false` behavior from `checkpoint.test.ts`.)
6. **inconclusive → human resumes** — thread `confirm: async () => "yes"`; verifier returns
   inconclusive round 0 then pass round 1. Assert `status === "passed"`, `rounds === 2`,
   and that `confirm` was called once (the checkpoint journaled and replayed, not re-asked).
7. **verifier is read-only** — assert the verifier `agent()` call receives `tools: []` and
   `disallowedTools` includes edit/write/bash (or that the `adversarial-evidence-reviewer`
   agentType enforces read-only). This is a static/capability assertion, not a recomputed
   verdict.
8. **resume replays the journaled verdict** — run to round 2, capture the journal via
   `onAgentJournal`, then re-run with `resumeJournal`. Assert the worker/verifier are *not*
   called again (call counters stay flat) and `result.status` is identical. (Oracle: the
   captured journal — resume correctness is asserted against the first run's result, not
   recomputed.)
9. **determinism** — run the same script twice with the same fake agent; assert identical
   `history` and `status`. (Oracle: identity across runs.)

Anti-tautology check: in every test, the expected `status`/`rounds`/`verdict.pass` come from
the *scripted fake's fixed return sequence*, which is authored independently of `goal()`'s
loop branching. The test never calls `goal()`'s own verdict-computation to produce the
expected value.

## Risks + guardrails (fail-closed, budget bounds, determinism, no silent shipping)

- **Silent shipping (P0).** `goal()` returns a typed `status`; anything other than `"passed"`
  must be treated as a stop by the calling script. Guardrail: the `GoalResult.status` union
  is exhaustive; tests #3/#4/#5 assert non-`passed` on exhaustion/max-rounds/inconclusive.
  Documentation must show the `if (result.status !== 'passed') return;` idiom.
- **Self-certification (P1).** The worker must not judge its own completion. Guardrail: the
  verifier is a **separate `agent()` call** with a read-only `agentType` and empty tool list,
  enforced by tool policy (not prompt text). Test #7 asserts this statically.
- **Budget bound (P1).** `tokenBudget` is opt-in; when set, `goal()` catches
  `TOKEN_BUDGET_EXHAUSTED` from `agent()` and returns `status: "budget_exhausted"` with the
  partial artifact — it does **not** swallow the error and report `passed`. The verifier
  call itself counts against `shared.spent`, so verification is never "free".
- **Determinism / resume (P1).** Every worker+verifier pair is a real `agent()` call under
  `callSeq`, so resume replays via `firstMiss` longest-unchanged-prefix exactly like
  `retry`/`gate`. No `Date.now`/`Math.random`/`setTimeout` in the loop body (prelude-enforced).
  Round count is reconstructed by re-running the script, not stored — a changed
  `objective`/`opts` invalidates the suffix hash, which is the correct behavior.
- **Infinite loop on `inconclusive` (P2).** Guardrail: `inconclusive` routes to
  `checkpoint({ default: false })`; a headless/background run declines and stops
  (`status: "inconclusive"`), it does not spin. This is the same AFK-safe posture the
  guidelines require for `checkpoint()`.
- **Verifier cost ballooning (P2).** The verifier defaults to `tier: 'big'` (Opus, 1M ctx),
  which under subscription is a latency/rate-limit cost, not a dollar cost. Guardrail: the
  script can set `verifierTier: 'medium'`; `maxRounds` (default 10) caps total verifier
  calls. Document that `big`-tier verification is for consequential goals, not bulk fan-out.
- **Scope creep into TUI (P3).** Non-goal: no Goal panel in the first PR. If the task panel
  needs a status string, it reads `GoalResult.status` — no new UI surface is built.
- **Cross-repo discipline.** This is a single-repo change to `pi-dynamic-workflows`. If the
  verifier `agentType`'s read-only enforcement needs strengthening, that is an
  `agent-registry.ts`/`tool-requirements.ts` change in *this* repo, not a Pi SDK change. If the
  SDK's tool-policy surface is insufficient, file a separate issue against the SDK per the
  cross-repo handoff rule in `AGENTS.md`.
- **#137 attribution.** The CHANGELOG and `docs/workflow-engine-review.md` must credit
  pi-maestro-flow as the concept source, stating the concept was *adapted*, not vendored.
  No adopted idea may be credited only in an ephemeral issue comment.
