Skip to content

feat(proof): first-class bounded convergence loops in DAG schema#184

Draft
tonyketcham wants to merge 2 commits intotoeknee/flatbread-task-dag-instances-18a9from
toeknee/proof-bounded-loop-cde0
Draft

feat(proof): first-class bounded convergence loops in DAG schema#184
tonyketcham wants to merge 2 commits intotoeknee/flatbread-task-dag-instances-18a9from
toeknee/proof-bounded-loop-cde0

Conversation

@tonyketcham
Copy link
Copy Markdown
Collaborator

Summary of changes

Generalizes the --converge-on/--max-iterations CLI singleton in @flatbread/proof into a DAG-native loops array, so a single run can stack multiple convergence tasks (e.g. one for the implementation reviewer, one for the docs reviewer) and so DAG-emitting tooling can declare loop intent reproducibly.

Resumes from cursor agent bc-0ff9d782-…, which discussed cyclic vs acyclic task graphs in proof and concluded:

  • The dependency graph should stay acyclic — depends_on is causality + parallelism, not iteration.
  • Bounded refinement (research → critique → refine, fix → test → fix-until-oracle) is real and useful, but it belongs in an explicit loop primitive, not a back-edge in the DAG.

This PR lifts that loop primitive into the DAG JSON.

What's new

{
  "title": "implementation + adversarial review",
  "loops": [
    {
      "id": "review-loop",
      "convergeOn": "review",
      "maxIterations": 3,
      "reexecute": { "kind": "ancestors" },
      "stopWhen": "no-blockers"
    }
  ],
  "tasks": [/**/]
}
  • convergeOn and maxIterations are required.
  • reexecute is optional. { kind: 'ancestors' } (default) mirrors the legacy CLI behavior. { kind: 'tasks'; tasks: [...] } is an explicit allow-list, validated to lie inside the convergence ancestor cone (off-cone tasks would break filtered topological ordering).
  • stopWhen is optional. Today the only value is 'no-blockers'; the schema is open for future predicates without further churn.
  • id is optional and defaults to loop-${convergeOn}.
  • DAG.budget.maxIterations keeps applying per-loop and the existing BUDGET-EXCEEDED terminal status carries through unchanged.
  • The CLI flag and DAG.loops are mutually exclusive — supplying both fails fast at startup rather than picking a silent precedence rule.

Backward compatibility

  • DAG JSON without loops parses unchanged.
  • --converge-on <id> --max-iterations <N> keeps working end-to-end. Internally it synthesizes a single-element loops array so the runner has one canonical code path.
  • extractConvergenceFindings, the findings-dir sidecar contract, the extraContext stitching format, and transitiveAncestors are all unchanged. Existing reviewer prompts keep working.

Out of scope (this PR)

  • New stopWhen predicates beyond 'no-blockers' (e.g. 'oracle-pass', score thresholds).
  • Nested loops.
  • Cross-loop coordination.

The full design rationale is in docs/proposals/proof-bounded-convergence-loops.md (commit 1).

Files changed

  • docs/proposals/proof-bounded-convergence-loops.md — proposal & rationale.
  • packages/proof/src/dag.tsDAGConvergenceLoop, LoopReexecute, LoopStopWhen, ResolvedConvergenceLoop types, validation, resolveConvergenceLoops().
  • packages/proof/src/converge_loop.ts — new resolveLoopReexecuteIds() helper.
  • packages/proof/src/run_dag.tsrunConvergenceLoop accepts a precomputed reExecIds set + loopId; main() iterates dag.loops (or a synthesized loop from the CLI flag).
  • packages/proof/src/index.ts — re-exports new types/helpers.
  • packages/proof/src/__tests__/loops.test.ts — 18 AVA cases.

Testing

  • pnpm --filter @flatbread/proof typecheck
  • pnpm --filter @flatbread/proof build
  • pnpm exec ava packages/proof/src/__tests__/loops.test.ts — 18/18 passing
  • pnpm test:ava — 73/73 passing across the workspace
  • pnpm test:vitest — 46/46 passing across @flatbread/codegen + @flatbread/utils
  • pnpm lint
  • pnpm build (full monorepo)
  • ✅ End-to-end CLI smoke: proof --init-only --dag <loops.json> writes the canvas; proof --dag <loops.json> --converge-on b errors with --converge-on cannot be combined with DAG.loops (DAG "loop-smoke" already declares 1 loop(s)). Remove one.

Notes

The user originally asked for /proof to drive a planning + implementation + self-review loop on this PR with Opus 4.7 (HIGH) and GPT 5.4 (MEDIUM). Three earlier resume attempts on this same agent thread errored out trying to launch the runner, so this PR ships the planning + implementation + tests directly and lets /proof ride the new DAG.loops config in a follow-up rather than blocking the change behind another runner spin-up. The implementation here is exactly the schema /proof would self-review against, so the follow-up is now a one-flag config change instead of "remember to pass --converge-on review --max-iterations 3".

Please don't delete this checklist!

  • I added doc comments to any new public exports, and inline comments to any hard-to-understand areas
  • My changes generate no new console errors locally
  • If applicable, try to include a test that fails without this PR but passes with it

Does this introduce any non-backwards compatible changes?

  • Yes
  • No

Does this include any user config changes?

  • Yes
    • If so, I have updated the relevant areas of documentation
  • No
Open in Web Open in Cursor 

cursoragent and others added 2 commits May 9, 2026 21:20
Captures the design rationale for moving the existing
--converge-on/--max-iterations CLI singleton into a DAG-native
`loops` array. Decision recap:

- DAG `depends_on` edges stay acyclic (causality + parallelism).
- Bounded refinement (research → critique → refine, fix → test → fix)
  becomes an explicit DAG.loops[] config instead of a back-edge.
- The CLI flag stays valid; loops just lift the same shape into the
  JSON file so multiple convergence tasks can stack in one run and
  reproducible runs do not depend on remembered flags.

Out of scope (this proposal): new stopWhen predicates beyond
'no-blockers', nested loops, cross-loop coordination.

Co-authored-by: Tony <tonyketcham@users.noreply.github.com>
Adds DAG.loops[] — an optional array of bounded convergence loops
that runs after the main rank loop completes. Generalizes the legacy
--converge-on/--max-iterations CLI singleton:

- Multiple loops per run (one per convergence task).
- Optional explicit `reexecute.tasks` allow-list, validated to lie
  inside the convergence ancestor cone (off-cone tasks would break
  filtered topological ordering).
- Default `reexecute: { kind: 'ancestors' }` matches the CLI
  behavior bit-for-bit.
- `stopWhen: 'no-blockers'` is the only predicate today; the
  schema is open for future predicates without further churn.

Schema, parsing, and re-execution id resolution all live in
dag.ts/converge_loop.ts; the runner consumes a single canonical
ResolvedConvergenceLoop list whether the user supplied DAG.loops or
the CLI flag (the two cannot be combined — the runner errors at
startup to avoid silent precedence rules).

DAG.budget.maxIterations continues to apply per-loop. Loops execute
sequentially in declaration order; each loop's BUDGET-EXCEEDED is
independent and surfaces via the existing per-task tally.

Tests (AVA, packages/proof/src/__tests__/loops.test.ts):
- 18 cases covering parser acceptance, validation rejections,
  default-filling, ancestor-cone enforcement, multi-loop ids, and
  reexecute selector resolution.
- Smoke verified end-to-end via the proof CLI: --init-only with a
  loops-bearing DAG renders the canvas, and combining --converge-on
  with DAG.loops fails fast with the expected message.

Co-authored-by: Tony <tonyketcham@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants