From f061e186a8f29edc1c5f80d71c72f7b1f7df91d1 Mon Sep 17 00:00:00 2001 From: memtomem <118508877+memtomem@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:01:42 +0900 Subject: [PATCH 1/3] Freeze released bundle, control-plan and apply-crash workflows (#271) The Workflows family of the persisted-contracts D rows, part 2b: the three cases part 2a left as "not yet" because they need new release-written bytes. freeze.py --part 2b adds scenarios to an existing release directory without writing anything already there (each new scenario directory and bindings-2b.json must not exist), from a fresh box of its own. For both v0.2.0 and v0.3.0: - bundle-saved: the worker exits right after the final node's bundle is saved, before the manifest records the node's outcome; then bundle-saved-recovered, after the release's own status. The fake kimi reports token usage here (opt-in), so the bundle carries some. - control-planned, control-completed: Toolgraph control plans on, with a fake toolgraph that answers both protocols from the request it gets. - apply-unreceipted, apply-receipted: a completed workflow's final artifact applied in the release interpreter, dying right after the applying record or right before the applied one (the artifacts family's crash script). The test reads each scenario with its own bindings document and rebinds the apply receipt's repository root (once; it holds no fingerprint). - resume reclaims the released bundle: no provider call, calls_used unchanged, the bundle's usage added once, and the final artifact's patch is the released bundle's; a bundle whose patch, commit record or patch digest disagrees is re-run (one more call, its usage never counted). - run of the released control plan: the control preflight's request is the released control-plan.json bytes, its evidence is ok with the released digest; effective limits are the tighter of current and stored, parallelism capped by the plan, node timeouts the stored ones. A changed candidate list, agent map or gate is refused before any call. With control plans off the plan still bounds the run (one legacy probe for the lead agent); with Toolgraph off, no call at all. - a released control-completed workflow reads as written through show, status, read_control_plan, conversations show and apply; its control-preflight.json is left as it is (no reader revalidates it). - recover classifies a released interrupted apply as completed, not applied or manual recovery, without touching the approval or the row. The doc row names these and drops "partly". Still not covered: the TUI's _read_artifact_result. 12 injected regressions each fail a test here, and part 2a's 24 still do. A thirteenth, skipping node_bundle_is_intact on reclaim, is equivalent at HEAD: read_node_result already verifies the bundle through the same read_node_bundle. Co-Authored-By: Claude Opus 5.5 --- docs/architecture/persisted-contracts.md | 2 +- tests/fixtures/persisted/workflows/freeze.py | 184 +++++++- .../v0.2.0/apply-receipted/runs/.apply.lock | 0 .../applications/20261109T213723644855Z.json | 7 + .../wf-15778afc04544c85-final/manifest.json | 44 ++ .../wf-15778afc04544c85-final/result.json | 7 + .../wf-15778afc04544c85-final/winner.patch | 8 + .../workflows/.locks/wf-15778afc04544c85.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 33 ++ .../winner.patch | 8 + .../wf-15778afc04544c85/nodes/fix/stream.log | 2 + .../wf-15778afc04544c85/planner-output.txt | 1 + .../workflows/wf-15778afc04544c85/spec.json | 25 ++ .../apply-receipted/workflows/workflows.db | Bin 0 -> 24576 bytes .../v0.2.0/apply-unreceipted/runs/.apply.lock | 0 .../wf-4e8e7fcd823e4b05-final/manifest.json | 44 ++ .../wf-4e8e7fcd823e4b05-final/result.json | 7 + .../wf-4e8e7fcd823e4b05-final/winner.patch | 8 + .../workflows/.locks/wf-4e8e7fcd823e4b05.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 33 ++ .../winner.patch | 8 + .../wf-4e8e7fcd823e4b05/nodes/fix/stream.log | 2 + .../wf-4e8e7fcd823e4b05/planner-output.txt | 1 + .../workflows/wf-4e8e7fcd823e4b05/spec.json | 25 ++ .../apply-unreceipted/workflows/workflows.db | Bin 0 -> 24576 bytes .../workflows/v0.2.0/bindings-2b.json | 140 ++++++ .../workflows/.locks/wf-bbcc5e3cb536418e.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 39 ++ .../winner.patch | 8 + .../wf-bbcc5e3cb536418e/nodes/fix/stream.log | 3 + .../wf-bbcc5e3cb536418e/planner-output.txt | 1 + .../workflows/wf-bbcc5e3cb536418e/spec.json | 25 ++ .../workflows/workflows.db | Bin 0 -> 24576 bytes .../workflows/.locks/wf-bbcc5e3cb536418e.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 39 ++ .../winner.patch | 8 + .../wf-bbcc5e3cb536418e/nodes/fix/stream.log | 3 + .../wf-bbcc5e3cb536418e/planner-output.txt | 1 + .../workflows/wf-bbcc5e3cb536418e/spec.json | 25 ++ .../bundle-saved/workflows/workflows.db | Bin 0 -> 24576 bytes .../v0.2.0/control-completed/runs/.apply.lock | 0 .../wf-6c587c6f51e941d8-final/manifest.json | 42 ++ .../wf-6c587c6f51e941d8-final/result.json | 7 + .../wf-6c587c6f51e941d8-final/winner.patch | 8 + .../workflows/.locks/wf-6c587c6f51e941d8.lock | 0 .../wf-6c587c6f51e941d8/control-plan.json | 1 + .../control-plan.json | 1 + .../control-preflight.json | 1 + .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 33 ++ .../winner.patch | 8 + .../wf-6c587c6f51e941d8/nodes/fix/stream.log | 2 + .../wf-6c587c6f51e941d8/planner-output.txt | 1 + .../workflows/wf-6c587c6f51e941d8/spec.json | 25 ++ .../control-completed/workflows/workflows.db | Bin 0 -> 24576 bytes .../workflows/.locks/wf-6494f914db7441a1.lock | 0 .../wf-6494f914db7441a1/control-plan.json | 1 + .../wf-6494f914db7441a1/planner-output.txt | 1 + .../workflows/wf-6494f914db7441a1/spec.json | 25 ++ .../control-planned/workflows/workflows.db | Bin 0 -> 24576 bytes .../v0.3.0/apply-receipted/runs/.apply.lock | 0 .../applications/20261109T213824926811Z.json | 7 + .../wf-3a0e23f792ce42d7-final/manifest.json | 45 ++ .../wf-3a0e23f792ce42d7-final/result.json | 7 + .../wf-3a0e23f792ce42d7-final/winner.patch | 8 + .../workflows/.locks/wf-3a0e23f792ce42d7.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 33 ++ .../winner.patch | 8 + .../wf-3a0e23f792ce42d7/nodes/fix/stream.log | 2 + .../wf-3a0e23f792ce42d7/planner-output.txt | 1 + .../workflows/wf-3a0e23f792ce42d7/spec.json | 25 ++ .../apply-receipted/workflows/workflows.db | Bin 0 -> 36864 bytes .../v0.3.0/apply-unreceipted/runs/.apply.lock | 0 .../wf-d17d6f7b1a6c4bb1-final/manifest.json | 45 ++ .../wf-d17d6f7b1a6c4bb1-final/result.json | 7 + .../wf-d17d6f7b1a6c4bb1-final/winner.patch | 8 + .../workflows/.locks/wf-d17d6f7b1a6c4bb1.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 33 ++ .../winner.patch | 8 + .../wf-d17d6f7b1a6c4bb1/nodes/fix/stream.log | 2 + .../wf-d17d6f7b1a6c4bb1/planner-output.txt | 1 + .../workflows/wf-d17d6f7b1a6c4bb1/spec.json | 25 ++ .../apply-unreceipted/workflows/workflows.db | Bin 0 -> 36864 bytes .../workflows/v0.3.0/bindings-2b.json | 140 ++++++ .../workflows/.locks/wf-061b5a03465742aa.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 39 ++ .../winner.patch | 8 + .../wf-061b5a03465742aa/nodes/fix/stream.log | 3 + .../wf-061b5a03465742aa/planner-output.txt | 1 + .../workflows/wf-061b5a03465742aa/spec.json | 25 ++ .../workflows/workflows.db | Bin 0 -> 36864 bytes .../workflows/.locks/wf-061b5a03465742aa.lock | 0 .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 39 ++ .../winner.patch | 8 + .../wf-061b5a03465742aa/nodes/fix/stream.log | 3 + .../wf-061b5a03465742aa/planner-output.txt | 1 + .../workflows/wf-061b5a03465742aa/spec.json | 25 ++ .../bundle-saved/workflows/workflows.db | Bin 0 -> 36864 bytes .../v0.3.0/control-completed/runs/.apply.lock | 0 .../wf-2c174f13cfcd4c62-final/manifest.json | 43 ++ .../wf-2c174f13cfcd4c62-final/result.json | 7 + .../wf-2c174f13cfcd4c62-final/winner.patch | 8 + .../workflows/.locks/wf-2c174f13cfcd4c62.lock | 0 .../wf-2c174f13cfcd4c62/control-plan.json | 1 + .../control-plan.json | 1 + .../control-preflight.json | 1 + .../commit.json | 1 + .../result.json | 27 ++ .../winner.patch | 8 + .../nodes/draft/stream.log | 3 + .../commit.json | 1 + .../result.json | 33 ++ .../winner.patch | 8 + .../wf-2c174f13cfcd4c62/nodes/fix/stream.log | 2 + .../wf-2c174f13cfcd4c62/planner-output.txt | 1 + .../workflows/wf-2c174f13cfcd4c62/spec.json | 25 ++ .../control-completed/workflows/workflows.db | Bin 0 -> 36864 bytes .../workflows/.locks/wf-68b63dd2ed314937.lock | 0 .../wf-68b63dd2ed314937/control-plan.json | 1 + .../wf-68b63dd2ed314937/planner-output.txt | 1 + .../workflows/wf-68b63dd2ed314937/spec.json | 25 ++ .../control-planned/workflows/workflows.db | Bin 0 -> 36864 bytes tests/test_frozen_workflows.py | 406 +++++++++++++++++- 167 files changed, 2393 insertions(+), 24 deletions(-) create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/.apply.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/applications/20261109T213723644855Z.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/manifest.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/.locks/wf-15778afc04544c85.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/runs/.apply.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/runs/wf-4e8e7fcd823e4b05-final/manifest.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/runs/wf-4e8e7fcd823e4b05-final/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/runs/wf-4e8e7fcd823e4b05-final/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/.locks/wf-4e8e7fcd823e4b05.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/draft/806172f1ce0b4475ba7c8c01844b21d0-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/draft/806172f1ce0b4475ba7c8c01844b21d0-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/draft/806172f1ce0b4475ba7c8c01844b21d0-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/fix/806172f1ce0b4475ba7c8c01844b21d0-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/fix/806172f1ce0b4475ba7c8c01844b21d0-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/fix/806172f1ce0b4475ba7c8c01844b21d0-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/wf-4e8e7fcd823e4b05/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/apply-unreceipted/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bindings-2b.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/.locks/wf-bbcc5e3cb536418e.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/.locks/wf-bbcc5e3cb536418e.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/.apply.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/manifest.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/.locks/wf-6c587c6f51e941d8.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/control-plan.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-plan.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-preflight.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/.locks/wf-6494f914db7441a1.lock create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/control-plan.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/runs/.apply.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/runs/wf-3a0e23f792ce42d7-final/applications/20261109T213824926811Z.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/runs/wf-3a0e23f792ce42d7-final/manifest.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/runs/wf-3a0e23f792ce42d7-final/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/runs/wf-3a0e23f792ce42d7-final/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/.locks/wf-3a0e23f792ce42d7.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/draft/40d12bc54e174ed59233a2ab26cb6e52-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/draft/40d12bc54e174ed59233a2ab26cb6e52-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/draft/40d12bc54e174ed59233a2ab26cb6e52-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/fix/40d12bc54e174ed59233a2ab26cb6e52-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/fix/40d12bc54e174ed59233a2ab26cb6e52-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/fix/40d12bc54e174ed59233a2ab26cb6e52-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/wf-3a0e23f792ce42d7/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-receipted/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/runs/.apply.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/runs/wf-d17d6f7b1a6c4bb1-final/manifest.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/runs/wf-d17d6f7b1a6c4bb1-final/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/runs/wf-d17d6f7b1a6c4bb1-final/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/.locks/wf-d17d6f7b1a6c4bb1.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/draft/718425862f0f4bf19c80094f5f12983c-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/draft/718425862f0f4bf19c80094f5f12983c-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/draft/718425862f0f4bf19c80094f5f12983c-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/fix/718425862f0f4bf19c80094f5f12983c-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/fix/718425862f0f4bf19c80094f5f12983c-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/fix/718425862f0f4bf19c80094f5f12983c-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/wf-d17d6f7b1a6c4bb1/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/apply-unreceipted/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bindings-2b.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/.locks/wf-061b5a03465742aa.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/draft/77aeb311175645c1b474da6302ce6417-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/draft/77aeb311175645c1b474da6302ce6417-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/draft/77aeb311175645c1b474da6302ce6417-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/fix/77aeb311175645c1b474da6302ce6417-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/fix/77aeb311175645c1b474da6302ce6417-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/fix/77aeb311175645c1b474da6302ce6417-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/wf-061b5a03465742aa/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved-recovered/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/.locks/wf-061b5a03465742aa.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/draft/77aeb311175645c1b474da6302ce6417-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/draft/77aeb311175645c1b474da6302ce6417-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/draft/77aeb311175645c1b474da6302ce6417-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/fix/77aeb311175645c1b474da6302ce6417-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/fix/77aeb311175645c1b474da6302ce6417-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/fix/77aeb311175645c1b474da6302ce6417-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/wf-061b5a03465742aa/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/bundle-saved/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/runs/.apply.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/runs/wf-2c174f13cfcd4c62-final/manifest.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/runs/wf-2c174f13cfcd4c62-final/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/runs/wf-2c174f13cfcd4c62-final/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/.locks/wf-2c174f13cfcd4c62.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/control-plan.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/executions/wfx-7b1756745c774ac5a4238b32/control-plan.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/executions/wfx-7b1756745c774ac5a4238b32/control-preflight.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/draft/8c977435d4644e4ba789d45f65f14991-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/draft/8c977435d4644e4ba789d45f65f14991-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/draft/8c977435d4644e4ba789d45f65f14991-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/draft/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/fix/8c977435d4644e4ba789d45f65f14991-1/commit.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/fix/8c977435d4644e4ba789d45f65f14991-1/result.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/fix/8c977435d4644e4ba789d45f65f14991-1/winner.patch create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/nodes/fix/stream.log create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/wf-2c174f13cfcd4c62/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-completed/workflows/workflows.db create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/.locks/wf-68b63dd2ed314937.lock create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/control-plan.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/planner-output.txt create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/spec.json create mode 100644 tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/workflows.db diff --git a/docs/architecture/persisted-contracts.md b/docs/architecture/persisted-contracts.md index 594ea8d..fc8a5b3 100644 --- a/docs/architecture/persisted-contracts.md +++ b/docs/architecture/persisted-contracts.md @@ -53,7 +53,7 @@ A golden over the current writer does not protect these. A reader can stop accep |---|---|---|---| | Artifacts | run `manifest.json`, `result.json`, `winner.patch`, apply receipts (`applications/*.json`), in every state a standalone `run` leaves them (room-, session- and workflow-origin artifacts are frozen with those families) | `runs list` / `show` / `prune`, `conversations list` / `show`, `diff`, `apply`, `recover` (`_bound_receipt`), `continue`, the TUI artifact view | `test_frozen_artifacts.py` (v0.1.2, v0.3.0) | | Rooms | the room database (`rooms.sqlite3`, with the WAL a killed process leaves): rooms, rounds, candidates, members, workspaces, events, `sessions_json`, `selection_json` v1, `provenance_json` v1/v2, `conversation_json` v1; candidate `manifest.json`, patch and output; room-bound run artifacts. No release wrote `conversation_json` v2 or judgment rows | `room list` / `rounds` / `show` / `candidates` / `diff` / `compare` / `export`, `select`, `apply`, `judge`, `recover`, `close`, `prune` `RoomStore.member_sessions` (and through these `RoomCandidateStore.load`, `_parse_sessions_json`, `selection_record_status`, `room_outcome_status`, `round_provenance_status`, `round_conversation_status`); the standalone `apply` / `recover` refusal and skip for room-bound artifacts; a HEAD `room round` on a released room (`branch-from` resuming the released sessions on the released base patch, `fresh`, a first round on a draft, and the `continue-own` refusal when the released workspaces are gone); `room attach` over the released member log paths (tmux faked); the `apply --verify` settings notices; raw export (`--include` preview and confirm); the TUI Rooms tab's readers and pure renders (`build_room_evidence`, `render_room`, `render_round_table`, and the Diff, Output and Details views), not the Textual app | `test_frozen_rooms.py` (v0.1.2, v0.2.0, v0.3.0) | -| Workflows | the workflow database (`workflows.db`): rows with `spec_json`, manifest `schema_version` 3 with `calls_used`, events, and from v0.3.0 the lineage columns and `file`/`revision` origins; per workflow `spec.json`, planner output, node bundles (`result.json`, `winner.patch`, `commit.json` v3) and stream logs; workflow-origin run artifacts, approved or not, with `plan.final_agent` from v0.3.0. No release wrote manifest v1 or v2 (both normalize to v3 before the insert); those readers stay covered by `test_workflow_durable.py::test_marked_v1_migration_preserves_exact_count_and_control_fields` and `test_workflow.py::test_a_workflow_whose_call_accounting_is_ambiguous_is_refused` | `workflow list` / `show` (`--prompts`) / `status` / `logs` / `lineage` / `export` / `cancel` / `prune`, a dead release-written lease recovered by `show` and `status`, `WorkflowStore.events` and `read_node_bundle`; for the final artifact `diff`, `runs show`, `apply` (approved, after a prune, approval healed from the database, refused for a workflow that never completed, `no_changes`), the standalone `recover` skip. A final artifact naming another run, and an unreadable store: `test_cli.py::test_apply_refuses_an_artifact_the_named_workflow_did_not_finalize` and `test_apply_guard_fails_closed_when_the_workflow_store_cannot_be_read`. HEAD execution (`WorkflowCoordinator.run`, what the job worker calls): `run` of released planned, created and revised rows (accounting, stored prompts, dependency output); `resume` of released crashes and failures against the stored deadline (expired: fails before dispatch; before it: the completed dependency's bundle is reused and the unfinished node's attempt refunded, a published final artifact is reused and approved without backfilling `final_agent`, an exhausted node dispatches nothing), refused from a final or unstarted state and while the owner lives, with the released execution history and events kept; `workflow revise` (a v0.2.0 parent gains a lineage); the launch's team peek and state check; `apply --verify` narrowing (v0.2.0: the gate's list only, with a notice; also after a prune); control plans enabled refuse a released workflow planned without one. Not yet: control-plan (toolgraph) workflows written by a release, a node bundle saved just before a crash (reclaimed on resume), recovery of an interrupted workflow-origin apply | `test_frozen_workflows.py` (v0.2.0, v0.3.0), partly | +| Workflows | the workflow database (`workflows.db`): rows with `spec_json`, manifest `schema_version` 3 with `calls_used`, events, and from v0.3.0 the lineage columns and `file`/`revision` origins; per workflow `spec.json`, planner output, node bundles (`result.json`, `winner.patch`, `commit.json` v3) and stream logs; workflow-origin run artifacts, approved or not, with `plan.final_agent` from v0.3.0. No release wrote manifest v1 or v2 (both normalize to v3 before the insert); those readers stay covered by `test_workflow_durable.py::test_marked_v1_migration_preserves_exact_count_and_control_fields` and `test_workflow.py::test_a_workflow_whose_call_accounting_is_ambiguous_is_refused` | `workflow list` / `show` (`--prompts`) / `status` / `logs` / `lineage` / `export` / `cancel` / `prune`, a dead release-written lease recovered by `show` and `status`, `WorkflowStore.events` and `read_node_bundle`; for the final artifact `diff`, `runs show`, `apply` (approved, after a prune, approval healed from the database, refused for a workflow that never completed, `no_changes`), the standalone `recover` skip. A final artifact naming another run, and an unreadable store: `test_cli.py::test_apply_refuses_an_artifact_the_named_workflow_did_not_finalize` and `test_apply_guard_fails_closed_when_the_workflow_store_cannot_be_read`. HEAD execution (`WorkflowCoordinator.run`, what the job worker calls): `run` of released planned, created and revised rows (accounting, stored prompts, dependency output); `resume` of released crashes and failures against the stored deadline (expired: fails before dispatch; before it: the completed dependency's bundle is reused and the unfinished node's attempt refunded, a published final artifact is reused and approved without backfilling `final_agent`, an exhausted node dispatches nothing), refused from a final or unstarted state and while the owner lives, with the released execution history and events kept; `workflow revise` (a v0.2.0 parent gains a lineage); the launch's team peek and state check; `apply --verify` narrowing (v0.2.0: the gate's list only, with a notice; also after a prune); control plans enabled refuse a released workflow planned without one. Written later by the same releases (`freeze.py --part 2b`, `bindings-2b.json`): a node bundle saved just before the crash, reclaimed on resume through `read_node_result` / `read_node_bundle` with its usage added once (`_record_usage`), and re-run when its patch, its commit record or its digest disagrees; control-plan workflows, whose released `control-plan.json` is the control preflight's request, whose binding and validation topology refuse a changed configuration, and whose stored limits and node timeouts bound a run with control plans on, off, or Toolgraph off (`_execution_limits`), plus `show`, `read_control_plan`, `conversations show` and `apply` of a completed one; an interrupted apply of a final artifact, classified by `recover` (completed, not applied, manual recovery; `_bound_receipt`) without touching its approval or the workflow row. Not covered: the TUI's `_read_artifact_result`; no reader revalidates a released execution's `control-preflight.json` | `test_frozen_workflows.py` (v0.2.0, v0.3.0) | | Sessions and project | isolated-session documents and their checkpoint run artifacts, project marker `.syncmill/project.json` | `IsolatedSessionStore.load` / `list`, `_marker_project_id`; the artifact readers for checkpoint runs | not yet | | Board | item rows and their evidence and inputs, decisions | `_row_to_item`, `_load_evidence`, `_row_to_decision` | not yet | | Memory backends | Mem0 and LangGraph stored payloads | the backends' read paths | not yet | diff --git a/tests/fixtures/persisted/workflows/freeze.py b/tests/fixtures/persisted/workflows/freeze.py index 8cd7f57..9edaaf4 100644 --- a/tests/fixtures/persisted/workflows/freeze.py +++ b/tests/fixtures/persisted/workflows/freeze.py @@ -32,6 +32,23 @@ copied byte for byte once every writer has stopped, including any WAL a killed process leaves. Workspaces and TUI job records are not copied. `bindings.json` records the values bound to the generating machine. + +`--part 2b` adds scenarios to an existing release directory without writing +anything already in it: each new scenario directory and `bindings-2b.json` +must not exist. Its scenarios, from a fresh box of their own: +- `bundle-saved`: the worker exits right after the final node's bundle is + saved, before the manifest records the node's outcome; then + `bundle-saved-recovered`, after the release's own `status`. The fake kimi + reports token usage in these, so the bundle carries some. +- `control-planned`, `control-completed`: with Toolgraph control plans on and + a fake `toolgraph` that answers both of its protocols from the request. +- `apply-unreceipted`, `apply-receipted`: a completed workflow's final + artifact applied in the release interpreter, dying right after the + `applying` record or right before the `applied` one (the artifacts family's + crash script). + + python tests/fixtures/persisted/workflows/freeze.py --part 2b \\ + --release-root /tmp/wt-v0.3.0 --out tests/fixtures/persisted/workflows/v0.3.0 """ from __future__ import annotations @@ -102,6 +119,9 @@ {"role": "meta", "type": "session.resume_hint", "session_id": "fake-kimi-session", "content": "To resume this session: kimi -r fake-kimi-session"}, ] + if control.get("kimi_usage"): + events.append({"role": "meta", "type": "usage", + "usage": {"input_tokens": 7, "output_tokens": 3}}) for event in events: print(json.dumps(event)) ''' @@ -141,10 +161,98 @@ def save(self, *args, **kwargs): os._exit(0) return saved ArtifactStore.save = save + elif point == "bundle": + from syncmill.workflow import WorkflowStore + + original_save = WorkflowStore.save_node_result + + def save_node_result(self, workflow_id, node_id, *args, **kwargs): + saved = original_save(self, workflow_id, node_id, *args, **kwargs) + if node_id == "fix": + os._exit(0) # bundle on disk, the node's outcome never recorded + return saved + WorkflowStore.save_node_result = save_node_result _install() ''' +# Both Toolgraph protocols (`preflight.py` is identical in v0.2.0, v0.3.0 and +# HEAD), each answered from the request it gets, never a fixed golden. +FAKE_TOOLGRAPH = '''\ +import hashlib, json, sys +from pathlib import Path +args = sys.argv[1:] +out = Path(args[args.index("--out") + 1]) +profile = args[args.index("--profile") + 1] +if args[0] == "control-preflight": + raw = Path(args[1]).read_bytes() + plan = json.loads(raw) + doc = { + "schema_version": 1, "kind": "toolgraph.control-preflight", + "run_id": plan["run_id"], "created_at": "2026-11-10T00:00:00+00:00", + "mode": "advisory", "profile": profile, + "plan_digest": "sha256:" + hashlib.sha256(raw).hexdigest(), + "graph_state": {"instance_id": "frozen-graph", "generation": 7}, + "decision": "advisory_allow", + "evaluations": [ + {"principal": row["id"], "agent_found": True, + "eligible": list(row["candidate_tools"]), "rejected": []} + for row in plan["principals"] + ], + "findings": [], + } +elif args[0] == "preflight": + agent, tools = args[1], args[2:args.index("--profile")] + doc = { + "schema_version": 1, "kind": "toolgraph.preflight", + "run_id": args[args.index("--run-id") + 1], + "created_at": "2026-11-10T00:00:00+00:00", "graph_generation": 7, + "agent": agent, "agent_found": True, "profile": profile, "mode": "advisory", + "eligible": list(tools), "rejected": [], "features": [], + "decision": "advisory_allow", + } +else: + sys.exit("fake toolgraph: unknown command " + repr(args[:1])) +out.write_text(json.dumps(doc), encoding="utf-8") +''' +TOOLGRAPH_ENV = { + "SYNCMILL_INTEGRATION__TOOLGRAPH__ENABLED": "true", + "SYNCMILL_INTEGRATION__TOOLGRAPH__CONTROL_PLAN_ENABLED": "true", + "SYNCMILL_INTEGRATION__TOOLGRAPH__CANDIDATE_TOOLS": json.dumps( + ["workspace::read", "workspace::write"] + ), + "SYNCMILL_INTEGRATION__TOOLGRAPH__AGENT_MAP": json.dumps( + {"codex": "codex", "kimi-code": "kimi"} + ), +} + +# The artifacts family's crash script: runs inside the released interpreter, +# applies, then dies at one manifest write. +CRASH_APPLY = '''\ +import os, sys +from pathlib import Path +from syncmill.artifacts import ArtifactStore +from syncmill.worktree import repository_fingerprint +runs_root, project, run_id, repo, die_at = sys.argv[1:6] +original = ArtifactStore._compare_and_swap_manifest +def die(self, run_dir, expected, replacement): + state = replacement.get("state") + if die_at == "before-applied" and state == "applied": + os._exit(0) # receipt written, patch in the source, flip never happens + original(self, run_dir, expected, replacement) + if die_at == "after-applying" and state == "applying": + os._exit(0) # applying recorded, source not yet touched, no receipt +ArtifactStore._compare_and_swap_manifest = die +store = ArtifactStore(Path(runs_root), project) +store.apply(run_id, Path(repo), repository_fingerprint(Path(repo))) +sys.exit("apply returned; the crash hook never fired") +''' + +PART_2B = ( + "bundle-saved", "bundle-saved-recovered", "control-planned", "control-completed", + "apply-unreceipted", "apply-receipted", +) + HOOK_MODULE = "_syncmill_freeze_hook" FROZEN_HOST = "frozen-host" @@ -191,17 +299,27 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--release-root", type=Path, required=True) parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--part", choices=("1", "2b"), default="1") args = parser.parse_args() release = args.release_root.resolve() out = args.out.resolve() - if out.exists(): - sys.exit(f"{out} exists; frozen fixtures are never regenerated") python = release / ".venv" / "bin" / "python" syncmill = release / ".venv" / "bin" / "syncmill" version = subprocess.run( [str(python), "-c", "import importlib.metadata as m; print(m.version('syncmill'))"], check=True, capture_output=True, text=True, ).stdout.strip() + if args.part == "1": + if out.exists(): + sys.exit(f"{out} exists; frozen fixtures are never regenerated") + else: + # Additive: the release directory exists, and nothing this part + # writes may be there already. + if json.loads((out / "bindings.json").read_text())["release"] != version: + sys.exit(f"{out} holds another release's fixtures") + taken = [name for name in (*PART_2B, "bindings-2b.json") if (out / name).exists()] + if taken: + sys.exit(f"{out} already has {taken}; frozen fixtures are never regenerated") site = _site_packages(python) hook_files = [site / f"{HOOK_MODULE}.py", site / f"zz{HOOK_MODULE}.pth"] if any(path.exists() for path in hook_files): @@ -214,14 +332,16 @@ def main() -> None: hook_files[0].write_text(CRASH_HOOK, encoding="utf-8") hook_files[1].write_text(f"import {HOOK_MODULE}\n", encoding="utf-8") with tempfile.TemporaryDirectory() as tmp: - _generate(Path(tmp).resolve(), runtime, syncmill, version, out) + _generate(Path(tmp).resolve(), runtime, python, syncmill, version, out, args.part) finally: for path in hook_files: path.unlink(missing_ok=True) shutil.rmtree(runtime, ignore_errors=True) -def _generate(box: Path, runtime: Path, syncmill: Path, version: str, out: Path) -> None: +def _generate( + box: Path, runtime: Path, python: Path, syncmill: Path, version: str, out: Path, part: str +) -> None: repo, home, state, fakebin = (box / n for n in ("repo", "home", "state", "fakebin")) for folder in (repo, home, state, fakebin): folder.mkdir() @@ -230,6 +350,11 @@ def _generate(box: Path, runtime: Path, syncmill: Path, version: str, out: Path) (fakebin / binary).write_text(f"#!{sys.executable}\n" + FAKE_AGENT, encoding="utf-8") (fakebin / binary).chmod(0o755) (fakebin / "python3").symlink_to(sys.executable) + if part == "2b": + (fakebin / "toolgraph").write_text( + f"#!{sys.executable}\n" + FAKE_TOOLGRAPH, encoding="utf-8" + ) + (fakebin / "toolgraph").chmod(0o755) base_env = { "HOME": str(home), "PATH": os.pathsep.join([str(fakebin), "/usr/bin", "/bin"]), @@ -258,7 +383,7 @@ def git(*argv: str) -> None: git("add", "-A") git("commit", "-q", "-m", "frozen tree") - out.mkdir(parents=True) + out.mkdir(parents=True, exist_ok=part == "2b") bindings: dict = { "release": version, "box_root": str(box), @@ -269,10 +394,11 @@ def git(*argv: str) -> None: } class Scenario: - def __init__(self, name: str, *, gate: bool = True, **control: str): + def __init__(self, name: str, *, gate: bool = True, env: dict | None = None, + **control): self.name = name self.project = f"frozen-workflows-{name}" - self.env = {**base_env, "SYNCMILL_PROJECT__ID": self.project} + self.env = {**base_env, **(env or {}), "SYNCMILL_PROJECT__ID": self.project} if gate: self.env["SYNCMILL_GATE__COMMANDS"] = json.dumps(GATE) self.control = {"spec": SPEC, **control} @@ -392,6 +518,11 @@ def run_until(sc: Scenario, workflow_id: str, marker: str) -> None: sys.exit(f"{sc.name}: the worker or its agent survived SIGKILL") time.sleep(0.1) + if part == "2b": + _part_2b(Scenario, freeze, git, home, state, repo, python) + (out / "bindings-2b.json").write_text(json.dumps(bindings, indent=2) + "\n") + return + # ── the lifecycle ──────────────────────────────────────────────────── sc = Scenario("planned") sc.plan() @@ -469,5 +600,44 @@ def run_until(sc: Scenario, workflow_id: str, marker: str) -> None: (out / "bindings.json").write_text(json.dumps(bindings, indent=2) + "\n") +def _part_2b(Scenario, freeze, git, home: Path, state: Path, repo: Path, python: Path) -> None: + def crashed_run(sc, workflow_id: str, point: str) -> None: + sc.prepare(crash_at=point) + sc.workflow("run", workflow_id, ok=False) + (home / "crash-at").unlink() + + # The worker exits right after the final node's bundle is saved; then + # the release's own `status` recovers the dead lease. + sc = Scenario("bundle-saved", kimi_usage=True) + workflow_id = sc.plan() + crashed_run(sc, workflow_id, "bundle") + freeze(sc) + sc.name = "bundle-saved-recovered" + sc.workflow("status", workflow_id) + freeze(sc) + + sc = Scenario("control-planned", env=TOOLGRAPH_ENV) + sc.plan() + freeze(sc) + + sc = Scenario("control-completed", env=TOOLGRAPH_ENV) + workflow_id = sc.plan() + sc.workflow("run", workflow_id) + freeze(sc) + + for name, die_at in (("apply-unreceipted", "after-applying"), + ("apply-receipted", "before-applied")): + sc = Scenario(name) + workflow_id = sc.plan() + sc.workflow("run", workflow_id) + subprocess.run( + [str(python), "-c", CRASH_APPLY, str(state / "runs"), sc.project, + f"{workflow_id}-final", str(repo), die_at], + cwd=repo, env=sc.env, check=True, + ) + freeze(sc) + git("checkout", "--", ".") + + if __name__ == "__main__": main() diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/.apply.lock b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/.apply.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/applications/20261109T213723644855Z.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/applications/20261109T213723644855Z.json new file mode 100644 index 0000000..0e44e7a --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/applications/20261109T213723644855Z.json @@ -0,0 +1,7 @@ +{ + "applied_at": "2026-11-09T21:37:23.644881+00:00", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpnhjq8lqv/repo", + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "apply_id": "94ef2849-5ce2-40cd-b87c-284c34be0fc5", + "run_id": "wf-15778afc04544c85-final" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/manifest.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/manifest.json new file mode 100644 index 0000000..4800912 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/manifest.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "run_id": "wf-15778afc04544c85-final", + "project_id": "frozen-workflows-apply-receipted", + "parent_run_id": null, + "created_at": "2026-11-09T21:37:21.321318+00:00", + "updated_at": "2026-11-09T21:37:23.607822+00:00", + "state": "applying", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpnhjq8lqv/repo", + "source_fingerprint": "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef", + "snapshot_commit": "7e90c6990ae7e08ea53f3d9c8a2b5ca8f03f0452", + "task_id": "4c27a7b7-0a62-4bd2-9359-638df1bb18d2", + "prompt": "fix add", + "strategy": "route", + "agents": [ + "kimi-code" + ], + "plan": { + "origin": "workflow", + "workflow_id": "wf-15778afc04544c85", + "node_id": "fix", + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "source_fingerprint": "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef", + "bundle_sha256": "0bd653ba0ada6e52667e0096e9cc24baa8b1f3c8f389b464449bde8e66820a13", + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "approved": { + "workflow_id": "wf-15778afc04544c85", + "run_id": "wf-15778afc04544c85-final", + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "approved_at": "2026-11-09T21:37:21.332020+00:00" + } + }, + "agent_id": "kimi-code", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null, + "apply_id": "94ef2849-5ce2-40cd-b87c-284c34be0fc5", + "apply_owner": "15589-7fe8fa59", + "apply_started_at": "2026-11-09T21:37:23.607711+00:00" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/result.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/result.json new file mode 100644 index 0000000..b429c06 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "kimi (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02 +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/runs/wf-15778afc04544c85-final/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/.locks/wf-15778afc04544c85.lock b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/.locks/wf-15778afc04544c85.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/commit.json new file mode 100644 index 0000000..a7485cd --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-15778afc04544c85", "node_id": "draft", "lease_id": "cd158718c45243ebbe0deb1b4562feb8", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "04439868670029d93d31e925c5c046bb6d94202e3b1042585d3cedf16433388b"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/result.json new file mode 100644 index 0000000..352a33e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/result.json @@ -0,0 +1,27 @@ +{ + "status": "completed", + "agent_id": "codex", + "output": "codex (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02, + "files_changed": [ + "calc.py" + ], + "gate": null, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-15778afc04544c85", + "node_id": "draft", + "lease_id": "cd158718c45243ebbe0deb1b4562feb8", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": { + "total_input_tokens": 1, + "output_tokens": 1, + "cache_read_tokens": 0, + "cache_creation_tokens": null, + "total_cost_usd": null + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/stream.log new file mode 100644 index 0000000..f5748d5 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/draft/stream.log @@ -0,0 +1,3 @@ +{"type": "thread.started", "thread_id": "fake-codex-thread"} +{"type": "item.completed", "item": {"type": "agent_message", "text": "codex (fake) fix."}} +{"type": "turn.completed", "usage": {"input_tokens": 1, "output_tokens": 1}} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/commit.json new file mode 100644 index 0000000..6055da7 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-15778afc04544c85", "node_id": "fix", "lease_id": "cd158718c45243ebbe0deb1b4562feb8", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "0bd653ba0ada6e52667e0096e9cc24baa8b1f3c8f389b464449bde8e66820a13"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/result.json new file mode 100644 index 0000000..1b88c07 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/result.json @@ -0,0 +1,33 @@ +{ + "status": "completed", + "agent_id": "kimi-code", + "output": "kimi (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02, + "files_changed": [ + "calc.py" + ], + "gate": { + "passed": true, + "score": 1.0, + "checks": [ + { + "command": "python3 -c \"from calc import add; assert add(2, 3) == 5\"", + "passed": true, + "exit_code": 0, + "output": "", + "duration_seconds": 0.02 + } + ] + }, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-15778afc04544c85", + "node_id": "fix", + "lease_id": "cd158718c45243ebbe0deb1b4562feb8", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": null +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/cd158718c45243ebbe0deb1b4562feb8-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/stream.log new file mode 100644 index 0000000..9ee3cc1 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/nodes/fix/stream.log @@ -0,0 +1,2 @@ +{"role": "assistant", "content": "kimi (fake) fix."} +{"role": "meta", "type": "session.resume_hint", "session_id": "fake-kimi-session", "content": "To resume this session: kimi -r fake-kimi-session"} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/planner-output.txt b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/planner-output.txt new file mode 100644 index 0000000..b6d2b3c --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/planner-output.txt @@ -0,0 +1 @@ +{"schema_version": 1, "goal": "fix add", "final_node": "fix", "nodes": [{"id": "draft", "agent": "codex", "prompt": "draft the fix"}, {"id": "fix", "agent": "kimi-code", "prompt": "apply the fix", "needs": ["draft"]}]} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/spec.json b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/spec.json new file mode 100644 index 0000000..f04e3ad --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/wf-15778afc04544c85/spec.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "goal": "fix add", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix", + "needs": [], + "timeout_seconds": null, + "max_attempts": 1 + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ], + "timeout_seconds": null, + "max_attempts": 1 + } + ], + "final_node": "fix" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/workflows.db b/tests/fixtures/persisted/workflows/v0.2.0/apply-receipted/workflows/workflows.db new file mode 100644 index 0000000000000000000000000000000000000000..d3c5ac032ef5275354ab71f248936d640cec670f GIT binary patch literal 24576 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 J{3*ocZvb4a;t&7; literal 0 HcmV?d00001 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bindings-2b.json b/tests/fixtures/persisted/workflows/v0.2.0/bindings-2b.json new file mode 100644 index 0000000..8f18036 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bindings-2b.json @@ -0,0 +1,140 @@ +{ + "release": "0.2.0", + "box_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpnhjq8lqv", + "hostname": "frozen-host", + "tree": { + "calc.py": "def add(a, b):\n return a - b\n" + }, + "spec": { + "schema_version": 1, + "goal": "fix add", + "final_node": "fix", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix" + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ] + } + ] + }, + "scenarios": { + "bundle-saved": { + "project_id": "frozen-workflows-bundle-saved", + "workflow_ids": [ + "wf-bbcc5e3cb536418e" + ], + "states": { + "wf-bbcc5e3cb536418e": "running" + }, + "source_fingerprints": [ + "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef" + ], + "lease_owners": [ + "15137@frozen-host" + ], + "final_run_ids": [], + "run_artifacts": [], + "wal": [] + }, + "bundle-saved-recovered": { + "project_id": "frozen-workflows-bundle-saved", + "workflow_ids": [ + "wf-bbcc5e3cb536418e" + ], + "states": { + "wf-bbcc5e3cb536418e": "recovery_required" + }, + "source_fingerprints": [ + "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef" + ], + "lease_owners": [], + "final_run_ids": [], + "run_artifacts": [], + "wal": [] + }, + "control-planned": { + "project_id": "frozen-workflows-control-planned", + "workflow_ids": [ + "wf-6494f914db7441a1" + ], + "states": { + "wf-6494f914db7441a1": "planned" + }, + "source_fingerprints": [ + "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef" + ], + "lease_owners": [], + "final_run_ids": [], + "run_artifacts": [], + "wal": [] + }, + "control-completed": { + "project_id": "frozen-workflows-control-completed", + "workflow_ids": [ + "wf-6c587c6f51e941d8" + ], + "states": { + "wf-6c587c6f51e941d8": "completed" + }, + "source_fingerprints": [ + "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef" + ], + "lease_owners": [], + "final_run_ids": [ + "wf-6c587c6f51e941d8-final" + ], + "run_artifacts": [ + "wf-6c587c6f51e941d8-final" + ], + "wal": [] + }, + "apply-unreceipted": { + "project_id": "frozen-workflows-apply-unreceipted", + "workflow_ids": [ + "wf-4e8e7fcd823e4b05" + ], + "states": { + "wf-4e8e7fcd823e4b05": "completed" + }, + "source_fingerprints": [ + "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef" + ], + "lease_owners": [], + "final_run_ids": [ + "wf-4e8e7fcd823e4b05-final" + ], + "run_artifacts": [ + "wf-4e8e7fcd823e4b05-final" + ], + "wal": [] + }, + "apply-receipted": { + "project_id": "frozen-workflows-apply-receipted", + "workflow_ids": [ + "wf-15778afc04544c85" + ], + "states": { + "wf-15778afc04544c85": "completed" + }, + "source_fingerprints": [ + "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef" + ], + "lease_owners": [], + "final_run_ids": [ + "wf-15778afc04544c85-final" + ], + "run_artifacts": [ + "wf-15778afc04544c85-final" + ], + "wal": [] + } + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/.locks/wf-bbcc5e3cb536418e.lock b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/.locks/wf-bbcc5e3cb536418e.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json new file mode 100644 index 0000000..540a358 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-bbcc5e3cb536418e", "node_id": "draft", "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "2a9b347efb9398f3b583e83f4cbab96fd2c1d5abb00edcb265ea73c04f341237"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json new file mode 100644 index 0000000..cdcf72a --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json @@ -0,0 +1,27 @@ +{ + "status": "completed", + "agent_id": "codex", + "output": "codex (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02, + "files_changed": [ + "calc.py" + ], + "gate": null, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-bbcc5e3cb536418e", + "node_id": "draft", + "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": { + "total_input_tokens": 1, + "output_tokens": 1, + "cache_read_tokens": 0, + "cache_creation_tokens": null, + "total_cost_usd": null + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log new file mode 100644 index 0000000..f5748d5 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log @@ -0,0 +1,3 @@ +{"type": "thread.started", "thread_id": "fake-codex-thread"} +{"type": "item.completed", "item": {"type": "agent_message", "text": "codex (fake) fix."}} +{"type": "turn.completed", "usage": {"input_tokens": 1, "output_tokens": 1}} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json new file mode 100644 index 0000000..080cdaa --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-bbcc5e3cb536418e", "node_id": "fix", "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "fd85b6d8dd1d9b0fa92a03fe1a90fbfe61d21bfcceb56c89ebcfca6080ce666c"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json new file mode 100644 index 0000000..4991f19 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json @@ -0,0 +1,39 @@ +{ + "status": "completed", + "agent_id": "kimi-code", + "output": "kimi (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.09, + "files_changed": [ + "calc.py" + ], + "gate": { + "passed": true, + "score": 1.0, + "checks": [ + { + "command": "python3 -c \"from calc import add; assert add(2, 3) == 5\"", + "passed": true, + "exit_code": 0, + "output": "", + "duration_seconds": 0.02 + } + ] + }, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-bbcc5e3cb536418e", + "node_id": "fix", + "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": { + "total_input_tokens": 7, + "output_tokens": 3, + "cache_read_tokens": null, + "cache_creation_tokens": null, + "total_cost_usd": null + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log new file mode 100644 index 0000000..e2f7891 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log @@ -0,0 +1,3 @@ +{"role": "assistant", "content": "kimi (fake) fix."} +{"role": "meta", "type": "session.resume_hint", "session_id": "fake-kimi-session", "content": "To resume this session: kimi -r fake-kimi-session"} +{"role": "meta", "type": "usage", "usage": {"input_tokens": 7, "output_tokens": 3}} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/planner-output.txt b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/planner-output.txt new file mode 100644 index 0000000..b6d2b3c --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/planner-output.txt @@ -0,0 +1 @@ +{"schema_version": 1, "goal": "fix add", "final_node": "fix", "nodes": [{"id": "draft", "agent": "codex", "prompt": "draft the fix"}, {"id": "fix", "agent": "kimi-code", "prompt": "apply the fix", "needs": ["draft"]}]} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/spec.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/spec.json new file mode 100644 index 0000000..f04e3ad --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/wf-bbcc5e3cb536418e/spec.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "goal": "fix add", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix", + "needs": [], + "timeout_seconds": null, + "max_attempts": 1 + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ], + "timeout_seconds": null, + "max_attempts": 1 + } + ], + "final_node": "fix" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/workflows.db b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved-recovered/workflows/workflows.db new file mode 100644 index 0000000000000000000000000000000000000000..02b36eedcee010200b1d0ee21a8e6a3972dbaed3 GIT binary patch literal 24576 zcmeI4PjBQ%6~NueB-yN#2?*^5X%8uxOTu*0{?~3hTnLF5jYu*}rU%VvHL}W8<+Nwq zw#Rno52FYTpyk8`IKzDp95{1bi4VYD(0&2L7vO~UmD_fAlFp?sO&KY%KNm&#)c+^D)PxkgE=N=^V7P+$=GEXBmWl8L&aXeKUvUrdZ=K4W&#L^@U zqNz$HA&w%Jy7-7qS0GCmabIL{G))KkI0}50}c*5lAk2T`S|E6Qo2kl~C~_nRnZ zxL>rTx)Yy3hc2Y_7*2B!6k4XH>^R5*=)%K;uJcpp5LMbmyUxekPxiV+j!N;39u>bfNz)I|kpYhqm%^?t;up0p(4!2J&hP85_*d_LJ_ThX+u0-#O?uYh_f% zInw>1^RaX29PBuc>rB~NT}?pOxh{8|JqHHao$bdv+q+J4OraMC#i9ppewJJw)}ok> zTs6UGVT|d7!C+mTnO|{M8&Y9@B|tpL_xC?~v-)Of<+qRi{DHJDwe}l}PZk=D#(O9JhUXE|W;*fOx@o8u z)Bbo;lxUSD{<2UIXe>(CmQXZvLsd5vyQ`^NdS^?qHx*4cRr7(OY$=L%`M#=bwoSXO z8^il4Bk?gy&)gK&1VPGZ3)bAnu&{3(nSHLHG9Wl zzef}}j-pn#mG_gV_u`cmzB+F46~Cn#Cb5}f8W>v)dmY`fJ)bepHyI%vYWu2&+my8l zrr5GnixSWGEo@`WvOMM!m~&5G%NgmhK6asve5osMsZIGP#-Y3=`@t!~l*&z+PtTdW z_33Llpm2kx*v}!k2nOJ~kObFB8uydD%7F47Y_|ueygXx+mw(=r^PtaQSqe)e5=T5S z8iZj}?&DKf2ImaQLz;TtEUOxpuPS;O^n(pv+jwO-Ny4)Um6gq$zD~$Fj4OC|hy9n& zuL44P6Ujw*ll8FHHsviO+ntVP8&s#v$A+(~X2)kfB^uU=rMAIJ+SIgkZi=?&S%#(S zCiUC4Y7Z+mhKzG!=7#WR3j>FW6x6*2HvQdZH7BKQNUyjRnt3TLTi64%x#c?l3@rTaY~Cm z}$=Xw7Q7~0sj0AnZuNlZZw8(*aphZ zm!@%p7JVFM?40ZI1{Z}%r(>I)c4y>o7hSZhc5F+5oAJ1-Yvts{<(T?NPBm7@bdd5r zv^vZ~uvvi^#Q78j5%O^mGWsLL9ktI8J59oX1bKMIZIAuS?b0PB%0Qt8EV}osOp&rf*m_)Ljp*nr{!cq<0HN*Q(oX+b2* z;{^N_^YN^1_WxnhfJrn@^Dfd&1-7q>nK!{H zXpq6AkBl;@RFZd=^O+y1iea@oCRa(4GCvHCdSeykDJC#umgCS61r0K8PE?-KyiLu} zwUJp~HShE1Q2BU&p~B00ck3?mJLe1A|N8qMzDWP~!`Ydc_hF^iSpK&JA8`l*f`A|( z2nYg#fFK|U2m*qDARq_`0)oI@C-C8{zBpm0XBhu~B`tq-*E=ZMF9-+%f`A|(2nYg# zfFK|U2m*qDARq{QI|=+?c7JjFS(ixHFK#cPIS6_ewlCd@;|o?BPS^)w=4CbkT!e^ z#WJoCtb00hOKGNlRod{UMksJ=+*X=azu8jvw3b%9|G)en2|nTw1Ox#=KoAfF1OY)n g5D)|e0YN|z5CjB)yHDVIjoAl{Y6qbFDa3`Z0SG^+g#Z8m literal 0 HcmV?d00001 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/.locks/wf-bbcc5e3cb536418e.lock b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/.locks/wf-bbcc5e3cb536418e.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json new file mode 100644 index 0000000..540a358 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-bbcc5e3cb536418e", "node_id": "draft", "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "2a9b347efb9398f3b583e83f4cbab96fd2c1d5abb00edcb265ea73c04f341237"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json new file mode 100644 index 0000000..cdcf72a --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json @@ -0,0 +1,27 @@ +{ + "status": "completed", + "agent_id": "codex", + "output": "codex (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02, + "files_changed": [ + "calc.py" + ], + "gate": null, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-bbcc5e3cb536418e", + "node_id": "draft", + "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": { + "total_input_tokens": 1, + "output_tokens": 1, + "cache_read_tokens": 0, + "cache_creation_tokens": null, + "total_cost_usd": null + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log new file mode 100644 index 0000000..f5748d5 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/draft/stream.log @@ -0,0 +1,3 @@ +{"type": "thread.started", "thread_id": "fake-codex-thread"} +{"type": "item.completed", "item": {"type": "agent_message", "text": "codex (fake) fix."}} +{"type": "turn.completed", "usage": {"input_tokens": 1, "output_tokens": 1}} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json new file mode 100644 index 0000000..080cdaa --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-bbcc5e3cb536418e", "node_id": "fix", "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "fd85b6d8dd1d9b0fa92a03fe1a90fbfe61d21bfcceb56c89ebcfca6080ce666c"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json new file mode 100644 index 0000000..4991f19 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/result.json @@ -0,0 +1,39 @@ +{ + "status": "completed", + "agent_id": "kimi-code", + "output": "kimi (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.09, + "files_changed": [ + "calc.py" + ], + "gate": { + "passed": true, + "score": 1.0, + "checks": [ + { + "command": "python3 -c \"from calc import add; assert add(2, 3) == 5\"", + "passed": true, + "exit_code": 0, + "output": "", + "duration_seconds": 0.02 + } + ] + }, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-bbcc5e3cb536418e", + "node_id": "fix", + "lease_id": "6322ef0c854f4ef4bff247c3ec2667c0", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": { + "total_input_tokens": 7, + "output_tokens": 3, + "cache_read_tokens": null, + "cache_creation_tokens": null, + "total_cost_usd": null + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/6322ef0c854f4ef4bff247c3ec2667c0-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log new file mode 100644 index 0000000..e2f7891 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/nodes/fix/stream.log @@ -0,0 +1,3 @@ +{"role": "assistant", "content": "kimi (fake) fix."} +{"role": "meta", "type": "session.resume_hint", "session_id": "fake-kimi-session", "content": "To resume this session: kimi -r fake-kimi-session"} +{"role": "meta", "type": "usage", "usage": {"input_tokens": 7, "output_tokens": 3}} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/planner-output.txt b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/planner-output.txt new file mode 100644 index 0000000..b6d2b3c --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/planner-output.txt @@ -0,0 +1 @@ +{"schema_version": 1, "goal": "fix add", "final_node": "fix", "nodes": [{"id": "draft", "agent": "codex", "prompt": "draft the fix"}, {"id": "fix", "agent": "kimi-code", "prompt": "apply the fix", "needs": ["draft"]}]} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/spec.json b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/spec.json new file mode 100644 index 0000000..f04e3ad --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/wf-bbcc5e3cb536418e/spec.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "goal": "fix add", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix", + "needs": [], + "timeout_seconds": null, + "max_attempts": 1 + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ], + "timeout_seconds": null, + "max_attempts": 1 + } + ], + "final_node": "fix" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/workflows.db b/tests/fixtures/persisted/workflows/v0.2.0/bundle-saved/workflows/workflows.db new file mode 100644 index 0000000000000000000000000000000000000000..97c9d3ee30f8d9e32e92a338e80f1167dd8967a0 GIT binary patch literal 24576 zcmeI4&2QVt6~HMuw$}^TYEg7uU=JqPi`_^Tzdw9h*X9;MWoPRsK!Qa;3}=QG6N*$w z${!mTMcQI}>ZRzpf50Ak=(*Pg`Ui9`#r^|D|AC&0zM)7-w&hq3wg+Q}kPvb{-pu^o zd-G-{qM!V#6XsM5lC+O#Jg6Sv-`dpuO}BMn`^&A> z)vtubm2ZTVZyH~(x@%vpJ-ewmPv-1UPJk15F9KioYs>2!8?`@d<;aid zX_CGQqU1Ci-_-Yx+}*A#c6Wc?am8`6xcQ+tzt_bBqxVO(kInsS;A$$5-aVB`|hW^Pdi;vu7m*=nxg!+ z!Y>nl(j!6@i6CWQ=5;v!OXy1LZ zx7GNM@E7LPzyI}paO%$%m)0LXtUWLKn!S!-lj&vj^^nGxUU^))G#OpV+>BA5HlgO$ zZ+`M%nTh=E@ul&5^aOlwJhhdNiB?B4E^z1@AcIi=9clVULfFFzk#oz~)n zOk8z<&Z7j88I!@5QeRqkH)=v*ekDLO%#-5TtGbnW%J_Bx;>rgswyeUpw)hVLKK$YY zH~~(86S(ID{v6lEYp?o7nkjzl`SPhp%-%`V?_n9PXqUZ1qts@+y} z6FXEkb%bn+d`q((KcLhPOp38ZoIp{LL8yTdLAI^f1p7f?BL}Ir?b855+&p_DWw=NC z$UA{l? zVLxoK)}|XngF$pYqp`A^^S22lM|%S{+|l@@i|c?yttm>y7A5N;)i9+UQF1Ixb#$T; z8X!H;6w?Z5K(LB5Y%2!XhCxhQW9H=ezO92zm?SV9#lecEtF|VA$P%*nrtJ6*+!fn6 z)&K^w5ry3%D>(rrD(sQ%t44)DqpSc;(CIC2u zS?}FdOjDQTNtJTHFhF^awNi|xU06!n)KrxQGPVsp&}pE98LGO4HHuZ!w6NS#z@W2X zly9?5A8Q|^;GXxMvQ9&`-P|r@j0O`?*k<$)>{`Km#m^?7YX1EOMu7|8c!nSwW} zra_UVVHr$DS5(cK5<2$B!rX=#DA`NFXHrs(Ay0?&qEOTs^moco%2|dwQlEN;X8})?z-O2M){7x)Qi6jyaz08*2H@aT^GJ9M=UJ2XB8Yk{DzeB>9s7*Kr>L1SoFo+ibbwOOAcfd1>Sa=?B>y~T z0bEpM-8L+fsbrASAPSFrQw`-Q!Vvh%>12e0gc&;>CDNRhYO1=XzROZCUO?m1fIX;ndMyEHXAF3qFJ8=Y4Qb)TRk|3nzsF7sRm0iRkduZj8{w4kR1wBamymM($XOw zwR}y+EtY2Z#MCSm@BZ^|fBZWA@6Q(M^$%gKTWkDFfDgYo0ZxDu-~>1UPJk2O1ULas zfD_;ZH~~)Jz7zOpL0g$|yfoVXe=9V;z3&6${pSQY0ZxDu-~>1UPJk2O1ULasfD_;Z zzMlksx^S>^?e_v#e%i@?|G%`>6dHeRkj6)Ae_#6o9{9xxZ~~kFC%_4C0-OLRzzJ{y zoB${Ay(Z9ES{Af}mhi(F|BrlapIN>lXzQb2%vUcuY?p8Gyt(Ks-cg!qY0Ayg9^6qH zyp$xDzviFOw}m@M8@&}fMt>00asa=;g`coqWxYh0|O&z}(Gal(!TJipWjJR4{Oy4K>4Q-m;MJNEQ#v? literal 0 HcmV?d00001 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/.apply.lock b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/.apply.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/manifest.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/manifest.json new file mode 100644 index 0000000..bb99d34 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/manifest.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "run_id": "wf-6c587c6f51e941d8-final", + "project_id": "frozen-workflows-control-completed", + "parent_run_id": null, + "created_at": "2026-11-09T21:37:13.405646+00:00", + "updated_at": "2026-11-09T21:37:13.415935+00:00", + "state": "pending", + "repo_root": "/private/var/folders/4n/mt3km5rs1bn_b0w_d6hvlmhc0000gn/T/tmpnhjq8lqv/repo", + "source_fingerprint": "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef", + "snapshot_commit": "ac84efac4258a31be893091b550013abd2972a80", + "task_id": "d6824576-76d1-42bf-a32b-0bd33b8ffabc", + "prompt": "fix add", + "strategy": "route", + "agents": [ + "kimi-code" + ], + "plan": { + "origin": "workflow", + "workflow_id": "wf-6c587c6f51e941d8", + "node_id": "fix", + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "source_fingerprint": "8146c9e064aa8eab7389bfeebf6ecc7d9f12a5de5cada8818dcbff8a9a288bef", + "bundle_sha256": "aac21f043fb90de2b50bd88997777cfcc18e208047346c641072e5aaeadc7653", + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "control_plan_digest": "sha256:d9f2f0b88948d39b838448957615a461cf8baa5c854ed879fbd8644db6372079", + "approved": { + "workflow_id": "wf-6c587c6f51e941d8", + "run_id": "wf-6c587c6f51e941d8-final", + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "approved_at": "2026-11-09T21:37:13.415929+00:00" + } + }, + "agent_id": "kimi-code", + "files_changed": [ + "calc.py" + ], + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "provider_session_id": null, + "conversation_id": null +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/result.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/result.json new file mode 100644 index 0000000..b429c06 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/result.json @@ -0,0 +1,7 @@ +{ + "status": "completed", + "output": "kimi (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02 +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/runs/wf-6c587c6f51e941d8-final/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/.locks/wf-6c587c6f51e941d8.lock b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/.locks/wf-6c587c6f51e941d8.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/control-plan.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/control-plan.json new file mode 100644 index 0000000..8c8bfe0 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/control-plan.json @@ -0,0 +1 @@ +{"edges":[{"decided_by":"code","from":"control:start","kind":"always","to":"workflow:draft"},{"decided_by":"code","from":"workflow:draft","kind":"success","to":"workflow:fix"},{"decided_by":"code","from":"workflow:draft","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"success","to":"control:validator:final"},{"decided_by":"code","from":"control:validator:final","kind":"success","to":"control:end"},{"decided_by":"code","from":"control:validator:final","kind":"failure","to":"control:end"}],"kind":"toolgraph.control-plan","limits":{"maximum_agent_calls":2,"maximum_cycles":0,"maximum_parallelism":2},"nodes":[{"agent_call":false,"id":"control:start","kind":"start","principals":[]},{"agent_call":true,"id":"workflow:draft","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["codex"],"timeout_seconds":300},{"agent_call":true,"id":"workflow:fix","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["kimi"],"timeout_seconds":300},{"agent_call":false,"id":"control:validator:final","kind":"validator","principals":[],"validator_scope":"workspace_result"},{"agent_call":false,"id":"control:end","kind":"end","principals":[]}],"principals":[{"candidate_tools":["workspace::read","workspace::write"],"id":"codex"},{"candidate_tools":["workspace::read","workspace::write"],"id":"kimi"}],"producer":{"name":"syncmill","version":"0.2.0"},"run_id":"wf-6c587c6f51e941d8","schema_version":1} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-plan.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-plan.json new file mode 100644 index 0000000..8c8bfe0 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-plan.json @@ -0,0 +1 @@ +{"edges":[{"decided_by":"code","from":"control:start","kind":"always","to":"workflow:draft"},{"decided_by":"code","from":"workflow:draft","kind":"success","to":"workflow:fix"},{"decided_by":"code","from":"workflow:draft","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"success","to":"control:validator:final"},{"decided_by":"code","from":"control:validator:final","kind":"success","to":"control:end"},{"decided_by":"code","from":"control:validator:final","kind":"failure","to":"control:end"}],"kind":"toolgraph.control-plan","limits":{"maximum_agent_calls":2,"maximum_cycles":0,"maximum_parallelism":2},"nodes":[{"agent_call":false,"id":"control:start","kind":"start","principals":[]},{"agent_call":true,"id":"workflow:draft","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["codex"],"timeout_seconds":300},{"agent_call":true,"id":"workflow:fix","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["kimi"],"timeout_seconds":300},{"agent_call":false,"id":"control:validator:final","kind":"validator","principals":[],"validator_scope":"workspace_result"},{"agent_call":false,"id":"control:end","kind":"end","principals":[]}],"principals":[{"candidate_tools":["workspace::read","workspace::write"],"id":"codex"},{"candidate_tools":["workspace::read","workspace::write"],"id":"kimi"}],"producer":{"name":"syncmill","version":"0.2.0"},"run_id":"wf-6c587c6f51e941d8","schema_version":1} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-preflight.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-preflight.json new file mode 100644 index 0000000..4f11494 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/executions/wfx-dcb87bb741f14738bbc1d2c5/control-preflight.json @@ -0,0 +1 @@ +{"schema_version": 1, "kind": "toolgraph.control-preflight", "run_id": "wf-6c587c6f51e941d8", "created_at": "2026-11-10T00:00:00+00:00", "mode": "advisory", "profile": "review", "plan_digest": "sha256:d9f2f0b88948d39b838448957615a461cf8baa5c854ed879fbd8644db6372079", "graph_state": {"instance_id": "frozen-graph", "generation": 7}, "decision": "advisory_allow", "evaluations": [{"principal": "codex", "agent_found": true, "eligible": ["workspace::read", "workspace::write"], "rejected": []}, {"principal": "kimi", "agent_found": true, "eligible": ["workspace::read", "workspace::write"], "rejected": []}], "findings": []} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/commit.json new file mode 100644 index 0000000..d59a8be --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-6c587c6f51e941d8", "node_id": "draft", "lease_id": "1d6e1263a5e64142a863168c44be5e39", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "ab4d22248a7da10b83f65a86efc4cd504fbfd32a870c9a1fa2271c61982455a8"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/result.json new file mode 100644 index 0000000..b56824f --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/result.json @@ -0,0 +1,27 @@ +{ + "status": "completed", + "agent_id": "codex", + "output": "codex (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02, + "files_changed": [ + "calc.py" + ], + "gate": null, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-6c587c6f51e941d8", + "node_id": "draft", + "lease_id": "1d6e1263a5e64142a863168c44be5e39", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": { + "total_input_tokens": 1, + "output_tokens": 1, + "cache_read_tokens": 0, + "cache_creation_tokens": null, + "total_cost_usd": null + } +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/1d6e1263a5e64142a863168c44be5e39-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/stream.log new file mode 100644 index 0000000..f5748d5 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/draft/stream.log @@ -0,0 +1,3 @@ +{"type": "thread.started", "thread_id": "fake-codex-thread"} +{"type": "item.completed", "item": {"type": "agent_message", "text": "codex (fake) fix."}} +{"type": "turn.completed", "usage": {"input_tokens": 1, "output_tokens": 1}} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/commit.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/commit.json new file mode 100644 index 0000000..c5633f4 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/commit.json @@ -0,0 +1 @@ +{"schema_version": 3, "workflow_id": "wf-6c587c6f51e941d8", "node_id": "fix", "lease_id": "1d6e1263a5e64142a863168c44be5e39", "attempt": 1, "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", "bundle_sha256": "aac21f043fb90de2b50bd88997777cfcc18e208047346c641072e5aaeadc7653"} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/result.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/result.json new file mode 100644 index 0000000..def5833 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/result.json @@ -0,0 +1,33 @@ +{ + "status": "completed", + "agent_id": "kimi-code", + "output": "kimi (fake) fix.", + "error": null, + "exit_code": 0, + "duration_seconds": 0.02, + "files_changed": [ + "calc.py" + ], + "gate": { + "passed": true, + "score": 1.0, + "checks": [ + { + "command": "python3 -c \"from calc import add; assert add(2, 3) == 5\"", + "passed": true, + "exit_code": 0, + "output": "", + "duration_seconds": 0.02 + } + ] + }, + "patch_sha256": "35dc711418e60edc7974186bd853c7ab08db843f16e43066417b07a53d8566ba", + "patch_bytes": 157, + "bundle_version": 3, + "workflow_id": "wf-6c587c6f51e941d8", + "node_id": "fix", + "lease_id": "1d6e1263a5e64142a863168c44be5e39", + "attempt": 1, + "spec_sha256": "977294d3defa4f3167fefdc2a3c8150025d683c5e39bb848336df5919c134283", + "usage": null +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/winner.patch b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/winner.patch new file mode 100644 index 0000000..95e886e --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/1d6e1263a5e64142a863168c44be5e39-1/winner.patch @@ -0,0 +1,8 @@ +diff --git a/calc.py b/calc.py +index 12ee743..4693ad3 100644 +--- a/calc.py ++++ b/calc.py +@@ -1,2 +1,2 @@ + def add(a, b): +- return a - b ++ return a + b diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/stream.log b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/stream.log new file mode 100644 index 0000000..9ee3cc1 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/nodes/fix/stream.log @@ -0,0 +1,2 @@ +{"role": "assistant", "content": "kimi (fake) fix."} +{"role": "meta", "type": "session.resume_hint", "session_id": "fake-kimi-session", "content": "To resume this session: kimi -r fake-kimi-session"} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/planner-output.txt b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/planner-output.txt new file mode 100644 index 0000000..b6d2b3c --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/planner-output.txt @@ -0,0 +1 @@ +{"schema_version": 1, "goal": "fix add", "final_node": "fix", "nodes": [{"id": "draft", "agent": "codex", "prompt": "draft the fix"}, {"id": "fix", "agent": "kimi-code", "prompt": "apply the fix", "needs": ["draft"]}]} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/spec.json b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/spec.json new file mode 100644 index 0000000..f04e3ad --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/wf-6c587c6f51e941d8/spec.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "goal": "fix add", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix", + "needs": [], + "timeout_seconds": null, + "max_attempts": 1 + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ], + "timeout_seconds": null, + "max_attempts": 1 + } + ], + "final_node": "fix" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/workflows.db b/tests/fixtures/persisted/workflows/v0.2.0/control-completed/workflows/workflows.db new file mode 100644 index 0000000000000000000000000000000000000000..504fc461a30d071b26aa7e964d31c3873bb0ab07 GIT binary patch literal 24576 zcmeHP&2QVt6_@NJju&v;qQJU94<^_iVnnt${2cnwt($0zptiGh6d=JOAciwTiwQ+4 zB;}7aj3RBZMGx(v*Zv24*h9~~w&*{wmtxU-(SM-FzM)7;wv|Lm19XEOLPE&-c=O)x zeGa4X!(Z%%Iah)t?UP(F8sBLwEHr+iD2+yA0lqijTV1PgbFF%Sf14Bcuex1mY<<45 zvhqda`tnze<*!!%y3)Dz#jVG$8ZO%<0uljGtC-AB#wFNkwZS)glxd6_bf}|Sp?{lI1G4}&pZLRCy|#9<4RLkl+#ioPBLEg zx0IZde!)xClN^IU7g~Cpq`40YEn8E56lNiaaDTts`B~>cskE!y>wLWPXt%3q6)}K? zrzC&V;Ae?H=7@2^qA=#dc)&_`TM8vn&=(f!2UqJ~;I!*Ke%dxzl`7@S44dV3X-({Kb&RlwW$haUmF8>bxD3 zK5xOyjbDBL+L9po>%$B3d;AD|uMTbHfMl2_#j{tjm3gSXU4*=HQ{;LDAV#IWXaW2xC}-aHOAZE9hM+JS|*Ya)hq zO}94@+SJ^xjPqXxg@>UF|ovOdZ*_KI*4Qe-LpfAli_Lk09N*k4V}M zl8Es%YnySqpBvBnR+=F{_I&NwV|MQ->i4Jy*J0f5w)6fV?mc^fqZdbQQ3qgT+SKKm zZ4!bx@f`!Ze!#gO*ql;_xdGCN#kfTYBN!vhs2>EFxJ1X;=K%#zJULS{+T(rV9RVAV z)6{L$QV$ansoQE0o+yMdwWSKD&D8Br&(x4X2TMtiLvv9gz;i(fo(CxinO6gpyazAg z!V@u{b0+3LZK-+K=a8%*aMC0ejqxywT56x1Kyu7E%!f8~-YSoIs2M0a#U>yaYyp2=T`wzjFzGnMNifvx@D`|it0L!?wZVCJRoLZAlnIe zz^G0Pijf7@VKEyU)Z&Kg``E;WVY9$;kxP+b>ex_$WC^lzRx-W~cST_+7O2ffj2r`{ zAn_8P=+Qhp5|vqDe2^9e_iyG{5cY>Li=YOm z&4BnO({?x`U{VT&$ZG44fcZ`(t5l z!wi({rQkCuD`d#iAwMq^bprg(YbXTv4F3?BU71? z(*My=2-Q)bnMUBd8soZeX+Fc)g*f6kG@uk=u4`B`9Rq?qIF07$+#&?_5!A7*B3_i% zH;bVNOL<%B{oJG>`V$P7m>Cw z{ldm4H$%{%L+6EUPuX^Hg9DA)mNs&g@lFz2)hS7Svxxt~3n5^J0fYvxj=%#RGuTOs zPJY7aQ0xF9RVk1Zs}i&f9#aTkm{kuu*@Tpg9p86M6d=$w_I-+&POX9k2uva&D=O@M zMT1~vD3-jK4YE8LKxiN$bYAHbd-HL8dKYO5(srIEktg00cq}}G&AeE4w8%D89eMzP z54$F2knFI5O%uD8V76oZ0hP8$I2PqGt@USPi9mUHLmdSIm zX1_@Jpmzo@0+RGpv_Ux&gb`E%paFP4z>h`aVM+!)Z~PJfdY!ttG8QOaVz`HWVX5mN zO+MrCW-+;-QolqH^96gyB#ptmM(+fmQOL-TNHi>7HPp?*=z%Xbr0|M{aaz2!5FA#F zdw@t%a9WwL=q;7DS%Xk#+AA8gv8B0&V{+<3W-_?t5^8EhhpLe~F4Z7^8J6uErUm5; z4UE8GmSa(0qoP#SO(EW-jll9}wL4wl1q|rbZ2ZrLbS_ZMQ$mFTCl$`ZOuRv4su%!T zdTnV0QBk*DbH36D>VAYLrc8#o^QYese1!1tI64SG>)7bMjT@Ul;Wci2J|(n{V)pXR zKmPK^zkRg)yTyft55!Jx^A6Gm(-@iF0JvjDcTch(q)9CFk z33{6|^Jh33st)1S^x#B|IBjuuzI$UnYPxGfXjUUTtIvwCw<{wc)e{{o#((b50_ literal 0 HcmV?d00001 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/.locks/wf-6494f914db7441a1.lock b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/.locks/wf-6494f914db7441a1.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/control-plan.json b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/control-plan.json new file mode 100644 index 0000000..0456f87 --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/control-plan.json @@ -0,0 +1 @@ +{"edges":[{"decided_by":"code","from":"control:start","kind":"always","to":"workflow:draft"},{"decided_by":"code","from":"workflow:draft","kind":"success","to":"workflow:fix"},{"decided_by":"code","from":"workflow:draft","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"success","to":"control:validator:final"},{"decided_by":"code","from":"control:validator:final","kind":"success","to":"control:end"},{"decided_by":"code","from":"control:validator:final","kind":"failure","to":"control:end"}],"kind":"toolgraph.control-plan","limits":{"maximum_agent_calls":2,"maximum_cycles":0,"maximum_parallelism":2},"nodes":[{"agent_call":false,"id":"control:start","kind":"start","principals":[]},{"agent_call":true,"id":"workflow:draft","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["codex"],"timeout_seconds":300},{"agent_call":true,"id":"workflow:fix","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["kimi"],"timeout_seconds":300},{"agent_call":false,"id":"control:validator:final","kind":"validator","principals":[],"validator_scope":"workspace_result"},{"agent_call":false,"id":"control:end","kind":"end","principals":[]}],"principals":[{"candidate_tools":["workspace::read","workspace::write"],"id":"codex"},{"candidate_tools":["workspace::read","workspace::write"],"id":"kimi"}],"producer":{"name":"syncmill","version":"0.2.0"},"run_id":"wf-6494f914db7441a1","schema_version":1} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/planner-output.txt b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/planner-output.txt new file mode 100644 index 0000000..b6d2b3c --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/planner-output.txt @@ -0,0 +1 @@ +{"schema_version": 1, "goal": "fix add", "final_node": "fix", "nodes": [{"id": "draft", "agent": "codex", "prompt": "draft the fix"}, {"id": "fix", "agent": "kimi-code", "prompt": "apply the fix", "needs": ["draft"]}]} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/spec.json b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/spec.json new file mode 100644 index 0000000..f04e3ad --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/wf-6494f914db7441a1/spec.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "goal": "fix add", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix", + "needs": [], + "timeout_seconds": null, + "max_attempts": 1 + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ], + "timeout_seconds": null, + "max_attempts": 1 + } + ], + "final_node": "fix" +} diff --git a/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/workflows.db b/tests/fixtures/persisted/workflows/v0.2.0/control-planned/workflows/workflows.db new file mode 100644 index 0000000000000000000000000000000000000000..90b02a7b98cf41d204438bf24fdb77fd495aa656 GIT binary patch literal 24576 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 C+MYuI literal 0 HcmV?d00001 diff --git a/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/.locks/wf-68b63dd2ed314937.lock b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/.locks/wf-68b63dd2ed314937.lock new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/control-plan.json b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/control-plan.json new file mode 100644 index 0000000..bd9158d --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/control-plan.json @@ -0,0 +1 @@ +{"edges":[{"decided_by":"code","from":"control:start","kind":"always","to":"workflow:draft"},{"decided_by":"code","from":"workflow:draft","kind":"success","to":"workflow:fix"},{"decided_by":"code","from":"workflow:draft","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"failure","to":"control:end"},{"decided_by":"code","from":"workflow:fix","kind":"success","to":"control:validator:final"},{"decided_by":"code","from":"control:validator:final","kind":"success","to":"control:end"},{"decided_by":"code","from":"control:validator:final","kind":"failure","to":"control:end"}],"kind":"toolgraph.control-plan","limits":{"maximum_agent_calls":2,"maximum_cycles":0,"maximum_parallelism":2},"nodes":[{"agent_call":false,"id":"control:start","kind":"start","principals":[]},{"agent_call":true,"id":"workflow:draft","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["codex"],"timeout_seconds":300},{"agent_call":true,"id":"workflow:fix","kind":"agent","max_invocations":1,"max_parallelism":1,"principals":["kimi"],"timeout_seconds":300},{"agent_call":false,"id":"control:validator:final","kind":"validator","principals":[],"validator_scope":"workspace_result"},{"agent_call":false,"id":"control:end","kind":"end","principals":[]}],"principals":[{"candidate_tools":["workspace::read","workspace::write"],"id":"codex"},{"candidate_tools":["workspace::read","workspace::write"],"id":"kimi"}],"producer":{"name":"syncmill","version":"0.3.0"},"run_id":"wf-68b63dd2ed314937","schema_version":1} diff --git a/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/planner-output.txt b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/planner-output.txt new file mode 100644 index 0000000..b6d2b3c --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/planner-output.txt @@ -0,0 +1 @@ +{"schema_version": 1, "goal": "fix add", "final_node": "fix", "nodes": [{"id": "draft", "agent": "codex", "prompt": "draft the fix"}, {"id": "fix", "agent": "kimi-code", "prompt": "apply the fix", "needs": ["draft"]}]} \ No newline at end of file diff --git a/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/spec.json b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/spec.json new file mode 100644 index 0000000..f04e3ad --- /dev/null +++ b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/wf-68b63dd2ed314937/spec.json @@ -0,0 +1,25 @@ +{ + "schema_version": 1, + "goal": "fix add", + "nodes": [ + { + "id": "draft", + "agent": "codex", + "prompt": "draft the fix", + "needs": [], + "timeout_seconds": null, + "max_attempts": 1 + }, + { + "id": "fix", + "agent": "kimi-code", + "prompt": "apply the fix", + "needs": [ + "draft" + ], + "timeout_seconds": null, + "max_attempts": 1 + } + ], + "final_node": "fix" +} diff --git a/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/workflows.db b/tests/fixtures/persisted/workflows/v0.3.0/control-planned/workflows/workflows.db new file mode 100644 index 0000000000000000000000000000000000000000..92f226b93429abc022d275ffb05b532e60ca08e8 GIT binary patch literal 36864 zcmeI4PjA~s7QiVdvE^VL_Y^I*2M@T1+K3#HA}LY2?5Z|f1ofZPk%0t7K@2%Viw#A} zBxO4YjKb|=cTc;A-unUiJ^C&B0lLS%?Q!2wr2g4SRA3i{(>IV|a{j$HzxU=*^JMGU z=X-w2i5JCvni5U=Kw4Uo{!EA@NlWmv3O_}82p4yY8~9zCx_;B;lJxkmTWf1SNab=# zT79(sEg-#=J`3M@zf2_OL^fCP}huS4MK59O80pEgQYt{*afaUR7ly&yVI z?7$B>J>m8lj}t!%#|^7HM^>k6k>i6WpC4P~$-%DmoQzxNhmpgBN#o|uVQ0^JwqtD; zll!bXZnk~4RV9OfhK0+hA)N@ZiC^v}x~WyVS* zOE(G!q4&uRWvsi;uB&q62sh?5<;;dRYMJhZX4M;e4+rcQ^iERfyvVdMA4GN>MGNVW zM8nwSw&#Z@JRZb;xX>u!0wIrW@V>Q)j%(rbTC=Ad3AHf6lPuP9Np2@)4lP!&Pph*+BAU#Z~A?WAHP3em{<^ zXvf@*V#e8%QD3qN z^NsB}^#mZD;0~ot+9du`&OxZ4-c{!pn51C|hECwEA!j`+%o&^ue zozAnJ&aPFRGU#mh&a|@ASA+Qg97b#sR0i}_5K(qR*xFK;mMhlAyX=-s?3LUB6J>T< zgNsu6-x9pAAOR$R1dsp{KmthM{Uq>@-<62AzyIOY#?@NoFNfc4WXmkWo1rq454BZk z_1xQTm`+1utj?LH>MgBVSL%&zRozxv-MZS=%(l{ethWrsFdr#OTT$NCK02qc8k~X8 z;GkVQqjAlP0tUI2njY5rsrIsO#EI&JwxgWetkF9Q`aM^HauU|MwX{DtJ=fFIAUUmx z6S%4B4Y$RWhEA!;sngWVmg8~mcn$8lP1f?%IyD$KT*|0vswQ(C&ogO@)=kslo(u0R zFJ8-u+v9y&q&(!dT9r>C8pv(g^DhWxOs>k}O=u#wzj!VC3>sKWy%egm!#Uh%Gr|2J zj{1YN=s?mQe5mAKi1C~=G5*V{ocetZ$3#1UGgT-m!(k9qZy2m$YOcYxmgAVZsc8-787;Nts+wLmH5sPNA(Pj+ z;%&IfQZhoofQJk+D5Bp4lSl^?Mj(Y1h|J?SieXSj7F10k%aEmvCUvk*Kk3a?=BM`o zO?#7`aOd{y**eb;cw)Of3Mn1%2Lg5lJ>1kk6S;-Vh>&%d%;5|}7I1rvD_1P9&XbFi z?!!dn9E(FY)O$2Y_$9~?-tbGYO!>@vi$<_n*vwODpb7IipAm_n1Tr(3c_BX{XEw>$ zPbY(*LNv~#ee$~51P@dykCl-LCz&*;&0PUJ)c3%H?84#mdVay(p~!MUP#C!xM2$3# z0$W5=sCz9JL*WLtK-Rb|8Xt??p4czUKY>)AXdXFWyY8^Mrc%}AMw6LLrA}Qp>rCU^ zXt|nU8oFLLv=-xf3pR7J=`o|h4eq#Jquw%|x|sBTz5fN8@nRvB()UFi6tSR4+KmGy z(He@__#$LN-U=+mAzBkXugZ<#yh=sLCaR*FMze8wnWxFlgYBl dict[str, list[str]]: - return {**SCENARIOS, **(SCENARIOS_V030 if release != "v0.2.0" else {})} + return {**SCENARIOS, **(SCENARIOS_V030 if release != "v0.2.0" else {}), **SCENARIOS_2B} + +def _binding_files(release: str) -> dict[str, dict]: + """Every bindings document of a release: one per generator part.""" + return { + path.name: json.loads(path.read_text()) + for path in sorted((FROZEN / release).glob("bindings*.json")) + } -def _bindings(release: str) -> dict: - return json.loads((FROZEN / release / "bindings.json").read_text()) + +def _bindings(release: str, scenario: str) -> dict: + """The bindings document that holds ``scenario``: its machine values + (box root, host) are that generator run's own.""" + [document] = [ + doc for doc in _binding_files(release).values() if scenario in doc["scenarios"] + ] + return document def _cells(conn: sqlite3.Connection) -> dict[tuple[str, str], list]: @@ -95,7 +120,7 @@ class Frozen: def __init__(self, release: str, scenario: str, tmp_path: Path, monkeypatch): self.release = release self.scenario = scenario - self.bindings = _bindings(release) + self.bindings = _bindings(release, scenario) self.facts = self.bindings["scenarios"][scenario] self.dir = FROZEN / release / scenario self.root = tmp_path.resolve() @@ -164,6 +189,9 @@ def _install(self) -> None: # its plan, and the repository root once. Exact counts. for manifest in self.project_dir.glob("runs/*/manifest.json"): self._replace_bytes(manifest, fingerprints=2, paths=1) + # An apply receipt names the repository once and no fingerprint. + for receipt in self.project_dir.glob("runs/*/applications/*.json"): + self._replace_bytes(receipt, fingerprints=0, paths=1) def _replace_bytes(self, path: Path, *, fingerprints: int, paths: int) -> None: data = path.read_bytes() @@ -207,10 +235,17 @@ def test_every_release_freezes_every_scenario(): # A release directory that lost a scenario would silently shrink coverage. assert RELEASES == ["v0.2.0", "v0.3.0"] for release in RELEASES: - bindings = _bindings(release) - assert bindings["release"] == release.removeprefix("v") - assert sorted(bindings["scenarios"]) == sorted(scenarios(release)) - for name, facts in bindings["scenarios"].items(): + files = _binding_files(release) + assert sorted(files) == ["bindings-2b.json", "bindings.json"] + named = [name for doc in files.values() for name in doc["scenarios"]] + assert len(named) == len(set(named)), release + assert sorted(named) == sorted(scenarios(release)) + assert sorted(files["bindings-2b.json"]["scenarios"]) == sorted(SCENARIOS_2B) + for bindings in files.values(): + assert bindings["release"] == release.removeprefix("v") + for name, facts in ( + item for doc in files.values() for item in doc["scenarios"].items() + ): assert list(facts["states"].values()) == scenarios(release)[name], name for stored in facts["source_fingerprints"]: assert fingerprint_version(stored) == 1 @@ -235,7 +270,7 @@ def _each(release: str) -> list[str]: return list(scenarios(release)) -@pytest.mark.parametrize("scenario", list(SCENARIOS) + list(SCENARIOS_V030)) +@pytest.mark.parametrize("scenario", ALL_SCENARIOS) def test_install_rebinds_only_machine_bound_values(release, scenario, tmp_path, monkeypatch): if scenario not in scenarios(release): pytest.skip(f"{release} has no {scenario}") @@ -290,7 +325,7 @@ def _fresh_heartbeat(frozen: "Frozen"): #: Scenarios whose released lease owner is dead; HEAD's show/status recover it. -DEAD_OWNER = {"running", "unapproved-running"} +DEAD_OWNER = {"running", "unapproved-running", "bundle-saved"} #: The final state each node of the frozen spec was left in, per scenario. NODES = { "planned": ("pending", "pending"), @@ -305,13 +340,19 @@ def _fresh_heartbeat(frozen: "Frozen"): "running": ("completed", "running"), "unapproved-running": ("completed", "completed"), "recovery-required": ("completed", "interrupted"), + "bundle-saved": ("completed", "running"), + "bundle-saved-recovered": ("completed", "interrupted"), + "control-planned": ("pending", "pending"), + "control-completed": ("completed", "completed"), + "apply-unreceipted": ("completed", "completed"), + "apply-receipted": ("completed", "completed"), } # ── reading every released workflow ─────────────────────────────────────── -@pytest.mark.parametrize("scenario", list(SCENARIOS) + list(SCENARIOS_V030)) +@pytest.mark.parametrize("scenario", ALL_SCENARIOS) def test_list_and_show_read_a_released_workflow(release, scenario, tmp_path, monkeypatch): if scenario not in scenarios(release): pytest.skip(f"{release} has no {scenario}") @@ -825,6 +866,7 @@ def __init__(self, agent_id: str, output: str, *, fix: bool = False): self.output = output self.fix = fix self.calls: list[str] = [] + self.timeouts: list[int] = [] def execute(self, prompt, workdir, timeout): raise AssertionError("a released plan needs no planner call") @@ -835,6 +877,7 @@ async def aexecute(self, prompt, workdir, timeout): from syncmill.models import TaskResult, TaskStatus self.calls.append(prompt) + self.timeouts.append(timeout) if self.fix: (Path(workdir) / "calc.py").write_text(FIXED, encoding="utf-8") return TaskResult( @@ -843,20 +886,23 @@ async def aexecute(self, prompt, workdir, timeout): ) -def _workflow_env(frozen: Frozen, monkeypatch) -> None: - """The configuration every released scenario was written under.""" +def _workflow_env(frozen: Frozen, monkeypatch, *, gate: bool | None = None) -> None: + """The configuration every released scenario was written under (``gate`` + overrides whether the gate is configured).""" monkeypatch.setenv("SYNCMILL_AGENT_PRIORITY", json.dumps(["codex", "kimi-code"])) monkeypatch.setenv("SYNCMILL_CLAUDE__ENABLED", "false") - if frozen.scenario != "no-changes": + if gate if gate is not None else frozen.scenario != "no-changes": monkeypatch.setenv("SYNCMILL_GATE__COMMANDS", json.dumps([GATE_COMMAND])) + else: + monkeypatch.setenv("SYNCMILL_GATE__COMMANDS", "[]") -def _coordinator(frozen: Frozen, monkeypatch): +def _coordinator(frozen: Frozen, monkeypatch, *, gate: bool | None = None): from conftest import FakeRegistry from syncmill.project import load_project_config from syncmill.workflow import WorkflowCoordinator - _workflow_env(frozen, monkeypatch) + _workflow_env(frozen, monkeypatch, gate=gate) _, config = load_project_config(frozen.repo) agents = { "codex": NodeAgent("codex", "HEAD draft."), @@ -1445,3 +1491,331 @@ def no_toolgraph(*args, **kwargs): assert frozen.row() == before assert agents["codex"].calls == agents["kimi-code"].calls == [] assert toolgraph_calls == [] + + +# ── part 2b: a bundle saved just before the crash ───────────────────────── + + +@pytest.mark.parametrize("scenario", ["bundle-saved", "bundle-saved-recovered"]) +def test_resume_reclaims_a_released_bundle_saved_before_the_crash( + release, scenario, tmp_path, monkeypatch, capsys +): + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + coordinator, agents = _coordinator(frozen, monkeypatch) + released = _before_deadline(frozen, monkeypatch) + fix = released["nodes"]["fix"] + # The crash came after the bundle was saved and before the manifest + # recorded the node's outcome. + assert fix["state"] == ("running" if scenario == "bundle-saved" else "interrupted") + assert (fix["bundle_sha256"], fix["usage_recorded"], fix["attempts"]) == (None, False, 1) + bundle = frozen.project_dir / "workflows" / frozen.workflow_id / Path(fix["output_path"]).parent + stored = json.loads((bundle / "result.json").read_bytes()) + assert (stored["status"], stored["gate"]["passed"]) == ("completed", True) + assert (stored["usage"]["total_input_tokens"], stored["usage"]["output_tokens"]) == (7, 3) + patch = (bundle / "winner.patch").read_bytes() + + done = coordinator.run(frozen.workflow_id, resume=True) + + assert done.state == "completed" + # Reclaimed, not re-run: no provider call, and the call the release + # reserved before dispatch stays counted once. + assert agents["codex"].calls == agents["kimi-code"].calls == [] + assert done.manifest["calls_used"] == released["calls_used"] == 3 + node = done.manifest["nodes"]["fix"] + assert (node["state"], node["attempts"], node["output_path"]) == ( + "completed", 1, fix["output_path"] + ) + # The bundle's usage joins the totals exactly once. + assert (done.manifest["usage"]["input_tokens"], done.manifest["usage"]["output_tokens"]) == ( + released["usage"]["input_tokens"] + 7, released["usage"]["output_tokens"] + 3 + ) + _approved_final(coordinator, done, "kimi-code") + assert coordinator.artifacts.patch(done.final_run_id) == patch + error, lines = _render(done, frozen, capsys, released) + assert error == "" and f"[syncmill] final run: {done.final_run_id}" in lines + + +def _damage_bundle_patch(bundle: Path) -> None: + (bundle / "winner.patch").write_bytes(b"not the saved patch\n") + + +def _damage_bundle_patch_recommitted(bundle: Path) -> None: + """A replaced patch whose commit record was recomputed to match: only + the patch digest in ``result.json`` still tells the difference.""" + from syncmill.workflow import bundle_digest + + patch = b"not the saved patch\n" + (bundle / "winner.patch").write_bytes(patch) + commit = json.loads((bundle / "commit.json").read_text()) + commit["bundle_sha256"] = bundle_digest((bundle / "result.json").read_bytes(), patch) + (bundle / "commit.json").write_text(json.dumps(commit), encoding="utf-8") + + +def _damage_bundle_commit(bundle: Path) -> None: + commit = json.loads((bundle / "commit.json").read_text()) + commit["bundle_sha256"] = "0" * 64 + (bundle / "commit.json").write_text(json.dumps(commit), encoding="utf-8") + + +@pytest.mark.parametrize( + "damage", + [_damage_bundle_patch, _damage_bundle_patch_recommitted, _damage_bundle_commit], + ids=["patch", "patch-recommitted", "commit"], +) +def test_resume_reruns_a_released_bundle_that_does_not_verify( + release, damage, tmp_path, monkeypatch +): + frozen = Frozen(release, "bundle-saved", tmp_path, monkeypatch) + coordinator, agents = _coordinator(frozen, monkeypatch) + released = _before_deadline(frozen, monkeypatch) + fix = released["nodes"]["fix"] + damage(frozen.project_dir / "workflows" / frozen.workflow_id / Path(fix["output_path"]).parent) + + done = coordinator.run(frozen.workflow_id, resume=True) + + assert done.state == "completed" + # Not reclaimed: the node runs again. Its attempt is refunded, the call + # it already spent is not. + assert agents["codex"].calls == [] + assert len(agents["kimi-code"].calls) == 1 + assert done.manifest["nodes"]["fix"]["attempts"] == 1 + assert done.manifest["calls_used"] == released["calls_used"] + 1 + # The damaged bundle's usage is never counted. + assert done.manifest["usage"]["input_tokens"] == released["usage"]["input_tokens"] + _approved_final(coordinator, done, "kimi-code") + assert "+ return a + b" in coordinator.artifacts.patch(done.final_run_id).decode() + + +# ── part 2b: control-plan workflows ─────────────────────────────────────── + +#: The control-plan configuration the 2b scenarios were written under. +TOOLGRAPH_TOOLS = ["workspace::read", "workspace::write"] +TOOLGRAPH_AGENTS = {"codex": "codex", "kimi-code": "kimi"} + + +def _toolgraph(monkeypatch, *, enabled=True, control=True, tools=None, agents=None) -> list: + """Configure Toolgraph and fake it through the preflight seam, answering + each protocol from the request it gets. Returns the recorded argv lists + (with the control plan's bytes for a control preflight).""" + from test_workflow import _fake_control_run + + if enabled: + monkeypatch.setenv("SYNCMILL_INTEGRATION__TOOLGRAPH__ENABLED", "true") + monkeypatch.setenv( + "SYNCMILL_INTEGRATION__TOOLGRAPH__CONTROL_PLAN_ENABLED", "true" if control else "false" + ) + monkeypatch.setenv( + "SYNCMILL_INTEGRATION__TOOLGRAPH__CANDIDATE_TOOLS", json.dumps(tools or TOOLGRAPH_TOOLS) + ) + monkeypatch.setenv( + "SYNCMILL_INTEGRATION__TOOLGRAPH__AGENT_MAP", json.dumps(agents or TOOLGRAPH_AGENTS) + ) + seen: list = [] + answer = _fake_control_run() + + def seam(argv, *args, **kwargs): + if argv[:2] == ["toolgraph", "control-preflight"]: + seen.append(("control", Path(argv[2]).read_bytes())) + elif argv[:1] == ["toolgraph"]: + seen.append(("legacy", argv[2])) + return answer(argv, *args, **kwargs) + + monkeypatch.setattr("syncmill.preflight._sync_run", seam) + return seen + + +def _sha256(data: bytes) -> str: + import hashlib + + return "sha256:" + hashlib.sha256(data).hexdigest() + + +@pytest.mark.parametrize("tight", [False, True], ids=["released-limits", "tighter-config"]) +def test_run_a_released_control_planned_workflow(release, tight, tmp_path, monkeypatch): + seen = _toolgraph(monkeypatch) + if tight: + monkeypatch.setenv("SYNCMILL_WORKFLOW__DEADLINE_SECONDS", "1800") + monkeypatch.setenv("SYNCMILL_WORKFLOW__MAX_PARALLEL", "1") + monkeypatch.setenv("SYNCMILL_KIMI_CODE__TIMEOUT_SECONDS", "120") + else: + # Wider than the plan's stored node timeout: the stored one holds. + monkeypatch.setenv("SYNCMILL_KIMI_CODE__TIMEOUT_SECONDS", "900") + frozen = Frozen(release, "control-planned", tmp_path, monkeypatch) + coordinator, agents = _coordinator(frozen, monkeypatch) + released = json.loads(frozen.row()["manifest_json"]) + plan_bytes = frozen.frozen_bytes(f"workflows/{frozen.workflow_id}/control-plan.json") + plan = json.loads(plan_bytes) + stored_timeouts = {row["id"]: row["timeout_seconds"] for row in plan["nodes"] + if row["kind"] == "agent"} + assert released["control_plan"]["digest"] == _sha256(plan_bytes) + assert released["execution_limits"]["max_parallel"] == 4 + assert plan["limits"]["maximum_parallelism"] == 2 + + done = coordinator.run(frozen.workflow_id) + + assert done.state == "completed" + # One control preflight over the released plan's own bytes; no legacy probe. + assert seen == [("control", plan_bytes)] + execution = done.manifest["executions"][-1] + assert execution["control_plan_digest"] == released["control_plan"]["digest"] + evidence = execution["preflight"] + assert (evidence["status"], evidence["control_plan"]["digest"]) == ( + "ok", released["control_plan"]["digest"] + ) + evidence_file = ( + coordinator.store.workflow_dir(frozen.workflow_id) / "executions" + / execution["execution_id"] / "control-preflight.json" + ) + assert evidence["digest"] == _sha256(evidence_file.read_bytes()) + # Each limit is the tighter of the current config and the released one; + # parallelism is further capped by the released plan. + stored = released["execution_limits"] + assert execution["limits"] == { + "deadline_seconds": 1800 if tight else stored["deadline_seconds"], + "max_parallel": 1 if tight else plan["limits"]["maximum_parallelism"], + "max_output_bytes": stored["max_output_bytes"], + } + assert agents["codex"].timeouts == [stored_timeouts["workflow:draft"]] + assert agents["kimi-code"].timeouts == [120 if tight else stored_timeouts["workflow:fix"]] + _approved_final(coordinator, done, "kimi-code") + # The released plan file is read, never rewritten. + assert (coordinator.store.workflow_dir(frozen.workflow_id) / "control-plan.json" + ).read_bytes() == plan_bytes + + +@pytest.mark.parametrize( + ("change", "error"), + [ + ({"tools": ["workspace::read"]}, + "Toolgraph agent mapping or candidate tools changed; create a new workflow plan"), + ({"agents": {"codex": "codex", "kimi-code": "kimi-2"}}, + "Toolgraph agent mapping or candidate tools changed; create a new workflow plan"), + ({"gate": False}, "workflow validation topology changed; create a new workflow plan"), + ], + ids=["tools", "agent-map", "gate"], +) +def test_a_released_control_plan_refuses_a_changed_configuration( + release, change, error, tmp_path, monkeypatch +): + from syncmill.workflow import WorkflowError + + change = dict(change) # the parameter is shared by both releases + gate = change.pop("gate", None) + seen = _toolgraph(monkeypatch, **change) + frozen = Frozen(release, "control-planned", tmp_path, monkeypatch) + coordinator, agents = _coordinator(frozen, monkeypatch, gate=gate) + before = frozen.row() + with pytest.raises(WorkflowError) as refused: + coordinator.run(frozen.workflow_id) + assert str(refused.value) == error + assert seen == [] + assert agents["codex"].calls == agents["kimi-code"].calls == [] + assert frozen.row() == before + + +@pytest.mark.parametrize("toolgraph", ["control-plans-off", "toolgraph-off"]) +def test_a_released_control_plan_still_bounds_a_run_without_control_plans( + release, toolgraph, tmp_path, monkeypatch +): + seen = _toolgraph(monkeypatch, enabled=toolgraph == "control-plans-off", control=False) + frozen = Frozen(release, "control-planned", tmp_path, monkeypatch) + coordinator, agents = _coordinator(frozen, monkeypatch) + released = json.loads(frozen.row()["manifest_json"]) + plan = json.loads(frozen.frozen_bytes(f"workflows/{frozen.workflow_id}/control-plan.json")) + + done = coordinator.run(frozen.workflow_id) + + assert done.state == "completed" + # Without control plans the run makes the lead agent's legacy advisory + # probe; without Toolgraph, none at all. + assert seen == ([("legacy", "codex")] if toolgraph == "control-plans-off" else []) + # The released plan is still read and still bounds the run. + execution = done.manifest["executions"][-1] + assert execution["control_plan_digest"] == released["control_plan"]["digest"] + assert execution["limits"]["max_parallel"] == plan["limits"]["maximum_parallelism"] + stored_fix = next(row["timeout_seconds"] for row in plan["nodes"] if row["id"] == "workflow:fix") + assert agents["kimi-code"].timeouts == [stored_fix] + + +def test_a_released_control_completed_workflow_reads_as_written(release, tmp_path, monkeypatch): + frozen = Frozen(release, "control-completed", tmp_path, monkeypatch) + released = json.loads(frozen.row()["manifest_json"]) + [execution] = released["executions"] + workflow_dir = f"workflows/{frozen.workflow_id}" + plan_bytes = frozen.frozen_bytes(f"{workflow_dir}/control-plan.json") + evidence_name = f"{workflow_dir}/executions/{execution['execution_id']}/control-preflight.json" + evidence_bytes = frozen.frozen_bytes(evidence_name) + assert execution["preflight"]["digest"] == _sha256(evidence_bytes) + + shown = frozen.cli("workflow", "show", frozen.workflow_id) + assert shown.exit_code == 0, shown.output + assert _lines(shown.output, "control_plan: ") == [ + f"control_plan: {released['control_plan']['digest']}" + ] + assert _lines(shown.output, "execution: ") == [ + f"execution: {execution['execution_id']} action=run status=completed" + ] + assert _lines(shown.output, "preflight: ") == [ + f"preflight: ok generation={execution['preflight']['graph_state']['generation']}" + ] + status = frozen.cli("workflow", "status", frozen.workflow_id) + assert status.exit_code == 0, status.output + assert _store(frozen).read_control_plan(frozen.workflow_id) == plan_bytes + + run_id, run_dir = _final(frozen) + manifest = json.loads((run_dir / "manifest.json").read_text()) + assert manifest["plan"]["control_plan_digest"] == released["control_plan"]["digest"] + conversation = frozen.cli("conversations", "show", run_id) + assert conversation.exit_code == 0, conversation.output + assert json.loads(conversation.stdout)["manifest"] == manifest + applied = frozen.cli("apply", run_id) + assert applied.exit_code == 0, applied.output + assert (frozen.repo / "calc.py").read_text() == FIXED + # No reader revalidates or rewrites the released evidence. + assert (frozen.project_dir / evidence_name).read_bytes() == evidence_bytes + assert (frozen.project_dir / workflow_dir / "control-plan.json").read_bytes() == plan_bytes + + +# ── part 2b: an interrupted apply of a released final artifact ──────────── + + +@pytest.mark.parametrize( + ("scenario", "patch_in_source", "outcome", "state"), + [ + ("apply-receipted", True, "completed", "applied"), + ("apply-unreceipted", False, "not applied", "pending"), + ("apply-receipted", False, "manual recovery required", "recovery_required"), + ], + ids=["receipted-applied", "unreceipted", "receipted-undecidable"], +) +def test_recover_a_released_interrupted_workflow_apply( + release, scenario, patch_in_source, outcome, state, tmp_path, monkeypatch +): + from syncmill.artifacts import ArtifactStore + + frozen = Frozen(release, scenario, tmp_path, monkeypatch) + run_id, run_dir = _final(frozen) + before = json.loads((run_dir / "manifest.json").read_text()) + assert before["state"] == "applying" and "approved" in before["plan"] + receipts = sorted((run_dir / "applications").glob("*.json")) + assert len(receipts) == (1 if scenario == "apply-receipted" else 0) + store = ArtifactStore(frozen.runs_root, frozen.facts["project_id"]) + # The receipt is bound to this apply by id, run and patch digest. + assert store._bound_receipt(run_dir, before) == (receipts[0] if receipts else None) + if patch_in_source: + subprocess.run(["git", "apply", "-"], cwd=frozen.repo, + input=(run_dir / "winner.patch").read_bytes(), check=True) + row = frozen.row() + + recovered = frozen.cli("recover", run_id) + + assert recovered.exit_code == 0, recovered.output + [line] = recovered.output.splitlines() + assert line.startswith(f"Run {run_id}: {outcome} \N{EM DASH} ") + after = json.loads((run_dir / "manifest.json").read_text()) + assert after["state"] == state + # Recovery neither needs nor touches the workflow's approval or its row. + assert after["plan"] == before["plan"] + assert frozen.row() == row + expected_source = FIXED if patch_in_source else frozen.bindings["tree"]["calc.py"] + assert (frozen.repo / "calc.py").read_text() == expected_source From 381fbc1c9adb8da0ad0d30d753a9b5fa84829ea5 Mon Sep 17 00:00:00 2001 From: memtomem <118508877+memtomem@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:21:13 +0900 Subject: [PATCH 2/3] Bind the 2b tests to the limits, result and sidecar they claim (#271) Review round 1 found assertions that survived the code they protect: - every current limit used to be equal to or tighter than the released one, so taking the current value outright passed; the released-limits cases now run with wider current limits and assert the released ceilings, including with control plans or Toolgraph off; - reclaim checked the totals and the patch but not the final result rebuilt from the bundle; its output and its usage (TokenUsage.from_mapping) are now captured where the artifact is saved; - conversations show was checked on its manifest only; its result must equal the released result.json. 16 injected regressions now each fail a test here, and part 2a's 24 still do. Co-Authored-By: Claude Opus 5.5 --- tests/test_frozen_workflows.py | 54 ++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/tests/test_frozen_workflows.py b/tests/test_frozen_workflows.py index 2ae614b..629ef98 100644 --- a/tests/test_frozen_workflows.py +++ b/tests/test_frozen_workflows.py @@ -1513,10 +1513,17 @@ def test_resume_reclaims_a_released_bundle_saved_before_the_crash( assert (stored["status"], stored["gate"]["passed"]) == ("completed", True) assert (stored["usage"]["total_input_tokens"], stored["usage"]["output_tokens"]) == (7, 3) patch = (bundle / "winner.patch").read_bytes() + saved = _capture_final_results(monkeypatch) done = coordinator.run(frozen.workflow_id, resume=True) assert done.state == "completed" + # The final result is rebuilt from the released bundle: its output and + # its usage (TokenUsage.from_mapping), as the release saved them. + [result] = saved + assert result.output == stored["output"] + assert result.usage is not None + assert (result.usage.total_input_tokens, result.usage.output_tokens) == (7, 3) # Reclaimed, not re-run: no provider call, and the call the release # reserved before dispatch stays counted once. assert agents["codex"].calls == agents["kimi-code"].calls == [] @@ -1535,6 +1542,21 @@ def test_resume_reclaims_a_released_bundle_saved_before_the_crash( assert error == "" and f"[syncmill] final run: {done.final_run_id}" in lines +def _capture_final_results(monkeypatch) -> list: + """Every TaskResult handed to the artifact store for saving.""" + from syncmill.artifacts import ArtifactStore + + saved: list = [] + original = ArtifactStore.save + + def save(self, **kwargs): + saved.append(kwargs["result"]) + return original(self, **kwargs) + + monkeypatch.setattr(ArtifactStore, "save", save) + return saved + + def _damage_bundle_patch(bundle: Path) -> None: (bundle / "winner.patch").write_bytes(b"not the saved patch\n") @@ -1624,6 +1646,16 @@ def seam(argv, *args, **kwargs): return seen +def _wider_than_released(monkeypatch) -> None: + """Current limits wider than every limit the 2b scenarios were written + under (deadline 3600 s, parallelism 4, output 1 MiB, agent timeout 300 s).""" + monkeypatch.setenv("SYNCMILL_WORKFLOW__DEADLINE_SECONDS", "7200") + monkeypatch.setenv("SYNCMILL_WORKFLOW__MAX_PARALLEL", "8") + monkeypatch.setenv("SYNCMILL_WORKFLOW__MAX_OUTPUT_BYTES", str(4 * 1024 * 1024)) + monkeypatch.setenv("SYNCMILL_KIMI_CODE__TIMEOUT_SECONDS", "900") + monkeypatch.setenv("SYNCMILL_CODEX__TIMEOUT_SECONDS", "900") + + def _sha256(data: bytes) -> str: import hashlib @@ -1638,8 +1670,8 @@ def test_run_a_released_control_planned_workflow(release, tight, tmp_path, monke monkeypatch.setenv("SYNCMILL_WORKFLOW__MAX_PARALLEL", "1") monkeypatch.setenv("SYNCMILL_KIMI_CODE__TIMEOUT_SECONDS", "120") else: - # Wider than the plan's stored node timeout: the stored one holds. - monkeypatch.setenv("SYNCMILL_KIMI_CODE__TIMEOUT_SECONDS", "900") + # Wider than every released limit: each released one holds. + _wider_than_released(monkeypatch) frozen = Frozen(release, "control-planned", tmp_path, monkeypatch) coordinator, agents = _coordinator(frozen, monkeypatch) released = json.loads(frozen.row()["manifest_json"]) @@ -1718,6 +1750,7 @@ def test_a_released_control_plan_still_bounds_a_run_without_control_plans( release, toolgraph, tmp_path, monkeypatch ): seen = _toolgraph(monkeypatch, enabled=toolgraph == "control-plans-off", control=False) + _wider_than_released(monkeypatch) frozen = Frozen(release, "control-planned", tmp_path, monkeypatch) coordinator, agents = _coordinator(frozen, monkeypatch) released = json.loads(frozen.row()["manifest_json"]) @@ -1732,9 +1765,15 @@ def test_a_released_control_plan_still_bounds_a_run_without_control_plans( # The released plan is still read and still bounds the run. execution = done.manifest["executions"][-1] assert execution["control_plan_digest"] == released["control_plan"]["digest"] - assert execution["limits"]["max_parallel"] == plan["limits"]["maximum_parallelism"] - stored_fix = next(row["timeout_seconds"] for row in plan["nodes"] if row["id"] == "workflow:fix") - assert agents["kimi-code"].timeouts == [stored_fix] + stored = released["execution_limits"] + assert execution["limits"] == { + "deadline_seconds": stored["deadline_seconds"], + "max_parallel": plan["limits"]["maximum_parallelism"], + "max_output_bytes": stored["max_output_bytes"], + } + timeouts = {row["id"]: row["timeout_seconds"] for row in plan["nodes"] if row["kind"] == "agent"} + assert agents["codex"].timeouts == [timeouts["workflow:draft"]] + assert agents["kimi-code"].timeouts == [timeouts["workflow:fix"]] def test_a_released_control_completed_workflow_reads_as_written(release, tmp_path, monkeypatch): @@ -1767,7 +1806,10 @@ def test_a_released_control_completed_workflow_reads_as_written(release, tmp_pat assert manifest["plan"]["control_plan_digest"] == released["control_plan"]["digest"] conversation = frozen.cli("conversations", "show", run_id) assert conversation.exit_code == 0, conversation.output - assert json.loads(conversation.stdout)["manifest"] == manifest + shown = json.loads(conversation.stdout) + assert shown["manifest"] == manifest + assert shown["result"] == json.loads(frozen.frozen_bytes(f"runs/{run_id}/result.json")) + assert shown["result"]["output"] applied = frozen.cli("apply", run_id) assert applied.exit_code == 0, applied.output assert (frozen.repo / "calc.py").read_text() == FIXED From 3e3ed4778dc1ddb1f07f9d43d64a8ac033293295 Mon Sep 17 00:00:00 2001 From: memtomem <118508877+memtomem@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:38:49 +0900 Subject: [PATCH 3/3] Pin the current output ceiling on a released control plan (#271) Review round 2: the tighter-config case never lowered max_output_bytes, so ignoring the current output ceiling in favour of the stored one passed. It now runs with 64 KiB and asserts that value. 17 injected regressions now each fail a test here. Co-Authored-By: Claude Opus 5.5 --- tests/test_frozen_workflows.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_frozen_workflows.py b/tests/test_frozen_workflows.py index 629ef98..6da44cc 100644 --- a/tests/test_frozen_workflows.py +++ b/tests/test_frozen_workflows.py @@ -1668,6 +1668,7 @@ def test_run_a_released_control_planned_workflow(release, tight, tmp_path, monke if tight: monkeypatch.setenv("SYNCMILL_WORKFLOW__DEADLINE_SECONDS", "1800") monkeypatch.setenv("SYNCMILL_WORKFLOW__MAX_PARALLEL", "1") + monkeypatch.setenv("SYNCMILL_WORKFLOW__MAX_OUTPUT_BYTES", "65536") monkeypatch.setenv("SYNCMILL_KIMI_CODE__TIMEOUT_SECONDS", "120") else: # Wider than every released limit: each released one holds. @@ -1705,7 +1706,7 @@ def test_run_a_released_control_planned_workflow(release, tight, tmp_path, monke assert execution["limits"] == { "deadline_seconds": 1800 if tight else stored["deadline_seconds"], "max_parallel": 1 if tight else plan["limits"]["maximum_parallelism"], - "max_output_bytes": stored["max_output_bytes"], + "max_output_bytes": 65536 if tight else stored["max_output_bytes"], } assert agents["codex"].timeouts == [stored_timeouts["workflow:draft"]] assert agents["kimi-code"].timeouts == [120 if tight else stored_timeouts["workflow:fix"]]