@kontourai/flow-agents 2.3.0 → 3.0.0

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 (301) hide show
  1. package/.github/CODEOWNERS +16 -0
  2. package/.github/workflows/ci.yml +44 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/AGENTS.md +4 -4
  5. package/CHANGELOG.md +62 -0
  6. package/CONTEXT.md +35 -0
  7. package/README.md +9 -3
  8. package/agents/dev.json +1 -1
  9. package/agents/tool-code-reviewer.json +1 -1
  10. package/agents/tool-planner.json +3 -3
  11. package/agents/tool-verifier.json +3 -3
  12. package/build/src/cli/assignment-provider.d.ts +1 -0
  13. package/build/src/cli/assignment-provider.js +748 -0
  14. package/build/src/cli/console-learning-projection.js +3 -2
  15. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  16. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  17. package/build/src/cli/init.js +104 -21
  18. package/build/src/cli/kit.js +37 -6
  19. package/build/src/cli/pull-work-provider.js +1 -1
  20. package/build/src/cli/usage-feedback.js +3 -3
  21. package/build/src/cli/validate-hook-influence.js +1 -0
  22. package/build/src/cli/validate-workflow-artifacts.js +27 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  24. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  25. package/build/src/cli/workflow-sidecar.js +938 -168
  26. package/build/src/cli.js +6 -0
  27. package/build/src/flow-kit/validate.d.ts +23 -0
  28. package/build/src/flow-kit/validate.js +67 -1
  29. package/build/src/index.d.ts +1 -1
  30. package/build/src/index.js +1 -1
  31. package/build/src/lib/flow-resolver.d.ts +6 -4
  32. package/build/src/lib/flow-resolver.js +125 -58
  33. package/build/src/lib/local-artifact-root.d.ts +14 -2
  34. package/build/src/lib/local-artifact-root.js +22 -5
  35. package/build/src/lib/workflow-learning-projection.js +2 -2
  36. package/build/src/runtime-adapters.d.ts +12 -0
  37. package/build/src/runtime-adapters.js +56 -16
  38. package/build/src/tools/build-universal-bundles.js +49 -16
  39. package/build/src/tools/generate-context-map.js +10 -8
  40. package/build/src/tools/validate-source-tree.js +24 -1
  41. package/console.telemetry.json +22 -0
  42. package/context/contracts/artifact-contract.md +40 -7
  43. package/context/contracts/assignment-provider-contract.md +239 -0
  44. package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
  45. package/context/contracts/decision-registry-contract.md +265 -0
  46. package/context/contracts/delivery-contract.md +3 -1
  47. package/context/contracts/execution-contract.md +25 -0
  48. package/context/contracts/governance-adapter-contract.md +2 -0
  49. package/context/contracts/knowledge-store-contract.md +197 -0
  50. package/context/contracts/planning-contract.md +4 -1
  51. package/context/contracts/review-contract.md +7 -3
  52. package/context/contracts/sandbox-policy.md +2 -0
  53. package/context/contracts/standing-directives.md +13 -0
  54. package/context/contracts/verification-contract.md +11 -1
  55. package/context/contracts/work-item-contract.md +3 -1
  56. package/context/deferred/parallelization.md +1 -1
  57. package/context/gate-awareness.md +4 -4
  58. package/context/scripts/git-status.sh +2 -2
  59. package/context/scripts/hooks/config-protection.js +502 -2
  60. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  61. package/context/scripts/hooks/run-hook.js +14 -1
  62. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  63. package/context/scripts/hooks/workflow-steering.js +212 -6
  64. package/context/settings/assignment-provider-settings.json +33 -0
  65. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  66. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  67. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  68. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  69. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  70. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
  71. package/docs/agent-system-guidebook.md +3 -3
  72. package/docs/agent-usage-feedback-loop.md +6 -6
  73. package/docs/context-map.md +10 -7
  74. package/docs/decisions/decision-records.md +57 -0
  75. package/docs/decisions/index.md +17 -0
  76. package/docs/decisions/knowledge-store-provider.md +51 -0
  77. package/docs/decisions/model-routing.md +63 -0
  78. package/docs/decisions/promotion-gate.md +52 -0
  79. package/docs/decisions/standing-directives.md +66 -0
  80. package/docs/developer-architecture.md +1 -1
  81. package/docs/fixture-ownership.md +4 -0
  82. package/docs/flow-kit-repository-contract.md +5 -5
  83. package/docs/getting-started.md +12 -2
  84. package/docs/integrations/harness-install.md +2 -1
  85. package/docs/kit-authoring-guide.md +64 -5
  86. package/docs/knowledge-kit.md +5 -1
  87. package/docs/learnings/2026-07-improvement-program.md +203 -0
  88. package/docs/migrations.md +6 -5
  89. package/docs/north-star.md +2 -2
  90. package/docs/operating-layers.md +4 -3
  91. package/docs/repository-structure.md +5 -5
  92. package/docs/skills-map.md +8 -8
  93. package/docs/spec/runtime-hook-surface.md +19 -15
  94. package/docs/spikes/graph-provider-2026-07.md +155 -0
  95. package/docs/standards-register.md +7 -7
  96. package/docs/trust-anchor-adoption.md +105 -4
  97. package/docs/work-item-adapters.md +1 -1
  98. package/docs/workflow-artifact-lifecycle.md +72 -9
  99. package/docs/workflow-eval-strategy.md +3 -3
  100. package/docs/workflow-shared-contracts.md +3 -2
  101. package/docs/workflow-usage-guide.md +260 -21
  102. package/evals/acceptance/DEMO-false-completion.md +20 -11
  103. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  104. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  105. package/evals/acceptance/test_kiro_harness.sh +1 -1
  106. package/evals/ci/antigaming-suite.sh +5 -0
  107. package/evals/ci/run-baseline.sh +71 -1
  108. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  109. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  110. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  111. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  112. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  113. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  114. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  115. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  116. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  117. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  118. package/evals/fixtures/hook-influence/cases.json +32 -0
  119. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  120. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  121. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  122. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  123. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  124. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  125. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  126. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  127. package/evals/integration/test_actor_identity.sh +254 -0
  128. package/evals/integration/test_assignment_provider_github.sh +318 -0
  129. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  130. package/evals/integration/test_builder_step_producers.sh +33 -23
  131. package/evals/integration/test_bundle_install.sh +22 -16
  132. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  133. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  134. package/evals/integration/test_checkpoint_signing.sh +43 -0
  135. package/evals/integration/test_claim_lookup.sh +4 -4
  136. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  137. package/evals/integration/test_command_log_concurrency.sh +8 -8
  138. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  139. package/evals/integration/test_command_log_integrity.sh +16 -16
  140. package/evals/integration/test_console_learning_projection.sh +1 -1
  141. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  142. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  143. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  144. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  145. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  146. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  147. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  148. package/evals/integration/test_flow_kit_repository.sh +2 -0
  149. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  150. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  151. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  152. package/evals/integration/test_gate_lockdown.sh +97 -73
  153. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  154. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  155. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  156. package/evals/integration/test_goal_fit_hook.sh +32 -32
  157. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  158. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  159. package/evals/integration/test_hook_influence_cases.sh +1 -0
  160. package/evals/integration/test_install_merge.sh +262 -1
  161. package/evals/integration/test_kit_identity_trust.sh +6 -6
  162. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  163. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  164. package/evals/integration/test_liveness_verdict.sh +394 -0
  165. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  166. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  167. package/evals/integration/test_promote_gate.sh +178 -0
  168. package/evals/integration/test_publish_delivery.sh +23 -6
  169. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  170. package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
  171. package/evals/integration/test_pull_work_provider.sh +2 -2
  172. package/evals/integration/test_reconcile_soundness.sh +37 -13
  173. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  174. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  175. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  176. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  177. package/evals/integration/test_trust_reconcile.sh +11 -10
  178. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  179. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  180. package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
  181. package/evals/integration/test_usage_feedback_global.sh +5 -5
  182. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  183. package/evals/integration/test_verify_cli.sh +2 -2
  184. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  185. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  186. package/evals/integration/test_workflow_artifacts.sh +33 -0
  187. package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
  188. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  189. package/evals/lib/codex-provider.sh +1 -1
  190. package/evals/lib/node.sh +19 -1
  191. package/evals/run.sh +32 -0
  192. package/evals/static/test_decisions.sh +150 -0
  193. package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
  194. package/evals/static/test_knowledge_providers.sh +23 -0
  195. package/evals/static/test_package.sh +19 -10
  196. package/evals/static/test_universal_bundles.sh +48 -3
  197. package/evals/static/test_workflow_skills.sh +41 -8
  198. package/kits/builder/flows/build.flow.json +3 -74
  199. package/kits/builder/flows/publish-learn.flow.json +90 -0
  200. package/kits/builder/kit.json +11 -0
  201. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  202. package/kits/builder/skills/deliver/SKILL.md +32 -11
  203. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  204. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  205. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  206. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  207. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  208. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  209. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  210. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  211. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  212. package/kits/builder/skills/pull-work/SKILL.md +148 -5
  213. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  214. package/kits/builder/skills/review-work/SKILL.md +24 -1
  215. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  216. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  217. package/kits/catalog.json +6 -0
  218. package/kits/knowledge/kit.json +35 -0
  219. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  220. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  221. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  222. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  223. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  224. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  225. package/kits/knowledge/providers/conformance/suite.test.js +125 -0
  226. package/kits/knowledge/providers/git-repo/index.js +236 -0
  227. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  228. package/kits/knowledge/providers/health/index.js +153 -0
  229. package/kits/knowledge/providers/index.js +24 -0
  230. package/kits/knowledge/providers/lib/model.js +91 -0
  231. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  232. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  233. package/kits/knowledge/providers/work-item/index.js +204 -0
  234. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  235. package/kits/veritas-governance/docs/README.md +75 -0
  236. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  237. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  238. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  239. package/kits/veritas-governance/kit.json +16 -0
  240. package/package.json +11 -5
  241. package/packaging/README.md +2 -2
  242. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  243. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  244. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  245. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  246. package/packaging/manifest.json +9 -10
  247. package/schemas/assignment-provider-settings.schema.json +125 -0
  248. package/schemas/decision-record.schema.json +109 -0
  249. package/schemas/knowledge/edge.schema.json +54 -0
  250. package/schemas/knowledge/health-report.schema.json +45 -0
  251. package/schemas/knowledge/node.schema.json +49 -0
  252. package/schemas/knowledge/proposal.schema.json +53 -0
  253. package/schemas/workflow-evidence.schema.json +1 -1
  254. package/schemas/workflow-state.schema.json +5 -0
  255. package/scripts/README.md +8 -3
  256. package/scripts/check-content-boundary.cjs +1 -1
  257. package/scripts/check-decisions.cjs +356 -0
  258. package/scripts/ci/derive-claim-status.mjs +74 -0
  259. package/scripts/ci/trust-reconcile.js +957 -103
  260. package/scripts/git-status.sh +2 -2
  261. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  262. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  263. package/scripts/hooks/config-protection.js +102 -30
  264. package/scripts/hooks/evidence-capture.js +3 -3
  265. package/scripts/hooks/lib/actor-identity.js +368 -0
  266. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  267. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  268. package/scripts/hooks/lib/liveness-policy.js +127 -0
  269. package/scripts/hooks/lib/liveness-read.js +105 -12
  270. package/scripts/hooks/lib/liveness-write.js +46 -0
  271. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  272. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  273. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  274. package/scripts/hooks/stop-goal-fit.js +157 -26
  275. package/scripts/hooks/workflow-steering.js +77 -3
  276. package/scripts/install-codex-home.sh +138 -38
  277. package/scripts/statusline/flow-agents-statusline.js +2 -2
  278. package/src/cli/assignment-provider.ts +845 -0
  279. package/src/cli/console-learning-projection.ts +3 -2
  280. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  281. package/src/cli/init.ts +101 -21
  282. package/src/cli/kit.ts +37 -6
  283. package/src/cli/public-api.test.mjs +58 -6
  284. package/src/cli/pull-work-provider.ts +1 -1
  285. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  286. package/src/cli/usage-feedback.ts +3 -3
  287. package/src/cli/validate-hook-influence.ts +1 -0
  288. package/src/cli/validate-workflow-artifacts.ts +27 -7
  289. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  290. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  291. package/src/cli/workflow-sidecar.ts +933 -168
  292. package/src/cli.ts +6 -0
  293. package/src/flow-kit/validate.ts +74 -1
  294. package/src/index.ts +6 -2
  295. package/src/lib/flow-resolver.ts +123 -55
  296. package/src/lib/local-artifact-root.ts +24 -5
  297. package/src/lib/workflow-learning-projection.ts +2 -2
  298. package/src/runtime-adapters.ts +61 -15
  299. package/src/tools/build-universal-bundles.ts +49 -16
  300. package/src/tools/generate-context-map.ts +10 -8
  301. package/src/tools/validate-source-tree.ts +24 -1
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env bash
2
+ # test_pull_work_liveness_preflight.sh — integration eval for the pull-work liveness
3
+ # selection preflight (issue #166, extended by ADR 0021 §1/§3; plan artifact at
4
+ # .kontourai/flow-agents/kontourai-flow-agents-166/kontourai-flow-agents-166--plan-work.md,
5
+ # Wave 2). Deterministic, no model spend, self-cleaning (mktemp -d + trap EXIT).
6
+ #
7
+ # Covers, per the plan's Wave 2 eval tasks (writer-shape unit checks + two-actor CLI-level
8
+ # exclusion simulation + static skill-text assertions, folded into one file):
9
+ # A. `liveness whoami` / `resolve-slug` CLI unit shapes (AC7):
10
+ # derived-actor fallback (ancestry/env chain), explicit --actor override, forced-unresolved
11
+ # non-throwing shape (contrasted with `liveness claim`'s still-fail-loud behavior), and
12
+ # resolve-slug's deterministic slug format + malformed-ref rejection (proving no duplicate
13
+ # validation logic exists outside workItemSlug()).
14
+ # B. resolve-slug parity with the workItemSlug()-derived `ensure-session --work-item` session
15
+ # directory (AC4).
16
+ # C. The deterministic two-actor CLI-level classification simulation (AC1, AC2, AC3, AC5):
17
+ # held (other-actor verified) / mine (self-actor verified) / reclaimable (stale, raw status
18
+ # not label) / free (no rows) / claim-emit-on-selection / force-write unconstrained at the
19
+ # CLI layer (Design Decision 4 — no holder-conflict check to override).
20
+ # D. Static skill-text require_text/reject_text assertions against the plan's prescribed
21
+ # sentences in kits/builder/skills/pull-work/SKILL.md and
22
+ # kits/builder/skills/pickup-probe/SKILL.md (AC6, AC8), including that the existing
23
+ # no-provider-mutation sentence is present and unchanged.
24
+ #
25
+ # Usage: bash evals/integration/test_pull_work_liveness_preflight.sh
26
+
27
+ set -uo pipefail
28
+
29
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
30
+ source "$ROOT/evals/lib/node.sh"
31
+
32
+ WRITER="workflow-sidecar"
33
+ SIDECAR_SRC="$ROOT/src/cli/workflow-sidecar.ts"
34
+ PULL="$ROOT/kits/builder/skills/pull-work/SKILL.md"
35
+ PICKUP_PROBE="$ROOT/kits/builder/skills/pickup-probe/SKILL.md"
36
+
37
+ for m in "$SIDECAR_SRC" "$PULL" "$PICKUP_PROBE"; do
38
+ if [[ ! -f "$m" ]]; then
39
+ echo "pull-work liveness preflight eval skipped: $m does not exist yet." >&2
40
+ exit 1
41
+ fi
42
+ done
43
+
44
+ TMPDIR_EVAL="$(mktemp -d)"
45
+ trap 'rm -rf "$TMPDIR_EVAL"' EXIT
46
+
47
+ errors=0
48
+ _pass() { echo " ✓ $1"; }
49
+ _fail() { echo " ✗ $1"; errors=$((errors + 1)); }
50
+
51
+ require_text() {
52
+ local path="$1" pattern="$2" label="$3"
53
+ if rg -q -- "$pattern" "$path"; then _pass "$label"; else _fail "$label"; fi
54
+ }
55
+
56
+ reject_text() {
57
+ local path="$1" pattern="$2" label="$3"
58
+ if rg -q -- "$pattern" "$path"; then _fail "$label"; else _pass "$label"; fi
59
+ }
60
+
61
+ # new_scratch — fresh scratch artifact-root under the eval's own tmpdir.
62
+ new_scratch() {
63
+ mktemp -d "$TMPDIR_EVAL/scratch-XXXXXX"
64
+ }
65
+
66
+ echo "=== Pull work liveness preflight (#166) ==="
67
+
68
+ # ─── A. `liveness whoami` / `resolve-slug` CLI unit shapes (AC7) ────────────
69
+ echo "--- A. whoami / resolve-slug unit shapes ---"
70
+
71
+ A_ROOT="$(new_scratch)/.kontourai/flow-agents"
72
+
73
+ # A1: derived-actor fallback — no explicit --actor/--json flag beyond a CLAUDE_CODE_SESSION_ID
74
+ # env override (mirrors the two-holders env-injection pattern from
75
+ # evals/integration/test_workflow_sidecar_writer.sh) — returns a JSON object with non-empty
76
+ # actor/source fields, exit 0.
77
+ A1_OUT="$(CLAUDE_CODE_SESSION_ID=sess-a flow_agents_node "$WRITER" liveness whoami --json --artifact-root "$A_ROOT" 2>"$TMPDIR_EVAL/a1.err")"
78
+ A1_STATUS=$?
79
+ if [[ "$A1_STATUS" -eq 0 ]] && node -e '
80
+ const o = JSON.parse(process.argv[1]);
81
+ if (typeof o.actor !== "string" || !o.actor) throw new Error("actor missing/empty");
82
+ if (typeof o.source !== "string" || !o.source) throw new Error("source missing/empty");
83
+ ' "$A1_OUT" 2>"$TMPDIR_EVAL/a1-check.err"; then
84
+ _pass "liveness whoami --json (derived-actor fallback) exits 0 with non-empty actor/source (AC7)"
85
+ else
86
+ _fail "liveness whoami derived-actor fallback did not return the expected shape: out=$A1_OUT status=$A1_STATUS err=$(cat "$TMPDIR_EVAL/a1.err" "$TMPDIR_EVAL/a1-check.err" 2>/dev/null)"
87
+ fi
88
+
89
+ # A2: explicit --actor override, after sanitizeSegment.
90
+ A2_OUT="$(flow_agents_node "$WRITER" liveness whoami --actor custom-name --json --artifact-root "$A_ROOT" 2>"$TMPDIR_EVAL/a2.err")"
91
+ if [[ "$A2_OUT" == '{"actor":"custom-name","source":"explicit-override"}' ]]; then
92
+ _pass "liveness whoami --actor custom-name --json returns {actor, source: explicit-override} (AC7)"
93
+ else
94
+ _fail "liveness whoami --actor override mismatch: out=$A2_OUT err=$(cat "$TMPDIR_EVAL/a2.err")"
95
+ fi
96
+
97
+ # A3: forced-unresolved actor — whoami never dies / never exits nonzero (read-only, advisory,
98
+ # non-enforcing contract), even though the actor could not be resolved.
99
+ if A3_OUT="$(FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1 NODE_ENV=test flow_agents_node "$WRITER" liveness whoami --json --artifact-root "$A_ROOT" 2>"$TMPDIR_EVAL/a3.err")" \
100
+ && [[ "$A3_OUT" == '{"actor":"","source":"test-forced-unresolved"}' ]]; then
101
+ _pass "liveness whoami never dies under a forced-unresolved actor — prints {actor:\"\", source:\"test-forced-unresolved\"} and exits 0 (AC7, read-only contract)"
102
+ else
103
+ _fail "liveness whoami under forced-unresolved actor did not behave as expected: out=$A3_OUT err=$(cat "$TMPDIR_EVAL/a3.err")"
104
+ fi
105
+
106
+ # A4: contrast — `liveness claim` under the SAME forced-unresolved env still fails loud (dies),
107
+ # proving the enforcement point stays at the write path, not whoami.
108
+ if FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1 NODE_ENV=test flow_agents_node "$WRITER" liveness claim forced-unresolved-preflight-subj --artifact-root "$A_ROOT" >"$TMPDIR_EVAL/a4.out" 2>"$TMPDIR_EVAL/a4.err"; then
109
+ _fail "liveness claim under a forced-unresolved actor should have exited nonzero (contrast with whoami)"
110
+ elif rg -q -- '--actor' "$TMPDIR_EVAL/a4.err" && rg -q -- 'FLOW_AGENTS_ACTOR' "$TMPDIR_EVAL/a4.err"; then
111
+ _pass "liveness claim still fails loud (nonzero exit, remediation naming --actor/FLOW_AGENTS_ACTOR) under the same forced-unresolved env whoami tolerates (AC7 contrast)"
112
+ else
113
+ _fail "liveness claim forced-unresolved rejection lacked expected remediation: $(cat "$TMPDIR_EVAL/a4.out" "$TMPDIR_EVAL/a4.err")"
114
+ fi
115
+
116
+ # A5: resolve-slug deterministic slug format.
117
+ A5_OUT="$(flow_agents_node "$WRITER" resolve-slug 'kontourai/flow-agents#166' 2>"$TMPDIR_EVAL/a5.err")"
118
+ A5_STATUS=$?
119
+ if [[ "$A5_STATUS" -eq 0 ]] && [[ "$A5_OUT" == "kontourai-flow-agents-166" ]]; then
120
+ _pass "resolve-slug kontourai/flow-agents#166 prints kontourai-flow-agents-166 and exits 0 (AC4, AC7)"
121
+ else
122
+ _fail "resolve-slug deterministic format mismatch: out=$A5_OUT status=$A5_STATUS err=$(cat "$TMPDIR_EVAL/a5.err")"
123
+ fi
124
+
125
+ # A6: malformed ref (no #id) is rejected with workItemSlug()'s existing message — proves no
126
+ # duplicate validation logic was introduced for resolve-slug.
127
+ if flow_agents_node "$WRITER" resolve-slug 'owner/repo' >"$TMPDIR_EVAL/a6.out" 2>"$TMPDIR_EVAL/a6.err"; then
128
+ _fail "resolve-slug should reject a ref with no # separator"
129
+ elif rg -q -- 'owner/repo#id format' "$TMPDIR_EVAL/a6.err"; then
130
+ _pass "resolve-slug rejects a ref with no # separator using workItemSlug()'s existing message (AC7)"
131
+ else
132
+ _fail "resolve-slug malformed-ref rejection message mismatch: $(cat "$TMPDIR_EVAL/a6.out" "$TMPDIR_EVAL/a6.err")"
133
+ fi
134
+
135
+ # A7: non-numeric id is rejected with workItemSlug()'s existing message.
136
+ if flow_agents_node "$WRITER" resolve-slug 'owner/repo#abc' >"$TMPDIR_EVAL/a7.out" 2>"$TMPDIR_EVAL/a7.err"; then
137
+ _fail "resolve-slug should reject a non-numeric issue id"
138
+ elif rg -q -- 'numeric issue number' "$TMPDIR_EVAL/a7.err"; then
139
+ _pass "resolve-slug rejects a non-numeric issue id using workItemSlug()'s existing message (AC7)"
140
+ else
141
+ _fail "resolve-slug non-numeric-id rejection message mismatch: $(cat "$TMPDIR_EVAL/a7.out" "$TMPDIR_EVAL/a7.err")"
142
+ fi
143
+
144
+ # ─── B. resolve-slug parity with ensure-session --work-item session dir (AC4) ───
145
+ echo "--- B. resolve-slug parity with ensure-session --work-item ---"
146
+
147
+ B_ROOT="$(new_scratch)/.kontourai/flow-agents"
148
+ if flow_agents_node "$WRITER" ensure-session \
149
+ --artifact-root "$B_ROOT" \
150
+ --work-item 'owner/repo#42' \
151
+ --title "Parity Check" \
152
+ --summary "resolve-slug parity fixture." \
153
+ --timestamp "2026-07-01T00:00:00Z" >"$TMPDIR_EVAL/b-ensure.out" 2>"$TMPDIR_EVAL/b-ensure.err"; then
154
+ B_SESSION_DIR="$(find "$B_ROOT" -mindepth 1 -maxdepth 1 -type d ! -name liveness | head -n1)"
155
+ B_SLUG_FROM_DIR="$(basename "$B_SESSION_DIR")"
156
+ B_SLUG_FROM_RESOLVE="$(flow_agents_node "$WRITER" resolve-slug 'owner/repo#42' 2>"$TMPDIR_EVAL/b-resolve.err")"
157
+ if [[ -n "$B_SESSION_DIR" ]] && [[ "$B_SLUG_FROM_DIR" == "$B_SLUG_FROM_RESOLVE" ]]; then
158
+ _pass "resolve-slug owner/repo#42 ($B_SLUG_FROM_RESOLVE) matches the ensure-session --work-item session directory name ($B_SLUG_FROM_DIR) (AC4)"
159
+ else
160
+ _fail "resolve-slug/ensure-session session-dir parity mismatch: dir=$B_SLUG_FROM_DIR resolve-slug=$B_SLUG_FROM_RESOLVE err=$(cat "$TMPDIR_EVAL/b-resolve.err")"
161
+ fi
162
+ else
163
+ _fail "ensure-session --work-item owner/repo#42 fixture setup failed: $(cat "$TMPDIR_EVAL/b-ensure.out" "$TMPDIR_EVAL/b-ensure.err")"
164
+ fi
165
+
166
+ # ─── C. Two-actor CLI-level classification simulation (AC1, AC2, AC3, AC5) ──
167
+ echo "--- C. Two-actor classification simulation ---"
168
+
169
+ C_ROOT="$(new_scratch)/.kontourai/flow-agents"
170
+
171
+ # status_row_json <root> <subjectId> <now_iso> — prints the raw `liveness status --json --subject`
172
+ # array for the given subject (the preflight's exact read shape: full row set, one call).
173
+ status_row_json() {
174
+ local root="$1" subject="$2" now="$3"
175
+ flow_agents_node "$WRITER" liveness status --json --subject "$subject" --now "$now" --artifact-root "$root"
176
+ }
177
+
178
+ # C1/C2: Held-exclusion + own-actor mine — actor agent-a claims subject subj-held.
179
+ flow_agents_node "$WRITER" liveness claim subj-held --actor agent-a --at "2026-07-01T12:00:00Z" --ttl 1800 --artifact-root "$C_ROOT" >/dev/null 2>"$TMPDIR_EVAL/c-claim-a.err"
180
+ C1_STATUS="$(status_row_json "$C_ROOT" subj-held "2026-07-01T12:10:00Z")"
181
+ C1_ROW_ACTOR="$(printf '%s' "$C1_STATUS" | node -e 'const rows=JSON.parse(require("fs").readFileSync(0,"utf8")); process.stdout.write(rows[0] ? rows[0].actor : "");')"
182
+ C1_ROW_STATUS="$(printf '%s' "$C1_STATUS" | node -e 'const rows=JSON.parse(require("fs").readFileSync(0,"utf8")); process.stdout.write(rows[0] ? rows[0].status : "");')"
183
+ C1_WHOAMI_B="$(flow_agents_node "$WRITER" liveness whoami --actor agent-b --json --artifact-root "$C_ROOT" | node -e 'const o=JSON.parse(require("fs").readFileSync(0,"utf8")); process.stdout.write(o.actor);')"
184
+ C1_WHOAMI_A="$(flow_agents_node "$WRITER" liveness whoami --actor agent-a --json --artifact-root "$C_ROOT" | node -e 'const o=JSON.parse(require("fs").readFileSync(0,"utf8")); process.stdout.write(o.actor);')"
185
+
186
+ if [[ "$C1_ROW_STATUS" == "verified" ]] && [[ "$C1_ROW_ACTOR" == "agent-a" ]] && [[ "$C1_ROW_ACTOR" != "$C1_WHOAMI_B" ]]; then
187
+ _pass "held-exclusion: a verified row for agent-a, read as agent-b's whoami, classifies subj-held as held (row actor != self) (AC1, AC2)"
188
+ else
189
+ _fail "held-exclusion classification failed: row_status=$C1_ROW_STATUS row_actor=$C1_ROW_ACTOR whoami_b=$C1_WHOAMI_B raw=$C1_STATUS"
190
+ fi
191
+
192
+ if [[ "$C1_ROW_STATUS" == "verified" ]] && [[ "$C1_ROW_ACTOR" == "agent-a" ]] && [[ "$C1_ROW_ACTOR" == "$C1_WHOAMI_A" ]]; then
193
+ _pass "own-actor mine: the SAME verified row, read as agent-a's own whoami, classifies subj-held as mine, not held (AC5)"
194
+ else
195
+ _fail "own-actor mine classification failed: row_status=$C1_ROW_STATUS row_actor=$C1_ROW_ACTOR whoami_a=$C1_WHOAMI_A raw=$C1_STATUS"
196
+ fi
197
+
198
+ # C3: Reclaimable — actor agent-c's claim is seeded far enough in the past (beyond ttlSeconds)
199
+ # that `--now` places it past TTL expiry. Assert the row's RAW `status` field reads `stale`
200
+ # (never `label`, which collapses stale to the coarser "free" and would lose the distinction —
201
+ # proving the "consume status, not label" design decision is real and observable).
202
+ flow_agents_node "$WRITER" liveness claim subj-stale --actor agent-c --at "2026-06-25T11:00:00Z" --ttl 1800 --artifact-root "$C_ROOT" >/dev/null 2>"$TMPDIR_EVAL/c-claim-c.err"
203
+ C3_STATUS="$(status_row_json "$C_ROOT" subj-stale "2026-07-01T12:10:00Z")"
204
+ C3_ROW_STATUS="$(printf '%s' "$C3_STATUS" | node -e 'const rows=JSON.parse(require("fs").readFileSync(0,"utf8")); process.stdout.write(rows[0] ? rows[0].status : "");')"
205
+ C3_ROW_LABEL="$(printf '%s' "$C3_STATUS" | node -e 'const rows=JSON.parse(require("fs").readFileSync(0,"utf8")); process.stdout.write(rows[0] ? rows[0].label : "");')"
206
+ if [[ "$C3_ROW_STATUS" == "stale" ]] && [[ "$C3_ROW_LABEL" == "free" ]]; then
207
+ _pass "reclaimable: raw status field reads 'stale' (distinct from the coarser label 'free') — proves 'consume status, not label' is real, not just documented (AC3)"
208
+ else
209
+ _fail "reclaimable classification failed: raw status=$C3_ROW_STATUS label=$C3_ROW_LABEL raw=$C3_STATUS"
210
+ fi
211
+
212
+ # C4: Free — a subject with no liveness events at all returns an empty row set.
213
+ C4_STATUS="$(status_row_json "$C_ROOT" subj-never-claimed "2026-07-01T12:10:00Z")"
214
+ if [[ "$C4_STATUS" == "[]" ]]; then
215
+ _pass "free: a never-claimed subject returns an empty row set for liveness status --json --subject (AC1)"
216
+ else
217
+ _fail "free-case status was not empty: $C4_STATUS"
218
+ fi
219
+
220
+ # C5: Claim-emit-on-selection — subjectId derived via resolve-slug; a "selection" runs
221
+ # `liveness claim <subjectId>` exactly once, appending exactly one new claim event for that
222
+ # (subjectId, actor) pair, and a subsequent status read shows that actor verified/held.
223
+ C5_SUBJECT_ID="$(flow_agents_node "$WRITER" resolve-slug 'owner/repo#77' 2>"$TMPDIR_EVAL/c5-resolve.err")"
224
+ C5_STREAM="$C_ROOT/liveness/events.jsonl"
225
+ C5_LINES_BEFORE=0
226
+ [[ -f "$C5_STREAM" ]] && C5_LINES_BEFORE="$(wc -l <"$C5_STREAM" | tr -d ' ')"
227
+ flow_agents_node "$WRITER" liveness claim "$C5_SUBJECT_ID" --actor agent-selector --artifact-root "$C_ROOT" >/dev/null 2>"$TMPDIR_EVAL/c5-claim.err"
228
+ C5_LINES_AFTER="$(wc -l <"$C5_STREAM" | tr -d ' ')"
229
+ C5_APPENDED=$((C5_LINES_AFTER - C5_LINES_BEFORE))
230
+ C5_STATUS_AFTER="$(status_row_json "$C_ROOT" "$C5_SUBJECT_ID" "2026-07-01T12:10:00Z")"
231
+ C5_ROW_STATUS_AFTER="$(printf '%s' "$C5_STATUS_AFTER" | node -e 'const rows=JSON.parse(require("fs").readFileSync(0,"utf8")); const r=rows.find(x=>x.actor==="agent-selector"); process.stdout.write(r ? r.status : "");')"
232
+ if [[ "$C5_SUBJECT_ID" == "owner-repo-77" ]] && [[ "$C5_APPENDED" -eq 1 ]] && [[ "$C5_ROW_STATUS_AFTER" == "verified" ]]; then
233
+ _pass "claim-emit-on-selection: liveness claim <resolve-slug-derived subjectId> appends exactly one event and shows verified/held for that actor thereafter (AC4)"
234
+ else
235
+ _fail "claim-emit-on-selection failed: subjectId=$C5_SUBJECT_ID appended=$C5_APPENDED row_status_after=$C5_ROW_STATUS_AFTER raw=$C5_STATUS_AFTER"
236
+ fi
237
+
238
+ # C6: Force-write unconstrained at the CLI layer — `liveness claim` on subj-held (already held
239
+ # fresh by agent-a, per C1/C2) succeeds unconditionally for a different actor. Proves --force
240
+ # needs no CLI-side change: the write path has no holder-conflict check to override (Design
241
+ # Decision 4).
242
+ if flow_agents_node "$WRITER" liveness claim subj-held --actor agent-force --artifact-root "$C_ROOT" >"$TMPDIR_EVAL/c6.out" 2>"$TMPDIR_EVAL/c6.err"; then
243
+ C6_STATUS="$(status_row_json "$C_ROOT" subj-held "2026-07-01T12:15:00Z")"
244
+ C6_BOTH_HELD="$(printf '%s' "$C6_STATUS" | node -e '
245
+ const rows = JSON.parse(require("fs").readFileSync(0, "utf8"));
246
+ const a = rows.find((r) => r.actor === "agent-a");
247
+ const f = rows.find((r) => r.actor === "agent-force");
248
+ process.stdout.write(a && f && a.status === "verified" && f.status === "verified" ? "yes" : "no");
249
+ ')"
250
+ if [[ "$C6_BOTH_HELD" == "yes" ]]; then
251
+ _pass "force-write unconstrained: liveness claim on an already-held subject succeeds unconditionally for a different actor (no CLI-side holder-conflict check) (Design Decision 4)"
252
+ else
253
+ _fail "force-write left an unexpected status row set: $C6_STATUS"
254
+ fi
255
+ else
256
+ _fail "liveness claim on an already-held subject unexpectedly failed (CLI should have no holder-conflict check): $(cat "$TMPDIR_EVAL/c6.out" "$TMPDIR_EVAL/c6.err")"
257
+ fi
258
+
259
+ # ─── E. F1 (fix-plan iteration 1) — liveness whoami never touches the workflow-sidecar
260
+ # lock, regardless of artifact-root state (mirrors resolve-slug's empty lockRoot) ──────────
261
+ echo "--- E. F1: liveness whoami lock-bypass proof ---"
262
+
263
+ # E1: existing artifact root + FLOW_AGENTS_WORKFLOW_SIDECAR_LOCK_DELAY set -> whoami never
264
+ # creates .workflow-sidecar.lockdir (polled live during the delay window, not just checked
265
+ # post-hoc — a post-hoc-only check could not distinguish "never locked" from "locked, delayed,
266
+ # then cleaned up before we looked") -> exits 0.
267
+ E1_ROOT="$(new_scratch)/.kontourai/flow-agents"
268
+ mkdir -p "$E1_ROOT"
269
+ FLOW_AGENTS_WORKFLOW_SIDECAR_LOCK_DELAY=3 flow_agents_node "$WRITER" liveness whoami --json --artifact-root "$E1_ROOT" >"$TMPDIR_EVAL/e1.out" 2>"$TMPDIR_EVAL/e1.err" &
270
+ E1_PID=$!
271
+ E1_LOCKDIR_SEEN="no"
272
+ E1_POLL_DEADLINE=$(( $(date +%s%N) + 1500000000 ))
273
+ while [[ "$(date +%s%N)" -lt "$E1_POLL_DEADLINE" ]]; do
274
+ if [[ -e "$E1_ROOT/.workflow-sidecar.lockdir" ]]; then E1_LOCKDIR_SEEN="yes"; break; fi
275
+ if ! kill -0 "$E1_PID" 2>/dev/null; then break; fi
276
+ sleep 0.05
277
+ done
278
+ wait "$E1_PID"
279
+ E1_STATUS=$?
280
+ if [[ "$E1_STATUS" -eq 0 ]] && [[ "$E1_LOCKDIR_SEEN" == "no" ]]; then
281
+ _pass "liveness whoami against an EXISTING artifact root with FLOW_AGENTS_WORKFLOW_SIDECAR_LOCK_DELAY set never creates .workflow-sidecar.lockdir (polled live) and exits 0 (F1)"
282
+ else
283
+ _fail "liveness whoami lock-bypass failed: status=$E1_STATUS lockdir_seen=$E1_LOCKDIR_SEEN out=$(cat "$TMPDIR_EVAL/e1.out") err=$(cat "$TMPDIR_EVAL/e1.err")"
284
+ fi
285
+
286
+ # E2: existing but non-writable (555) artifact root -> whoami still exits 0 (no lock attempt,
287
+ # so no mkdir-under-a-read-only-dir failure is even possible).
288
+ E2_ROOT="$(new_scratch)/.kontourai/flow-agents"
289
+ mkdir -p "$E2_ROOT"
290
+ chmod 555 "$E2_ROOT"
291
+ E2_STATUS=0
292
+ flow_agents_node "$WRITER" liveness whoami --json --artifact-root "$E2_ROOT" >"$TMPDIR_EVAL/e2.out" 2>"$TMPDIR_EVAL/e2.err" || E2_STATUS=$?
293
+ chmod 755 "$E2_ROOT"
294
+ if [[ "$E2_STATUS" -eq 0 ]]; then
295
+ _pass "liveness whoami against a non-writable (555) EXISTING artifact root still exits 0 (F1)"
296
+ else
297
+ _fail "liveness whoami under a 555 artifact root unexpectedly failed: status=$E2_STATUS out=$(cat "$TMPDIR_EVAL/e2.out") err=$(cat "$TMPDIR_EVAL/e2.err")"
298
+ fi
299
+
300
+ # E3: contrast — `liveness status` (out of scope for F1, pre-existing lock behavior preserved)
301
+ # still acquires the lock against an existing root with the delay set. Proves F1's bypass is
302
+ # scoped to the `whoami` action only, not a blanket change to `liveness` command lock routing.
303
+ E3_ROOT="$(new_scratch)/.kontourai/flow-agents"
304
+ mkdir -p "$E3_ROOT"
305
+ FLOW_AGENTS_WORKFLOW_SIDECAR_LOCK_DELAY=1.5 flow_agents_node "$WRITER" liveness status --json --artifact-root "$E3_ROOT" >"$TMPDIR_EVAL/e3.out" 2>"$TMPDIR_EVAL/e3.err" &
306
+ E3_PID=$!
307
+ E3_LOCKDIR_SEEN="no"
308
+ E3_POLL_DEADLINE=$(( $(date +%s%N) + 1200000000 ))
309
+ while [[ "$(date +%s%N)" -lt "$E3_POLL_DEADLINE" ]]; do
310
+ if [[ -e "$E3_ROOT/.workflow-sidecar.lockdir" ]]; then E3_LOCKDIR_SEEN="yes"; break; fi
311
+ if ! kill -0 "$E3_PID" 2>/dev/null; then break; fi
312
+ sleep 0.05
313
+ done
314
+ wait "$E3_PID"
315
+ if [[ "$E3_LOCKDIR_SEEN" == "yes" ]]; then
316
+ _pass "liveness status (out of scope for F1) still acquires .workflow-sidecar.lockdir against an existing root — proves F1's bypass is action-scoped to whoami, not a blanket liveness-command change"
317
+ else
318
+ _fail "liveness status unexpectedly did not acquire the lock — F1's bypass may have leaked beyond the whoami action: out=$(cat "$TMPDIR_EVAL/e3.out") err=$(cat "$TMPDIR_EVAL/e3.err")"
319
+ fi
320
+
321
+ # ─── D. Static skill-text assertions (AC6, AC8) ─────────────────────────────
322
+ echo "--- D. Static skill-text assertions ---"
323
+
324
+ require_text "$PULL" '### 1a\. Liveness Selection Preflight' "pull-work documents the Liveness Selection Preflight subsection (AC1, AC8)"
325
+ require_text "$PULL" 'liveness whoami --json' "pull-work references liveness whoami --json by exact command name (AC1, AC8)"
326
+ require_text "$PULL" 'liveness status --json' "pull-work references liveness status --json by exact command name (AC1, AC8)"
327
+ require_text "$PULL" 'resolve-slug <owner>/<repo>#<issue-number>' "pull-work references resolve-slug by exact command name (AC1, AC4, AC8)"
328
+ require_text "$PULL" 'liveness claim <subjectId>' "pull-work references liveness claim <subjectId> by exact command name (AC4, AC8)"
329
+ require_text "$PULL" 'reading each row.s raw .status. field \(never .label.\)' "pull-work instructs consuming raw status, never label (AC1, AC8)"
330
+ require_text "$PULL" 'a .verified. row for an actor other than self.*.held.' "pull-work documents held classification (AC1, AC2, AC8)"
331
+ require_text "$PULL" 'excluded from the ready set by default' "pull-work excludes held candidates by default (AC2, AC8)"
332
+ require_text "$PULL" 'effective_state: .reclaimable.' "pull-work documents reclaimable classification via the assignment-provider join (AC1, AC3, AC8, #290)"
333
+ require_text "$PULL" 'requires an explicit recorded opt-in' "pull-work requires an explicit recorded opt-in for reclaimable selection (AC3, AC8)"
334
+ require_text "$PULL" 'never a silent normal pick' "pull-work states reclaimable is never a silent normal pick (AC3, AC8)"
335
+ require_text "$PULL" 'a .verified. row for the resolved self actor.*.mine.' "pull-work documents mine classification (AC1, AC5, AC8)"
336
+ require_text "$PULL" 'do not re-offer as new, do not exclude as held-by-other' "pull-work folds own-actor mine into WIP logic, not re-offered or re-excluded (AC5, AC8)"
337
+ require_text "$PULL" 'An explicit user instruction to proceed despite a .held. or .reclaimable. classification' "pull-work documents the --force/explicit-instruction override (AC2, AC3, AC8)"
338
+ require_text "$PULL" '.--force.' "pull-work names --force as the override mechanism (AC2, AC8)"
339
+ require_text "$PULL" 'override and its stated reason must be recorded in the artifact' "pull-work requires the override + reason to be recorded (AC2, AC8)"
340
+ require_text "$PULL" 'do not mutate GitHub Projects, provider fields, or cross-repo provider state from this skill' "pull-work's existing no-provider-mutation sentence is present verbatim and unchanged (AC6, AC8)"
341
+ require_text "$PULL" 'read/write the local runtime liveness stream, never GitHub issue/label/assignee state' "pull-work states the liveness claim is local runtime state, not a provider mutation (AC6, AC8)"
342
+ require_text "$PULL" 'this is not\s*$|this is not a provider mutation' "pull-work explicitly states the liveness claim is not a provider mutation (AC6, AC8)"
343
+ require_text "$PULL" '#290' "pull-work names #290 as the deferred assignment-provider seam (AC6, AC8)"
344
+ require_text "$PULL" 'liveness_preflight' "pull-work artifact contract records liveness_preflight (AC1, AC8)"
345
+ require_text "$PULL" 'liveness_claim' "pull-work artifact contract records liveness_claim (AC4, AC8)"
346
+ require_text "$PICKUP_PROBE" 'flags the selected item .reclaimable., treat the recorded opt-in as a decision to re-confirm' "pickup-probe re-confirms (not silently accepts) a reclaimable opt-in on drift (AC3, AC8)"
347
+
348
+ # ─── F2-F6 (fix-plan iteration 1) static assertions ─────────────────────────
349
+ require_text "$PULL" 'group all rows for that .subjectId. by subject' "pull-work classifies per-subject (grouped rows), not per-row (F2, AC1, AC8)"
350
+ require_text "$PULL" 'classify in this precedence order' "pull-work states the classification is an ordered precedence rule (F2, AC8)"
351
+ # F2's original numbered precedence list was liveness-only (#166); #290 (Wave 3) replaced it with
352
+ # the full ADR 0021 §1 assignment ⋈ liveness join, computed via `assignment-provider status`'s
353
+ # `effective_state`/`reason` fields. These assertions are updated to match the new five-row list
354
+ # (mine/held/reclaimable/human-held/free) rather than the old four-row liveness-only list.
355
+ require_text "$PULL" '^1\. .effective_state: .held.. with .reason: .self_is_holder..' "pull-work's precedence rule step 1 (mine, via self_is_holder) is numbered verbatim (F2, #290, AC8)"
356
+ require_text "$PULL" '^2\. else .effective_state: .held..' "pull-work's precedence rule step 2 (held, excluded) is numbered verbatim (F2, #290, AC8)"
357
+ require_text "$PULL" '^3\. else .effective_state: .reclaimable..' "pull-work's precedence rule step 3 (reclaimable) is numbered verbatim (F2, #290, AC8)"
358
+ require_text "$PULL" '^4\. else .effective_state: .human-held..' "pull-work's precedence rule step 4 (human-held) is numbered verbatim (F2, #290, AC11, AC8)"
359
+ require_text "$PULL" '^5\. else .effective_state: .free..' "pull-work's precedence rule step 5 (free) is numbered verbatim (F2, #290, AC8)"
360
+ require_text "$PULL" 'double-hold' "pull-work names the own-actor + other-actor co-existing row combination a double-hold (F2, AC8)"
361
+ require_text "$PULL" 'explicit conflict warning \(per ADR 0012 §4 detection' "pull-work cites ADR 0012 §4 detection for the double-hold conflict warning (F2, AC8)"
362
+ require_text "$PULL" 'never silently resolve it to .mine.' "pull-work states a double-hold is never silently resolved to mine (F2, AC8)"
363
+
364
+ require_text "$PULL" 'pin it for reuse' "pull-work pins self_actor for reuse across the pass (F3, AC8)"
365
+ require_text "$PULL" 'Capture the returned .actor. value as .self_actor.' "pull-work captures self_actor exactly once per pass from whoami (F3, AC8)"
366
+ require_text "$PULL" 'liveness claim <subjectId> --actor <self_actor>' "pull-work's claim-on-selection command passes --actor <self_actor> explicitly (F3, AC4, AC8)"
367
+ require_text "$PULL" 'always pass .--actor <self_actor>. explicitly on every claim in this pass' "pull-work requires --actor <self_actor> on every claim in the pass, not just the first (F3, AC8)"
368
+ require_text "$PULL" 'never a fresh derivation per claim call' "pull-work states the actor is never re-derived per claim call (F3, AC8)"
369
+
370
+ require_text "$PULL" 'On any claim-emit failure \(non-zero exit, unresolved actor, or liveness disabled/unavailable\)' "pull-work names the precise claim-emit failure modes covered by fail-open (F4, AC8)"
371
+ require_text "$PULL" '\{skipped: <stderr reason>\}' "pull-work's fail-open shape records {skipped: <stderr reason>} (F4, AC8)"
372
+ require_text "$PULL" 'also surface that skip reason in pull-work.s user-facing output' "pull-work requires the skip reason to be surfaced in user-facing output, never silent (F4, AC8)"
373
+ require_text "$PULL" 'unresolved-actor failure.? additionally names? the remediation' "pull-work requires unresolved-actor failures to additionally name the remediation (F4, AC8)"
374
+
375
+ require_text "$PULL" '.reclaimable_override.: recorded only when a .reclaimable.' "pull-work's Artifact Contract declares the reclaimable_override field (F5, AC8)"
376
+
377
+ require_text "$PULL" '### Post-Claim Conflict Re-check' "pull-work documents the Post-Claim Conflict Re-check subsection (F6, AC8)"
378
+ require_text "$PULL" 'coexists with this session.s own just-emitted claim on the same subject' "pull-work's post-claim re-check detects a double-hold against the just-emitted claim (F6, AC8)"
379
+ require_text "$PULL" 'surface the conflict prominently in the user-facing output and instruct the user to coordinate before proceeding' "pull-work instructs surfacing the post-claim conflict prominently and coordinating before proceeding, not silently continuing (F6, AC8)"
380
+ require_text "$PULL" 'does not provide true mutual exclusion across the read-then-write race window itself' "pull-work's honesty note states the exclusion guarantee still does not provide true mutual exclusion across the read-then-write race window (F6, AC8; updated #320 wording — see also test_liveness_verdict.sh)"
381
+ require_text "$PULL" '.#290.\x27s provider assignment lease closes this gap for the .{0,2}local-file.{0,2} provider only' "pull-work scopes true mutual exclusion to the local-file provider only, via #290's lock (fix-plan iteration 1, F5, AC8)"
382
+ require_text "$PULL" 'two concurrent local-file .claim. calls on the same subject genuinely cannot both win' "pull-work states local-file claim/claim races genuinely cannot both win under #290's lock (fix-plan iteration 1, F5, AC8)"
383
+ require_text "$PULL" 'The .{0,2}GitHub.{0,2} provider \(assignee/label/claim-comment\) remains advisory/last-writer' "pull-work does NOT overclaim true mutual exclusion for the GitHub provider — advisory/last-writer, detect not prevent (fix-plan iteration 1, F5, AC8)"
384
+ require_text "$PULL" 'Do not read .provider assignment lease. as closing the GitHub race; only the .verify-hold. publish gate \(#293\)' "pull-work explicitly warns not to read #290 as closing the GitHub race — only #293's verify-hold gate would (fix-plan iteration 1, F5, AC8)"
385
+
386
+
387
+ echo ""
388
+ if [[ "$errors" -eq 0 ]]; then
389
+ echo "Pull work liveness preflight integration passed."
390
+ exit 0
391
+ fi
392
+
393
+ echo "Pull work liveness preflight integration failed: $errors issue(s)."
394
+ exit 1
@@ -35,7 +35,7 @@ status=$?
35
35
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.id")" == "github:kontourai/flow-agents#22" ]] && pass "normalizes provider-qualified id" || fail "normalizes provider-qualified id"
36
36
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.source_provider.role")" == "WorkItemProvider" ]] && pass "preserves work item provider ref" || fail "preserves work item provider ref"
37
37
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.board_membership.role")" == "BoardProvider" ]] && pass "preserves board provider ref" || fail "preserves board provider ref"
38
- [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.artifact_refs.0")" == ".flow-agents/flow-agents-kit-platform-backlog/flow-agents-kit-platform-backlog--idea-to-backlog.md" ]] && pass "preserves source artifact ref" || fail "preserves source artifact ref"
38
+ [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.artifact_refs.0")" == ".kontourai/flow-agents/flow-agents-kit-platform-backlog/flow-agents-kit-platform-backlog--idea-to-backlog.md" ]] && pass "preserves source artifact ref" || fail "preserves source artifact ref"
39
39
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.blockers.0.ref.owner")" == "kontourai" ]] && pass "preserves blocker owner" || fail "preserves blocker owner"
40
40
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.blockers.0.ref.repo")" == "flow" ]] && pass "preserves cross-repo blocker repo" || fail "preserves cross-repo blocker repo"
41
41
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.0.dependency_impacts.0.ref.owner")" == "kontourai" ]] && pass "projects prose blocker impact owner" || fail "projects prose blocker impact owner"
@@ -51,7 +51,7 @@ status=$?
51
51
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.planned_base_ref")" == "main" ]] && pass "marker normalizes planned_base_ref" || fail "marker normalizes planned_base_ref"
52
52
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.planned_base_sha")" == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ]] && pass "marker normalizes planned_base_sha" || fail "marker normalizes planned_base_sha"
53
53
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.planned_at")" == "2026-06-03T03:23:14Z" ]] && pass "marker normalizes planned_at" || fail "marker normalizes planned_at"
54
- [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.planning_artifact_ref")" == ".flow-agents/idea-to-backlog-source-revision-structured-blockers/idea-to-backlog-source-revision-structured-blockers--plan.md" ]] && pass "marker normalizes planning_artifact_ref" || fail "marker normalizes planning_artifact_ref"
54
+ [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.planning_artifact_ref")" == ".kontourai/flow-agents/idea-to-backlog-source-revision-structured-blockers/idea-to-backlog-source-revision-structured-blockers--plan.md" ]] && pass "marker normalizes planning_artifact_ref" || fail "marker normalizes planning_artifact_ref"
55
55
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.planning_scope_refs.0")" == "kits/builder/skills/idea-to-backlog/SKILL.md" ]] && pass "marker normalizes planning_scope_refs" || fail "marker normalizes planning_scope_refs"
56
56
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.source_revisions.1.repo")" == "kontourai/flow" ]] && pass "marker preserves repo-scoped source_revisions" || fail "marker preserves repo-scoped source_revisions"
57
57
  [[ "$(json_query "$TMPDIR_EVAL/normalized.json" "items.5.blockers.0.ref.owner")" == "kontourai" ]] && pass "structured blocker preserves provider owner" || fail "structured blocker preserves provider owner"
@@ -9,8 +9,13 @@
9
9
  # B. REAL-VERIFY-CLOSED: build passes but a fake "real verify" (eval:static substitute)
10
10
  # fails → trust-reconcile exits 1 (PRE-FIX this would exit 0 because only build ran).
11
11
  #
12
- # C. REAL-VERIFY-PASSES: the comprehensive verify resolves green exits 0 (legit
13
- # work is not false-blocked when package.json trust-reconcile-verify is present).
12
+ # C. REAL-VERIFY-PASSES: the comprehensive verify resolves green, no bundle, but a
13
+ # well-formed in-scope delivery/DECLARED marker exempts Step 2 (ADR 0022 §1) → exits 0
14
+ # with the DECLARED (no-agent-delivery) line (legit work is not false-blocked; the
15
+ # bundle-absence branch is bundle-required by default post-ADR-0022, so this case is
16
+ # now anchored by a DECLARED marker rather than by bundle absence alone — see
17
+ # evals/integration/test_trust_reconcile_negatives.sh section 7 for the no-marker /
18
+ # out-of-scope-marker fail-closed cases this test does NOT re-cover).
14
19
  #
15
20
  # D. CHECKPOINT-BYPASS-CLOSED: a checkpoint-only bundle (no evidence/claims, statusByClaimId
16
21
  # all-passed) → exits 1 with checkpoint-bypass divergence (not a silent skip).
@@ -56,8 +61,14 @@ fs.writeFileSync('$PKG_NO_VERIFY/package.json', JSON.stringify(pkg, null, 2));
56
61
  "
57
62
 
58
63
  # ─── Minimal package.json WITH trust-reconcile-verify ─────────────────────────
64
+ # ADR 0022 §1 (bundle-required by default): no bundle is written for this fixture, so a
65
+ # well-formed, in-scope delivery/DECLARED marker (ADR 0022 §2) is seeded alongside it —
66
+ # this is the new legitimate no-bundle path (a real human/bot PR author with no agent
67
+ # delivery), preserving TEST C's original "no bundle" shape more faithfully than
68
+ # switching it to a --bundle fixture would (a bundle would test Step 2 reconcile, which
69
+ # TEST C was never about — it proves Step 1 fresh-verify success is honored).
59
70
  PKG_WITH_VERIFY="$TMP/pkg_with_verify"
60
- mkdir -p "$PKG_WITH_VERIFY"
71
+ mkdir -p "$PKG_WITH_VERIFY/delivery"
61
72
  node -e "
62
73
  const fs = require('fs');
63
74
  const pkg = {
@@ -68,6 +79,13 @@ const pkg = {
68
79
  }
69
80
  };
70
81
  fs.writeFileSync('$PKG_WITH_VERIFY/package.json', JSON.stringify(pkg, null, 2));
82
+ const declared = {
83
+ scope: 'ref:test-verify-passes',
84
+ reason: 'test fixture: real verify passes, no agent delivery bundle',
85
+ approved_by: 'test-harness',
86
+ declared_at: '2026-06-27T00:00:00Z'
87
+ };
88
+ fs.writeFileSync('$PKG_WITH_VERIFY/delivery/DECLARED', JSON.stringify(declared, null, 2));
71
89
  "
72
90
 
73
91
  # ─── Minimal package.json with failing verify ─────────────────────────────────
@@ -98,7 +116,7 @@ const [,, bundlePath, label, passingVal] = process.argv;
98
116
  // Evaluate passing value as JS literal
99
117
  const passing = JSON.parse(passingVal);
100
118
  const bundle = {
101
- schemaVersion: 3,
119
+ schemaVersion: 5,
102
120
  source: "test-fixture",
103
121
  claims: [
104
122
  {
@@ -107,7 +125,7 @@ const bundle = {
107
125
  value: "pass",
108
126
  status: "verified",
109
127
  subjectId: "test/build",
110
- surface: "flow-agents.workflow",
128
+ facet: "flow-agents.workflow",
111
129
  subjectType: "workflow-check",
112
130
  fieldOrBehavior: "build",
113
131
  createdAt: "2026-06-27T00:00:00Z",
@@ -170,7 +188,7 @@ write_bundle_claim_no_evidence() {
170
188
  const fs = require('fs');
171
189
  const [,, bundlePath] = process.argv;
172
190
  const bundle = {
173
- schemaVersion: 3,
191
+ schemaVersion: 5,
174
192
  source: "test-fixture",
175
193
  claims: [
176
194
  {
@@ -179,7 +197,7 @@ const bundle = {
179
197
  value: "pass",
180
198
  status: "verified",
181
199
  subjectId: "test/command-never-run",
182
- surface: "flow-agents.workflow",
200
+ facet: "flow-agents.workflow",
183
201
  subjectType: "workflow-check",
184
202
  fieldOrBehavior: "npm run test-never-ran",
185
203
  createdAt: "2026-06-27T00:00:00Z",
@@ -255,22 +273,28 @@ fi
255
273
  # package.json has passing trust-reconcile-verify → exits 0 (not false-blocked)
256
274
  # ═══════════════════════════════════════════════════════════════════════════════
257
275
  echo ""
258
- echo "=== TEST C: REAL-VERIFY-PASSES — passing verify + no bundle → exits 0 ==="
276
+ echo "=== TEST C: REAL-VERIFY-PASSES — passing verify + in-scope delivery/DECLARED marker (no bundle) → exits 0 with DECLARED exemption ==="
259
277
 
260
- outC=$(node "$RECONCILE" \
278
+ outC=$(TRUST_RECONCILE_REF="test-verify-passes" node "$RECONCILE" \
261
279
  --repo-root "$PKG_WITH_VERIFY" 2>&1)
262
280
  exitC=$?
263
281
 
264
282
  if [[ $exitC -eq 0 ]]; then
265
- _pass "REAL-VERIFY-PASSES: exits 0 when real verify passes and no bundle (got $exitC)"
283
+ _pass "REAL-VERIFY-PASSES: exits 0 when real verify passes and a well-formed, in-scope delivery/DECLARED marker exempts Step 2 (got $exitC)"
266
284
  else
267
285
  _fail "REAL-VERIFY-PASSES: expected exit 0, got $exitC — output: $outC"
268
286
  fi
269
287
 
270
- if echo "$outC" | grep -q "fresh verify passed"; then
271
- _pass "REAL-VERIFY-PASSES: 'fresh verify passed' message present"
288
+ if echo "$outC" | grep -qF "DECLARED (no-agent-delivery): ref:test-verify-passes — test fixture: real verify passes, no agent delivery bundle (approved by test-harness, declared 2026-06-27T00:00:00Z)"; then
289
+ _pass "REAL-VERIFY-PASSES: exact DECLARED (no-agent-delivery) exemption line present"
290
+ else
291
+ _fail "REAL-VERIFY-PASSES: expected the exact DECLARED (no-agent-delivery) line, got: $outC"
292
+ fi
293
+
294
+ if echo "$outC" | grep -q "running: npm run trust-reconcile-verify"; then
295
+ _pass "REAL-VERIFY-PASSES: Step 1 (fresh verify) still ran the real trust-reconcile-verify command (not skipped by the exemption)"
272
296
  else
273
- _fail "REAL-VERIFY-PASSES: expected 'fresh verify passed' in output, got: $outC"
297
+ _fail "REAL-VERIFY-PASSES: expected Step 1 to have run 'npm run trust-reconcile-verify', got: $outC"
274
298
  fi
275
299
 
276
300
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -76,6 +76,28 @@ else
76
76
  _fail "resolveFlowFilePath: some cases did not match expected"
77
77
  fi
78
78
 
79
+ echo ""
80
+ echo "=== 1b. resolveFlowFilePath symlink escape → null (realpath containment) ==="
81
+
82
+ SYMLINK_REPO="$TMP/symlink-repo"
83
+ SYMLINK_SECRET="$TMP/symlink-secret"
84
+ mkdir -p "$SYMLINK_REPO/kits/builder/flows" "$SYMLINK_SECRET"
85
+ printf '{"id":"builder.build","version":"1.0.0","steps":[],"gates":{}}' > "$SYMLINK_SECRET/build.flow.json"
86
+ ln -s "$SYMLINK_SECRET/build.flow.json" "$SYMLINK_REPO/kits/builder/flows/build.flow.json"
87
+
88
+ node --input-type=module << JSEOF 2>&1
89
+ import { resolveFlowFilePath } from '${FLOW_RESOLVER_JS}';
90
+ const result = resolveFlowFilePath("builder", "build", "builder.build", "$SYMLINK_REPO");
91
+ if (result !== null) throw new Error("symlink escape should return null, got " + result);
92
+ console.log(" PASS: symlinked flow file pointing outside kits/ returned null");
93
+ JSEOF
94
+
95
+ if [ $? -eq 0 ]; then
96
+ _pass "resolveFlowFilePath: symlink escape rejected"
97
+ else
98
+ _fail "resolveFlowFilePath: symlink escape was not rejected"
99
+ fi
100
+
79
101
  # ─── Behavioral: ensure-session with traversal --flow-id → null active_step_id ─
80
102
  echo ""
81
103
  echo "=== 2. ensure-session --flow-id traversal → no active_step_id (null return) ==="