@kontourai/flow-agents 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -12,6 +12,34 @@
12
12
  # 6. AC6 SHAPE-INVALID-REFUSED (#356): a shape-invalid trust.bundle (test_output claim
13
13
  # naming a non-manifest command) is REFUSED by publishDelivery() -- non-zero exit, loud
14
14
  # message, nothing copied to delivery/. Distinct from FAIL-SOFT (bundle absent).
15
+ # 7. AC1 HEAD-MISMATCH-REFUSED (#413 Facet A): a real git repo whose sealed
16
+ # trust.checkpoint.json commit_sha names commit A, but the repo's actual HEAD has been
17
+ # advanced to an unrelated orphan commit B (no ancestor relationship) -- publish-delivery
18
+ # REFUSES loudly (non-zero exit, RepoHeadMismatchError message), nothing copied to
19
+ # delivery/. The happy path (HEAD matches the sealed commit_sha) still publishes.
20
+ # 8. AC2 CROSS-SESSION-SEAL-SURVIVES (#413 Facet B): a fixture delivery/<other-slug>/ seal
21
+ # pre-exists before publishing a DIFFERENT slug -- the other slug's seal directory
22
+ # survives byte-identical; pruneSupersededSeals only ever touches its OWN keepSlug
23
+ # directory, never another session's.
24
+ # 9. FIX 4 SHALLOW-UNDETERMINABLE-WARNS-NOT-REFUSES (#413 iteration-2): a REAL shallow clone
25
+ # (git clone --depth 1) whose sealed trust.checkpoint.json commit_sha names a commit the
26
+ # shallow clone's object database does not have (so ancestry is UNDETERMINABLE, not
27
+ # positively confirmed absent) -- publish-delivery WARNS but still PUBLISHES (exit 0, bundle
28
+ # copied). Distinct from AC1 HEAD-MISMATCH-REFUSED above, which is a POSITIVELY-DETERMINABLE
29
+ # mismatch and correctly still hard-refuses.
30
+ # 10. NON-GIT-REPOROOT-WITH-SEALED-COMMIT-ALLOWS (#413 iteration-2 Fix 5, REVERTED in
31
+ # iteration-3): a sealed trust.checkpoint.json names a real commit_sha, but the resolved
32
+ # repoRoot is NOT a git working tree at all (a bare mkdir -p, no .git) -- publish-delivery
33
+ # still PUBLISHES (exit 0, bundle copied). This is a SUPPORTED flow (e.g. checkpoint-signing
34
+ # against a scratch/non-git dir -- see evals/integration/test_checkpoint_signing.sh TEST 2),
35
+ # not a mismatch signal: iteration-2's Fix 5 briefly hard-refused this case ("a sealed
36
+ # delivery implies the target should be a real git repo"), but that premise is false and
37
+ # regressed the supported scratch-dir flow, so it was reverted. Wrong-tree protection for a
38
+ # REAL git repoRoot still relies on the shape-check, verify-hold, and HEAD-vs-commit_sha
39
+ # ancestry gates (TEST 6/7/9 above), which correctly no-op (allow) here since there is no git
40
+ # HEAD to compare against. The pre-existing "no checkpoint / no commit_sha -> allow"
41
+ # no-opinion case (already covered by TEST 1/2/5/6 above, which use bare mkdir -p repos with
42
+ # no checkpoint at all) stays exactly the same allow behavior.
15
43
  #
16
44
  # Deterministic, no model spend, self-cleaning.
17
45
  # Usage: bash evals/integration/test_publish_delivery.sh
@@ -116,6 +144,14 @@ AROOT1="$REPO1/.flow-agents"
116
144
  SLUG1="pd-release-test"
117
145
  SESSION_DIR1="$AROOT1/$SLUG1"
118
146
  mkdir -p "$REPO1/kits"
147
+ printf '# marker\n' > "$REPO1/kits/.keep"
148
+ # REPO1 is a bare mkdir -p, not a real git working tree (#413 iteration-2 Fix 5 briefly required a
149
+ # real git repo here since it hard-refused a non-git repoRoot with a sealed commit_sha; Fix 5 was
150
+ # reverted in iteration-3 -- see checkpointHeadAncestry's doc comment -- so the bare-dir fixture is
151
+ # restored). record-release's sealTrustCheckpoint() calls resolveCommitSha(), which is deliberately
152
+ # AMBIENT (cwd-inheriting, never --repo-root-scoped) and fails (returns null) since $TMP itself is
153
+ # not inside any git working tree -- so no commit_sha is ever sealed here, and this test never
154
+ # touches the HEAD-vs-commit_sha ancestry gate at all (mismatchKind stays "none").
119
155
 
120
156
  FIXTURE1="$TMP/fixture1.json"
121
157
  write_bundle_to "$FIXTURE1" "node --version" "true"
@@ -128,7 +164,7 @@ setup_session "$AROOT1" "$SLUG1" "$FIXTURE1"
128
164
  # into the manifest. Setting it here makes this fixture genuinely shape-valid (a
129
165
  # manifest-matched command claim), matching what CI would actually accept for a repo whose
130
166
  # canonical verify is "node --version" — not a preflight-specific carve-out.
131
- rr_out1=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" record-release "$SESSION_DIR1" \
167
+ rr_out1=$(cd "$REPO1" && TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" record-release "$SESSION_DIR1" \
132
168
  --decision merge \
133
169
  --gate-json '{"name":"merge","status":"pass","summary":"Ready."}' \
134
170
  --summary "Release." --repo-root "$REPO1" \
@@ -339,6 +375,300 @@ else
339
375
  _fail "AC6 SHAPE-INVALID-REFUSED: delivery/$SLUG6/trust.bundle was created despite invalid shape"
340
376
  fi
341
377
 
378
+ # ==== TEST 7: AC1 HEAD-MISMATCH-REFUSED (#413 Facet A) ==============
379
+ # publishDelivery() now refuses loudly when the resolved repoRoot's HEAD is not an
380
+ # ancestor-or-equal of the session's sealed trust.checkpoint.json commit_sha -- a real git
381
+ # repo is required here (not a bare mkdir -p fixture) so `git rev-parse HEAD` genuinely
382
+ # resolves and the ancestor check has real commits to compare.
383
+ echo ""
384
+ echo "=== TEST 7: AC1 HEAD-MISMATCH-REFUSED ==="
385
+
386
+ REPO7="$TMP/repo7"
387
+ AROOT7="$REPO7/.flow-agents"
388
+ SLUG7="pd-head-mismatch-test"
389
+ SESSION_DIR7="$AROOT7/$SLUG7"
390
+ mkdir -p "$REPO7/kits"
391
+ printf '# marker\n' > "$REPO7/kits/.keep"
392
+ (cd "$REPO7" && git init -q && git config user.email "test@test.local" && git config user.name "test" && git add -A && git commit -q -m "commit A")
393
+ SHA_A="$(cd "$REPO7" && git rev-parse HEAD)"
394
+
395
+ FIXTURE7="$TMP/fixture7.json"
396
+ write_bundle_to "$FIXTURE7" "node --version" "true"
397
+ setup_session "$AROOT7" "$SLUG7" "$FIXTURE7"
398
+ # Seal a checkpoint naming commit A as the trusted commit_sha -- mirrors write_checkpoint_to's
399
+ # minimal, well-formed shape but targets the SESSION dir (where publishDelivery reads it from),
400
+ # not the delivery/ dir (write_checkpoint_to's existing target for the reconcile tests above).
401
+ printf '{"schema_version":"1.0","slug":"%s","work_item":null,"status":"delivered","phase":"release","sealed_at":"2026-06-27T00:00:00Z","commit_sha":"%s","checkpoint":{"asOf":"2026-06-27T00:00:00.000Z","statusByClaimId":{}}}' \
402
+ "$SLUG7" "$SHA_A" > "$SESSION_DIR7/trust.checkpoint.json"
403
+
404
+ # Advance repo7's HEAD to a genuinely UNRELATED orphan commit B -- no ancestor relationship
405
+ # with commit A at all.
406
+ (cd "$REPO7" && git checkout -q --orphan orphan-branch && git rm -rq --cached . >/dev/null 2>&1)
407
+ (cd "$REPO7" && mkdir -p kits && printf 'unrelated\n' > kits/unrelated.txt && git add -A && git commit -q -m "commit B (unrelated orphan)")
408
+ SHA_B="$(cd "$REPO7" && git rev-parse HEAD)"
409
+
410
+ if [[ "$SHA_A" != "$SHA_B" ]]; then
411
+ _pass "TEST 7 setup: commit A and commit B are genuinely distinct shas"
412
+ else
413
+ _fail "TEST 7 setup: commit A and commit B unexpectedly matched"
414
+ fi
415
+
416
+ mismatch_out=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR7" --repo-root "$REPO7" 2>&1)
417
+ mismatch_exit=$?
418
+
419
+ if [[ $mismatch_exit -ne 0 ]]; then
420
+ _pass "AC1 HEAD-MISMATCH-REFUSED: publish-delivery exits non-zero when repoRoot HEAD does not match the sealed commit_sha"
421
+ else
422
+ _fail "AC1 HEAD-MISMATCH-REFUSED: expected non-zero exit, got 0 -- $mismatch_out"
423
+ fi
424
+
425
+ if echo "$mismatch_out" | grep -q "REFUSING to publish"; then
426
+ _pass "AC1 HEAD-MISMATCH-REFUSED: output names the refusal loudly (REFUSING to publish)"
427
+ else
428
+ _fail "AC1 HEAD-MISMATCH-REFUSED: expected a loud REFUSING to publish message, got: $mismatch_out"
429
+ fi
430
+
431
+ if echo "$mismatch_out" | grep -qF "$SHA_A" && echo "$mismatch_out" | grep -qF "$SHA_B"; then
432
+ _pass "AC1 HEAD-MISMATCH-REFUSED: refusal message names both the checkpoint commit_sha and the mismatched HEAD sha"
433
+ else
434
+ _fail "AC1 HEAD-MISMATCH-REFUSED: refusal message did not name both shas: $mismatch_out"
435
+ fi
436
+
437
+ DELIVERY_BUNDLE7="$REPO7/delivery/$SLUG7/trust.bundle"
438
+ if [[ ! -f "$DELIVERY_BUNDLE7" ]]; then
439
+ _pass "AC1 HEAD-MISMATCH-REFUSED: delivery/$SLUG7/trust.bundle NOT created on HEAD mismatch"
440
+ else
441
+ _fail "AC1 HEAD-MISMATCH-REFUSED: delivery/$SLUG7/trust.bundle was created despite the HEAD mismatch"
442
+ fi
443
+
444
+ # Happy path: seal a SECOND session's checkpoint against the repo's CURRENT (matching) HEAD --
445
+ # publish-delivery must still succeed. Proves the gate is scoped strictly to the mismatch case,
446
+ # never a spurious refusal.
447
+ SLUG7B="pd-head-match-test"
448
+ SESSION_DIR7B="$AROOT7/$SLUG7B"
449
+ FIXTURE7B="$TMP/fixture7b.json"
450
+ write_bundle_to "$FIXTURE7B" "node --version" "true"
451
+ setup_session "$AROOT7" "$SLUG7B" "$FIXTURE7B"
452
+ printf '{"schema_version":"1.0","slug":"%s","work_item":null,"status":"delivered","phase":"release","sealed_at":"2026-06-27T00:00:00Z","commit_sha":"%s","checkpoint":{"asOf":"2026-06-27T00:00:00.000Z","statusByClaimId":{}}}' "$SLUG7B" "$SHA_B" > "$SESSION_DIR7B/trust.checkpoint.json"
453
+
454
+ match_out=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR7B" --repo-root "$REPO7" 2>&1)
455
+ match_exit=$?
456
+
457
+ if [[ $match_exit -eq 0 ]]; then
458
+ _pass "AC1 happy path: publish-delivery succeeds when repoRoot HEAD matches the sealed commit_sha"
459
+ else
460
+ _fail "AC1 happy path: publish-delivery unexpectedly failed on a matching HEAD/commit_sha: $match_out"
461
+ fi
462
+ if [[ -f "$REPO7/delivery/$SLUG7B/trust.bundle" ]]; then
463
+ _pass "AC1 happy path: delivery/$SLUG7B/trust.bundle created when HEAD matches commit_sha"
464
+ else
465
+ _fail "AC1 happy path: delivery/$SLUG7B/trust.bundle NOT created despite a matching HEAD/commit_sha"
466
+ fi
467
+
468
+ # ==== TEST 8: AC2 CROSS-SESSION-SEAL-SURVIVES (#413 Facet B) ========
469
+ # pruneSupersededSeals must never delete another session's delivery/<other-slug>/ directory --
470
+ # only its OWN keepSlug directory. Seed a fixture other-session seal BEFORE publishing a
471
+ # DIFFERENT slug, then assert the fixture survives byte-identical.
472
+ echo ""
473
+ echo "=== TEST 8: AC2 CROSS-SESSION-SEAL-SURVIVES ==="
474
+
475
+ REPO8="$TMP/repo8"
476
+ AROOT8="$REPO8/.flow-agents"
477
+ SLUG8="pd-cross-session-test"
478
+ SESSION_DIR8="$AROOT8/$SLUG8"
479
+ mkdir -p "$REPO8/kits"
480
+
481
+ OTHER_SLUG="pd-other-inflight-session"
482
+ OTHER_SEAL_DIR="$REPO8/delivery/$OTHER_SLUG"
483
+ mkdir -p "$OTHER_SEAL_DIR"
484
+ printf '{"marker":"other-session-trust-bundle","schemaVersion":5}' > "$OTHER_SEAL_DIR/trust.bundle"
485
+ printf '{"schema_version":"1.0","slug":"%s","work_item":null,"status":"delivered","phase":"release","sealed_at":"2026-06-27T00:00:00Z","commit_sha":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef","checkpoint":{"asOf":"2026-06-27T00:00:00.000Z","statusByClaimId":{}}}' > "$OTHER_SEAL_DIR/trust.checkpoint.json"
486
+ OTHER_BUNDLE_BEFORE="$(cat "$OTHER_SEAL_DIR/trust.bundle")"
487
+ OTHER_CHECKPOINT_BEFORE="$(cat "$OTHER_SEAL_DIR/trust.checkpoint.json")"
488
+
489
+ FIXTURE8="$TMP/fixture8.json"
490
+ write_bundle_to "$FIXTURE8" "node --version" "true"
491
+ setup_session "$AROOT8" "$SLUG8" "$FIXTURE8"
492
+
493
+ cross_out=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR8" --repo-root "$REPO8" 2>&1)
494
+ cross_exit=$?
495
+
496
+ if [[ $cross_exit -eq 0 ]]; then
497
+ _pass "AC2 CROSS-SESSION-SEAL-SURVIVES: publish-delivery for $SLUG8 exits 0"
498
+ else
499
+ _fail "AC2 CROSS-SESSION-SEAL-SURVIVES: publish-delivery for $SLUG8 unexpectedly failed: $cross_out"
500
+ fi
501
+
502
+ if [[ -f "$REPO8/delivery/$SLUG8/trust.bundle" ]]; then
503
+ _pass "AC2 CROSS-SESSION-SEAL-SURVIVES: this session's OWN delivery/$SLUG8/trust.bundle was written"
504
+ else
505
+ _fail "AC2 CROSS-SESSION-SEAL-SURVIVES: delivery/$SLUG8/trust.bundle was not written"
506
+ fi
507
+
508
+ if [[ -d "$OTHER_SEAL_DIR" ]]; then
509
+ _pass "AC2 CROSS-SESSION-SEAL-SURVIVES: the OTHER session's delivery/$OTHER_SLUG/ directory SURVIVES"
510
+ else
511
+ _fail "AC2 CROSS-SESSION-SEAL-SURVIVES: the other session's delivery/$OTHER_SLUG/ directory was deleted -- cross-slug pruning regressed"
512
+ fi
513
+
514
+ if [[ -f "$OTHER_SEAL_DIR/trust.bundle" && "$(cat "$OTHER_SEAL_DIR/trust.bundle" 2>/dev/null)" == "$OTHER_BUNDLE_BEFORE" ]]; then
515
+ _pass "AC2 CROSS-SESSION-SEAL-SURVIVES: the other session's trust.bundle is byte-identical after this publish"
516
+ else
517
+ _fail "AC2 CROSS-SESSION-SEAL-SURVIVES: the other session's trust.bundle was mutated or removed"
518
+ fi
519
+
520
+ if [[ -f "$OTHER_SEAL_DIR/trust.checkpoint.json" && "$(cat "$OTHER_SEAL_DIR/trust.checkpoint.json" 2>/dev/null)" == "$OTHER_CHECKPOINT_BEFORE" ]]; then
521
+ _pass "AC2 CROSS-SESSION-SEAL-SURVIVES: the other session's trust.checkpoint.json is byte-identical after this publish"
522
+ else
523
+ _fail "AC2 CROSS-SESSION-SEAL-SURVIVES: the other session's trust.checkpoint.json was mutated or removed"
524
+ fi
525
+
526
+ # Mutation-test note (prune scoping): this eval passes ONLY because pruneSupersededSeals was
527
+ # scoped to keepSlug's own directory (#413 Facet B). Prior to that fix, this loop deleted ANY
528
+ # non-keepSlug directory under delivery/ that looked like a seal (contained trust.bundle or
529
+ # trust.checkpoint.json) -- exactly OTHER_SEAL_DIR's shape -- so this test would have failed
530
+ # the two directory/byte-identical checks above against the pre-fix implementation.
531
+
532
+ # ==== TEST 9: FIX 4 SHALLOW-UNDETERMINABLE-WARNS-NOT-REFUSES (#413 iteration-2) =====
533
+ # A REAL shallow clone (git clone --depth 1) whose sealed trust.checkpoint.json commit_sha names
534
+ # an OLDER commit the shallow clone's object database genuinely does not have -- `git merge-base
535
+ # --is-ancestor` cannot determine ancestry here (exit 128, not 0 or 1), which is the
536
+ # UNDETERMINABLE case Fix 4 must warn-and-allow, never hard-refuse. Distinct from TEST 7's
537
+ # POSITIVELY-DETERMINABLE orphan-commit mismatch, which correctly still hard-refuses.
538
+ echo ""
539
+ echo "=== TEST 9: FIX 4 SHALLOW-UNDETERMINABLE-WARNS-NOT-REFUSES ==="
540
+
541
+ SRC9="$TMP/src9"
542
+ mkdir -p "$SRC9/kits"
543
+ (cd "$SRC9" && git init -q && git config user.email "test@test.local" && git config user.name "test" \
544
+ && printf 'a\n' > kits/.keep && git add -A && git commit -q -m "commit A (older, will be missing from the shallow clone)")
545
+ SHA9_A="$(cd "$SRC9" && git rev-parse HEAD)"
546
+ (cd "$SRC9" && printf 'b\n' > kits/b.txt && git add -A && git commit -q -m "commit B")
547
+ (cd "$SRC9" && printf 'c\n' > kits/c.txt && git add -A && git commit -q -m "commit C (HEAD, shallow clone's only commit)")
548
+
549
+ REPO9="$TMP/repo9"
550
+ if git clone -q --depth 1 --no-local "file://$SRC9" "$REPO9" >/dev/null 2>"$TMP/shallow-clone9.err"; then
551
+ _pass "TEST 9 setup: a real shallow clone (--depth 1) was created"
552
+ else
553
+ _fail "TEST 9 setup: shallow clone failed: $(cat "$TMP/shallow-clone9.err")"
554
+ fi
555
+ SHA9_HEAD="$(cd "$REPO9" && git rev-parse HEAD)"
556
+
557
+ # Confirm the shallow clone genuinely cannot determine ancestry for the older, missing commit --
558
+ # the exact precondition this test depends on.
559
+ if ! (cd "$REPO9" && git cat-file -e "$SHA9_A" 2>/dev/null); then
560
+ _pass "TEST 9 setup: the shallow clone's object database genuinely lacks commit A (the precondition for UNDETERMINABLE ancestry)"
561
+ else
562
+ _fail "TEST 9 setup: the shallow clone unexpectedly has commit A -- this fixture no longer exercises the undeterminable path"
563
+ fi
564
+
565
+ AROOT9="$REPO9/.flow-agents"
566
+ SLUG9="pd-shallow-undeterminable-test"
567
+ SESSION_DIR9="$AROOT9/$SLUG9"
568
+ mkdir -p "$REPO9/kits" # kits/ already exists from the clone, but keep parity with other tests' setup shape
569
+
570
+ FIXTURE9="$TMP/fixture9.json"
571
+ write_bundle_to "$FIXTURE9" "node --version" "true"
572
+ setup_session "$AROOT9" "$SLUG9" "$FIXTURE9"
573
+ # Seal a checkpoint naming the OLDER commit (A) the shallow clone does not have -- mirrors TEST
574
+ # 7's shape but targets a genuinely shallow repo.
575
+ printf '{"schema_version":"1.0","slug":"%s","work_item":null,"status":"delivered","phase":"release","sealed_at":"2026-06-27T00:00:00Z","commit_sha":"%s","checkpoint":{"asOf":"2026-06-27T00:00:00.000Z","statusByClaimId":{}}}' \
576
+ "$SLUG9" "$SHA9_A" > "$SESSION_DIR9/trust.checkpoint.json"
577
+
578
+ shallow_out=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR9" --repo-root "$REPO9" 2>&1)
579
+ shallow_exit=$?
580
+
581
+ if [[ $shallow_exit -eq 0 ]]; then
582
+ _pass "FIX 4: publish-delivery from a shallow clone with an undeterminable checkpoint ancestry still exits 0 (warns, does not refuse)"
583
+ else
584
+ _fail "FIX 4: publish-delivery unexpectedly refused (non-zero exit) for the shallow/undeterminable case: $shallow_out"
585
+ fi
586
+
587
+ if echo "$shallow_out" | grep -qi "WARNING.*could not determine"; then
588
+ _pass "FIX 4: output contains a loud WARNING naming the undeterminable ancestry"
589
+ else
590
+ _fail "FIX 4: expected a loud WARNING for the undeterminable ancestry case, got: $shallow_out"
591
+ fi
592
+
593
+ if echo "$shallow_out" | grep -q "REFUSING to publish"; then
594
+ _fail "FIX 4: unexpectedly REFUSED the shallow/undeterminable case -- this must warn and allow, never hard-refuse"
595
+ else
596
+ _pass "FIX 4: no REFUSING to publish message -- the shallow/undeterminable case is correctly NOT treated as a hard refuse"
597
+ fi
598
+
599
+ if [[ -f "$REPO9/delivery/$SLUG9/trust.bundle" ]]; then
600
+ _pass "FIX 4: delivery/$SLUG9/trust.bundle WAS published despite the undeterminable ancestry (warn-and-allow, not refuse)"
601
+ else
602
+ _fail "FIX 4: delivery/$SLUG9/trust.bundle was NOT published -- the shallow/undeterminable case was incorrectly blocked"
603
+ fi
604
+
605
+ # Mutation-test note (Fix 4): this test passes ONLY because checkpointHeadAncestry's
606
+ # mismatchKind classification distinguishes "positive" (git merge-base --is-ancestor exits 1,
607
+ # a real answer) from "undeterminable-shallow" (exit 128 / spawn failure, e.g. this shallow
608
+ # clone's missing commit object). Prior to Fix 4, the collapsed isAncestorCommit() boolean
609
+ # treated shallow-missing exactly the same as a positively-confirmed non-ancestor, and this
610
+ # test would have failed the REFUSING/exit-0/bundle-published assertions above.
611
+
612
+ # ==== TEST 10: NON-GIT-REPOROOT-WITH-SEALED-COMMIT-ALLOWS (#413 iteration-2 Fix 5, reverted) ====
613
+ # repoRoot resolves to a real path, but it is NOT a git working tree at all (a bare mkdir -p,
614
+ # no .git anywhere) -- while the session's sealed trust.checkpoint.json names a real commit_sha.
615
+ # This is a SUPPORTED flow (scratch/checkpoint-signing dirs, see
616
+ # evals/integration/test_checkpoint_signing.sh TEST 2), so publish-delivery must still PUBLISH
617
+ # (exit 0, bundle copied) -- not the FIX 5 hard-refuse iteration-2 briefly added and iteration-3
618
+ # reverted. Distinct from the pre-existing "no checkpoint / no commit_sha -> allow" no-opinion
619
+ # case TEST 1/2/5/6 already cover with their own bare mkdir -p repos that have no checkpoint at
620
+ # all -- this test's REPO10 DOES have a sealed checkpoint with a real-looking commit_sha, and
621
+ # still allows.
622
+ echo ""
623
+ echo "=== TEST 10: NON-GIT-REPOROOT-WITH-SEALED-COMMIT-ALLOWS ==="
624
+
625
+ REPO10="$TMP/repo10"
626
+ AROOT10="$REPO10/.flow-agents"
627
+ SLUG10="pd-non-git-reporoot-test"
628
+ SESSION_DIR10="$AROOT10/$SLUG10"
629
+ mkdir -p "$REPO10/kits" # kits/ marker only -- deliberately NO git init anywhere under REPO10
630
+
631
+ FIXTURE10="$TMP/fixture10.json"
632
+ write_bundle_to "$FIXTURE10" "node --version" "true"
633
+ setup_session "$AROOT10" "$SLUG10" "$FIXTURE10"
634
+ # Seal a checkpoint naming a plausible-looking (but unverifiable, since there's no git repo here
635
+ # at all) commit_sha.
636
+ printf '{"schema_version":"1.0","slug":"%s","work_item":null,"status":"delivered","phase":"release","sealed_at":"2026-06-27T00:00:00Z","commit_sha":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","checkpoint":{"asOf":"2026-06-27T00:00:00.000Z","statusByClaimId":{}}}' \
637
+ > "$SESSION_DIR10/trust.checkpoint.json"
638
+
639
+ if [[ ! -d "$REPO10/.git" ]]; then
640
+ _pass "TEST 10 setup: REPO10 genuinely has no .git -- not a git working tree at all"
641
+ else
642
+ _fail "TEST 10 setup: REPO10 unexpectedly has a .git directory"
643
+ fi
644
+
645
+ nongit_out=$(TRUST_RECONCILE_COMMANDS="node --version" flow_agents_node "$WRITER" publish-delivery "$SESSION_DIR10" --repo-root "$REPO10" 2>&1)
646
+ nongit_exit=$?
647
+
648
+ if [[ $nongit_exit -eq 0 ]]; then
649
+ _pass "NON-GIT-REPOROOT-ALLOWS: publish-delivery exits 0 for a non-git repoRoot with a sealed commit_sha"
650
+ else
651
+ _fail "NON-GIT-REPOROOT-ALLOWS: expected exit 0 for a non-git repoRoot with a sealed commit_sha, got $nongit_exit -- $nongit_out"
652
+ fi
653
+
654
+ if ! echo "$nongit_out" | grep -q "REFUSING to publish"; then
655
+ _pass "NON-GIT-REPOROOT-ALLOWS: output does not contain a REFUSING to publish message"
656
+ else
657
+ _fail "NON-GIT-REPOROOT-ALLOWS: unexpected REFUSING to publish message, got: $nongit_out"
658
+ fi
659
+
660
+ if [[ -f "$REPO10/delivery/$SLUG10/trust.bundle" ]]; then
661
+ _pass "NON-GIT-REPOROOT-ALLOWS: delivery/$SLUG10/trust.bundle created for a non-git repoRoot with a sealed commit_sha"
662
+ else
663
+ _fail "NON-GIT-REPOROOT-ALLOWS: delivery/$SLUG10/trust.bundle NOT created despite the supported non-git-repoRoot flow"
664
+ fi
665
+
666
+ # Mutation-test note: this test passes only because checkpointHeadAncestry's non-git-repoRoot
667
+ # path resolves headSha to empty (git rev-parse HEAD fails outside a working tree) and returns
668
+ # mismatchKind:"none" (the same "no opinion, allow" result as the genuinely no-checkpoint case) --
669
+ # never the iteration-2 Fix 5 non-git-repo refuse arm, which this test's own removal proves is
670
+ # gone.
671
+
342
672
  # ---- Summary ----
343
673
  echo ""
344
674
  echo "----------------------------------------------"
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
+ source "$ROOT/evals/lib/env.sh"
6
+ flow_agents_eval_bootstrap "$ROOT" || exit $?
7
+ SCRIPT="$ROOT/scripts/pull-work-provider.js"
8
+ TMPDIR_EVAL="$(mktemp -d)"
9
+ trap 'rm -rf "$TMPDIR_EVAL"' EXIT
10
+
11
+ errors=0
12
+ pass() { echo " ✓ $1"; }
13
+ fail() { echo " ✗ $1"; errors=$((errors + 1)); }
14
+
15
+ json_query() {
16
+ NO_COLOR=1 FORCE_COLOR=0 node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=part==="length" ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]); console.log(cur);' "$1" "$2"
17
+ }
18
+
19
+ cat > "$TMPDIR_EVAL/settings.json" <<'JSON'
20
+ {
21
+ "schema_version": "1.0",
22
+ "projects": [
23
+ {
24
+ "project": { "repo": { "owner": "kontourai", "name": "flow-agents" } },
25
+ "work_item_provider": {
26
+ "role": "WorkItemProvider",
27
+ "kind": "github",
28
+ "repo": { "owner": "kontourai", "name": "flow-agents" },
29
+ "capabilities": ["issues", "labels"]
30
+ },
31
+ "board_provider": {
32
+ "role": "BoardProvider",
33
+ "kind": "github",
34
+ "repo": { "owner": "kontourai", "name": "flow-agents" },
35
+ "board": { "type": "github_project", "owner": "kontourai", "number": 1 },
36
+ "capabilities": ["projects_boards", "status_fields", "custom_fields"]
37
+ },
38
+ "selection": {
39
+ "filters": {
40
+ "issue_state": "open",
41
+ "include_labels": [],
42
+ "ready_statuses": ["ready"],
43
+ "exclude_statuses": ["in_progress", "blocked", "review", "verification", "done"]
44
+ },
45
+ "wip_policy": {
46
+ "prefer_finishing_active_work": true,
47
+ "active_statuses": ["in_progress", "review", "verification"],
48
+ "block_new_work_when_active_count_exceeds": 0
49
+ }
50
+ }
51
+ }
52
+ ]
53
+ }
54
+ JSON
55
+
56
+ cat > "$TMPDIR_EVAL/items.json" <<'JSON'
57
+ {
58
+ "items": [
59
+ {
60
+ "id": "PVTI_1",
61
+ "position": 0,
62
+ "fieldValues": [
63
+ { "field": { "name": "Status" }, "name": "Ready" },
64
+ { "field": { "name": "Priority" }, "name": "P1" }
65
+ ],
66
+ "content": {
67
+ "id": "I_7",
68
+ "number": 7,
69
+ "title": "P1 ready board item",
70
+ "body": "## Scope\nDo P1 work.\n\n## Acceptance criteria\nDone.",
71
+ "state": "OPEN",
72
+ "url": "https://github.com/kontourai/flow-agents/issues/7",
73
+ "repository": { "name": "flow-agents", "owner": { "login": "kontourai" } },
74
+ "labels": { "nodes": [{ "name": "builder" }] }
75
+ }
76
+ },
77
+ {
78
+ "id": "PVTI_2",
79
+ "position": 1,
80
+ "status": "In Progress",
81
+ "priority": "P0",
82
+ "content": {
83
+ "id": "I_8",
84
+ "number": 8,
85
+ "title": "Active item excluded from ready queue",
86
+ "body": "## Scope\nAlready active.",
87
+ "state": "OPEN",
88
+ "url": "https://github.com/kontourai/flow-agents/issues/8",
89
+ "repository": { "name": "flow-agents", "owner": { "login": "kontourai" } },
90
+ "labels": []
91
+ }
92
+ },
93
+ {
94
+ "id": "PVTI_3",
95
+ "position": 2,
96
+ "status": "ready",
97
+ "priority": "P0",
98
+ "content": {
99
+ "id": "I_5",
100
+ "number": 5,
101
+ "title": "P0 lower-case ready item",
102
+ "body": "## Scope\nDo P0 work.\n\n## Acceptance criteria\nDone.",
103
+ "state": "OPEN",
104
+ "url": "https://github.com/kontourai/flow-agents/issues/5",
105
+ "repository": { "name": "flow-agents", "owner": { "login": "kontourai" } },
106
+ "labels": []
107
+ }
108
+ },
109
+ {
110
+ "id": "PVTI_4",
111
+ "position": 3,
112
+ "status": "Ready",
113
+ "priority": "P2",
114
+ "content": {
115
+ "id": "I_9",
116
+ "number": 9,
117
+ "title": "P2 ready board item",
118
+ "body": "## Scope\nDo P2 work.\n\n## Acceptance criteria\nDone.",
119
+ "state": "OPEN",
120
+ "url": "https://github.com/kontourai/flow-agents/issues/9",
121
+ "repository": { "name": "flow-agents", "owner": { "login": "kontourai" } },
122
+ "labels": []
123
+ }
124
+ }
125
+ ],
126
+ "open_issues": [
127
+ {
128
+ "id": "I_5",
129
+ "number": 5,
130
+ "title": "P0 lower-case ready item",
131
+ "state": "OPEN",
132
+ "url": "https://github.com/kontourai/flow-agents/issues/5",
133
+ "labels": []
134
+ },
135
+ {
136
+ "id": "I_11",
137
+ "number": 11,
138
+ "title": "Open issue missing from board",
139
+ "state": "OPEN",
140
+ "url": "https://github.com/kontourai/flow-agents/issues/11",
141
+ "labels": [{ "name": "intake" }]
142
+ }
143
+ ]
144
+ }
145
+ JSON
146
+
147
+ cat > "$TMPDIR_EVAL/zero-ready.json" <<'JSON'
148
+ {
149
+ "items": [
150
+ {
151
+ "id": "PVTI_10",
152
+ "position": 0,
153
+ "status": "Blocked",
154
+ "priority": "P0",
155
+ "content": {
156
+ "id": "I_10",
157
+ "number": 10,
158
+ "title": "Blocked board item",
159
+ "state": "OPEN",
160
+ "url": "https://github.com/kontourai/flow-agents/issues/10",
161
+ "labels": []
162
+ }
163
+ }
164
+ ],
165
+ "open_issues": []
166
+ }
167
+ JSON
168
+
169
+ echo "=== Pull Work Board Ready Queue ==="
170
+
171
+ node "$SCRIPT" \
172
+ --settings-json "$TMPDIR_EVAL/settings.json" \
173
+ --items-json "$TMPDIR_EVAL/items.json" \
174
+ > "$TMPDIR_EVAL/board.json"
175
+ status=$?
176
+ [[ "$status" -eq 0 ]] && pass "board reader exits successfully" || fail "board reader exits successfully"
177
+
178
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "ready_queue.length")" == "3" ]] && pass "filters only ready board items" || fail "filters only ready board items"
179
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "ready_queue.0.id")" == "github:kontourai/flow-agents#5" ]] && pass "orders P0 before P1 and preserves board position tiebreak" || fail "orders P0 before P1 and preserves board position tiebreak"
180
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "ready_queue.1.id")" == "github:kontourai/flow-agents#7" ]] && pass "orders P1 before P2" || fail "orders P1 before P2"
181
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "ready_queue.2.id")" == "github:kontourai/flow-agents#9" ]] && pass "keeps P2 ready after higher priorities" || fail "keeps P2 ready after higher priorities"
182
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "ready_queue.0.status")" == "ready" ]] && pass "matches ready status case-insensitively" || fail "matches ready status case-insensitively"
183
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "ready_queue.0.board_membership.priority")" == "P0" ]] && pass "preserves board priority on candidate" || fail "preserves board priority on candidate"
184
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "intake_gaps.length")" == "1" ]] && pass "detects open WorkItemProvider issues absent from board" || fail "detects open WorkItemProvider issues absent from board"
185
+ [[ "$(json_query "$TMPDIR_EVAL/board.json" "intake_gaps.0.id")" == "github:kontourai/flow-agents#11" ]] && pass "reports missing issue as intake gap" || fail "reports missing issue as intake gap"
186
+
187
+ node "$SCRIPT" \
188
+ --settings-json "$TMPDIR_EVAL/settings.json" \
189
+ --items-json "$TMPDIR_EVAL/zero-ready.json" \
190
+ > "$TMPDIR_EVAL/zero-ready-output.json"
191
+ [[ "$(json_query "$TMPDIR_EVAL/zero-ready-output.json" "ready_queue.length")" == "0" ]] && pass "zero-ready fixture has empty ready queue" || fail "zero-ready fixture has empty ready queue"
192
+ [[ "$(json_query "$TMPDIR_EVAL/zero-ready-output.json" "warnings.0.code")" == "zero_ready_items" ]] && pass "zero ready emits loud warning" || fail "zero ready emits loud warning"
193
+
194
+ if [[ "$errors" -eq 0 ]]; then
195
+ echo "Pull work board checks passed"
196
+ else
197
+ echo "Pull work board checks failed: $errors"
198
+ fi
199
+
200
+ exit "$errors"
@@ -13,7 +13,7 @@ pass() { echo " ✓ $1"; }
13
13
  fail() { echo " ✗ $1"; errors=$((errors + 1)); }
14
14
 
15
15
  json_query() {
16
- node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=part==="length" ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]); console.log(cur);' "$1" "$2"
16
+ NO_COLOR=1 FORCE_COLOR=0 node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=part==="length" ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]); console.log(cur);' "$1" "$2"
17
17
  }
18
18
 
19
19
  echo "=== Pull Work Provider Normalization ==="