@kontourai/flow-agents 2.2.0 → 2.4.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 (250) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/AGENTS.md +4 -4
  4. package/CHANGELOG.md +46 -0
  5. package/CONTEXT.md +14 -0
  6. package/README.md +9 -3
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-code-reviewer.json +1 -1
  9. package/agents/tool-planner.json +3 -3
  10. package/agents/tool-verifier.json +3 -3
  11. package/build/src/cli/console-learning-projection.js +3 -2
  12. package/build/src/cli/init.js +104 -21
  13. package/build/src/cli/kit.js +37 -6
  14. package/build/src/cli/pull-work-provider.js +1 -1
  15. package/build/src/cli/usage-feedback.js +3 -3
  16. package/build/src/cli/validate-hook-influence.js +1 -0
  17. package/build/src/cli/validate-workflow-artifacts.js +22 -6
  18. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  19. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  20. package/build/src/cli/workflow-sidecar.js +792 -69
  21. package/build/src/flow-kit/validate.d.ts +23 -0
  22. package/build/src/flow-kit/validate.js +67 -1
  23. package/build/src/index.d.ts +1 -1
  24. package/build/src/index.js +1 -1
  25. package/build/src/lib/flow-resolver.d.ts +6 -4
  26. package/build/src/lib/flow-resolver.js +125 -58
  27. package/build/src/lib/local-artifact-root.d.ts +14 -2
  28. package/build/src/lib/local-artifact-root.js +22 -5
  29. package/build/src/lib/workflow-learning-projection.js +2 -2
  30. package/build/src/runtime-adapters.d.ts +12 -0
  31. package/build/src/runtime-adapters.js +56 -16
  32. package/build/src/tools/build-universal-bundles.js +49 -16
  33. package/build/src/tools/generate-context-map.js +10 -8
  34. package/build/src/tools/validate-source-tree.js +23 -1
  35. package/console.telemetry.json +22 -0
  36. package/context/contracts/artifact-contract.md +38 -7
  37. package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
  38. package/context/contracts/decision-registry-contract.md +263 -0
  39. package/context/contracts/delivery-contract.md +1 -1
  40. package/context/contracts/planning-contract.md +2 -1
  41. package/context/contracts/review-contract.md +5 -3
  42. package/context/contracts/verification-contract.md +9 -1
  43. package/context/contracts/work-item-contract.md +1 -1
  44. package/context/deferred/parallelization.md +1 -1
  45. package/context/gate-awareness.md +4 -4
  46. package/context/scripts/git-status.sh +2 -2
  47. package/context/scripts/hooks/config-protection.js +502 -2
  48. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  49. package/context/scripts/hooks/run-hook.js +14 -1
  50. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  51. package/context/scripts/hooks/workflow-steering.js +212 -6
  52. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  54. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  55. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  56. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  57. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
  58. package/docs/agent-system-guidebook.md +3 -3
  59. package/docs/agent-usage-feedback-loop.md +6 -6
  60. package/docs/context-map.md +9 -7
  61. package/docs/decisions/decision-records.md +57 -0
  62. package/docs/decisions/index.md +14 -0
  63. package/docs/decisions/promotion-gate.md +52 -0
  64. package/docs/developer-architecture.md +1 -1
  65. package/docs/fixture-ownership.md +3 -0
  66. package/docs/flow-kit-repository-contract.md +5 -5
  67. package/docs/getting-started.md +12 -2
  68. package/docs/integrations/harness-install.md +2 -1
  69. package/docs/kit-authoring-guide.md +64 -5
  70. package/docs/knowledge-kit.md +5 -1
  71. package/docs/learnings/2026-07-improvement-program.md +203 -0
  72. package/docs/migrations.md +6 -5
  73. package/docs/north-star.md +2 -2
  74. package/docs/operating-layers.md +4 -3
  75. package/docs/repository-structure.md +5 -5
  76. package/docs/skills-map.md +8 -8
  77. package/docs/spec/runtime-hook-surface.md +19 -15
  78. package/docs/spikes/graph-provider-2026-07.md +155 -0
  79. package/docs/standards-register.md +7 -7
  80. package/docs/trust-anchor-adoption.md +105 -4
  81. package/docs/work-item-adapters.md +1 -1
  82. package/docs/workflow-artifact-lifecycle.md +72 -9
  83. package/docs/workflow-eval-strategy.md +3 -3
  84. package/docs/workflow-shared-contracts.md +1 -1
  85. package/docs/workflow-usage-guide.md +254 -21
  86. package/evals/acceptance/DEMO-false-completion.md +20 -11
  87. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  88. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  89. package/evals/acceptance/test_kiro_harness.sh +1 -1
  90. package/evals/ci/antigaming-suite.sh +5 -0
  91. package/evals/ci/run-baseline.sh +65 -1
  92. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  93. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  94. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  95. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  96. package/evals/fixtures/hook-influence/cases.json +32 -0
  97. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  98. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  99. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  100. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  101. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  102. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  103. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  104. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  105. package/evals/integration/test_actor_identity.sh +254 -0
  106. package/evals/integration/test_builder_step_producers.sh +33 -23
  107. package/evals/integration/test_bundle_install.sh +22 -16
  108. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  109. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  110. package/evals/integration/test_checkpoint_signing.sh +43 -0
  111. package/evals/integration/test_claim_lookup.sh +4 -4
  112. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  113. package/evals/integration/test_command_log_concurrency.sh +8 -8
  114. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  115. package/evals/integration/test_command_log_integrity.sh +16 -16
  116. package/evals/integration/test_console_learning_projection.sh +1 -1
  117. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  118. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  119. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  120. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  121. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  122. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  123. package/evals/integration/test_flow_kit_repository.sh +2 -0
  124. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  125. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  126. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  127. package/evals/integration/test_gate_lockdown.sh +97 -73
  128. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  129. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  130. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  131. package/evals/integration/test_goal_fit_hook.sh +32 -32
  132. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  133. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  134. package/evals/integration/test_hook_influence_cases.sh +1 -0
  135. package/evals/integration/test_install_merge.sh +262 -1
  136. package/evals/integration/test_kit_identity_trust.sh +6 -6
  137. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  138. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  139. package/evals/integration/test_liveness_verdict.sh +394 -0
  140. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  141. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  142. package/evals/integration/test_promote_gate.sh +178 -0
  143. package/evals/integration/test_publish_delivery.sh +2 -2
  144. package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
  145. package/evals/integration/test_pull_work_provider.sh +2 -2
  146. package/evals/integration/test_reconcile_soundness.sh +4 -4
  147. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  148. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  149. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  150. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  151. package/evals/integration/test_trust_reconcile.sh +2 -2
  152. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  153. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  154. package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
  155. package/evals/integration/test_usage_feedback_global.sh +5 -5
  156. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  157. package/evals/integration/test_verify_cli.sh +2 -2
  158. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  159. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  160. package/evals/integration/test_workflow_artifacts.sh +33 -0
  161. package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
  162. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  163. package/evals/lib/codex-provider.sh +1 -1
  164. package/evals/lib/node.sh +19 -1
  165. package/evals/run.sh +22 -0
  166. package/evals/static/test_decisions.sh +150 -0
  167. package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
  168. package/evals/static/test_package.sh +19 -10
  169. package/evals/static/test_universal_bundles.sh +48 -3
  170. package/evals/static/test_workflow_skills.sh +41 -8
  171. package/kits/builder/flows/build.flow.json +3 -74
  172. package/kits/builder/flows/publish-learn.flow.json +90 -0
  173. package/kits/builder/kit.json +11 -0
  174. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  175. package/kits/builder/skills/deliver/SKILL.md +13 -11
  176. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  177. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  178. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  179. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  180. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  181. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  182. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  183. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  184. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  185. package/kits/builder/skills/pull-work/SKILL.md +80 -5
  186. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  187. package/kits/builder/skills/review-work/SKILL.md +24 -1
  188. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  189. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  190. package/kits/catalog.json +6 -0
  191. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  192. package/kits/veritas-governance/docs/README.md +75 -0
  193. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  194. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  195. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  196. package/kits/veritas-governance/kit.json +16 -0
  197. package/package.json +8 -4
  198. package/packaging/README.md +2 -2
  199. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  200. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  201. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  202. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  203. package/packaging/manifest.json +9 -10
  204. package/schemas/decision-record.schema.json +109 -0
  205. package/schemas/workflow-evidence.schema.json +1 -1
  206. package/schemas/workflow-state.schema.json +5 -0
  207. package/scripts/README.md +8 -3
  208. package/scripts/check-content-boundary.cjs +1 -1
  209. package/scripts/check-decisions.cjs +356 -0
  210. package/scripts/ci/derive-claim-status.mjs +74 -0
  211. package/scripts/ci/trust-reconcile.js +436 -79
  212. package/scripts/git-status.sh +2 -2
  213. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  214. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  215. package/scripts/hooks/config-protection.js +102 -30
  216. package/scripts/hooks/evidence-capture.js +3 -3
  217. package/scripts/hooks/lib/actor-identity.js +368 -0
  218. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  219. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  220. package/scripts/hooks/lib/liveness-policy.js +127 -0
  221. package/scripts/hooks/lib/liveness-read.js +105 -12
  222. package/scripts/hooks/lib/liveness-write.js +46 -0
  223. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  224. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  225. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  226. package/scripts/hooks/stop-goal-fit.js +157 -26
  227. package/scripts/hooks/workflow-steering.js +77 -3
  228. package/scripts/install-codex-home.sh +138 -38
  229. package/scripts/statusline/flow-agents-statusline.js +2 -2
  230. package/src/cli/console-learning-projection.ts +3 -2
  231. package/src/cli/init.ts +101 -21
  232. package/src/cli/kit.ts +37 -6
  233. package/src/cli/public-api.test.mjs +58 -6
  234. package/src/cli/pull-work-provider.ts +1 -1
  235. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  236. package/src/cli/usage-feedback.ts +3 -3
  237. package/src/cli/validate-hook-influence.ts +1 -0
  238. package/src/cli/validate-workflow-artifacts.ts +22 -6
  239. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  240. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  241. package/src/cli/workflow-sidecar.ts +795 -71
  242. package/src/flow-kit/validate.ts +74 -1
  243. package/src/index.ts +6 -2
  244. package/src/lib/flow-resolver.ts +123 -55
  245. package/src/lib/local-artifact-root.ts +24 -5
  246. package/src/lib/workflow-learning-projection.ts +2 -2
  247. package/src/runtime-adapters.ts +61 -15
  248. package/src/tools/build-universal-bundles.ts +49 -16
  249. package/src/tools/generate-context-map.ts +10 -8
  250. package/src/tools/validate-source-tree.ts +23 -1
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env bash
2
+ # test_promote_gate.sh - promote-then-archive gate (issue #312).
3
+ #
4
+ # Durable-residue extraction is the archival act. Covers:
5
+ # 1. promote writes a well-formed, session-local promotion claim referencing
6
+ # existing durable doc paths (metadata.promotion + policy_rule evidence, no
7
+ # execution.label) and an auditable promotion.json.
8
+ # 2. promote REJECTS a nonexistent --evidence-path (fail loud).
9
+ # 3. promote --none --reason records an explicit no-residue claim; --none without
10
+ # --reason is refused.
11
+ # 4. cleanup-audit classification: a delivered/accepted session WITHOUT a promotion
12
+ # claim is a cleanup_candidate (blocked from archive) with a remedy naming the
13
+ # promote step; WITH a real claim OR a --none claim it stays terminal_done (AC1-3).
14
+ # 5. CRITICAL: trust-reconcile stays exit 0 on a bundle carrying a promotion claim -
15
+ # the claim classifies session-local (ATTESTED), never an unbacked/not-run command
16
+ # divergence (proves R1 reconcile-safety, no new manifest entry required).
17
+ #
18
+ # Deterministic, no model spend, self-cleaning.
19
+ # Usage: bash evals/integration/test_promote_gate.sh
20
+ set -uo pipefail
21
+
22
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
23
+ source "$ROOT/evals/lib/node.sh"
24
+
25
+ TMP="$(mktemp -d)"
26
+ trap 'rm -rf "$TMP"' EXIT
27
+ unset FORCE_COLOR # known local artifact that can perturb child-process output
28
+
29
+ errors=0
30
+ pass() { echo " PASS: $1"; }
31
+ fail() { echo " FAIL: $1"; errors=$((errors + 1)); }
32
+
33
+ echo "=== Promote-then-archive gate (#312) ==="
34
+
35
+ # JSON reader helpers (dedicated files; no inline interpreter flags).
36
+ cat > "$TMP/inspect.cjs" <<'JS'
37
+ const fs = require("fs");
38
+ const b = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
39
+ const claim = (b.claims || []).find((c) => c.metadata && c.metadata.promotion);
40
+ const ev = claim && (b.evidence || []).find((e) => e.claimId === claim.id);
41
+ const out = {
42
+ found: String(!!claim),
43
+ status: (claim && claim.status) || "",
44
+ targets: (claim && claim.metadata.promotion.targets) || [],
45
+ none: String(claim ? claim.metadata.promotion.none : ""),
46
+ evidenceType: (ev && ev.evidenceType) || "",
47
+ hasExecutionLabel: String(!!(ev && ev.execution && ev.execution.label)),
48
+ };
49
+ fs.writeFileSync(process.argv[3], JSON.stringify(out));
50
+ JS
51
+ cat > "$TMP/field.cjs" <<'JS'
52
+ const fs = require("fs");
53
+ const o = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
54
+ let v = o;
55
+ for (const k of process.argv[3].split(".")) v = v[k];
56
+ console.log(Array.isArray(v) ? v.join(",") : v);
57
+ JS
58
+ cat > "$TMP/bucketof.cjs" <<'JS'
59
+ const fs = require("fs");
60
+ const d = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
61
+ for (const [b, arr] of Object.entries(d.buckets)) {
62
+ for (const it of arr) if (it.slug === process.argv[3]) { console.log(b + "|" + (it.reasons[0] || "")); process.exit(0); }
63
+ }
64
+ console.log("MISSING|");
65
+ JS
66
+
67
+ REPO="$TMP/repo"
68
+ mkdir -p "$REPO/kits" "$REPO/docs/decisions" "$REPO/docs/learnings"
69
+ printf 'x\n' > "$REPO/docs/decisions/promotion-gate.md"
70
+ printf 'y\n' > "$REPO/docs/learnings/note.md"
71
+ SESSION_ROOT="$REPO/.kontourai/flow-agents"
72
+
73
+ ensure() {
74
+ flow_agents_node workflow-sidecar ensure-session \
75
+ --artifact-root "$SESSION_ROOT" --task-slug "$1" \
76
+ --source-request "r" --title "T" --summary "s" \
77
+ --criterion "c1" --next-action "n" --timestamp "2026-07-02T00:00:00Z" \
78
+ >/dev/null 2>"$TMP/$1.ensure.err"
79
+ }
80
+ seed_evidence() {
81
+ flow_agents_node workflow-sidecar record-evidence "$1" --verdict pass \
82
+ --check-json '{"id":"docs-check","kind":"policy","status":"pass","summary":"docs reviewed"}' \
83
+ --timestamp "2026-07-02T00:01:00Z" >/dev/null 2>"$TMP/ev.err"
84
+ }
85
+ term_state() {
86
+ printf '{"schema_version":"1.0","task_slug":"%s","status":"accepted","phase":"done","updated_at":"2026-07-02T00:00:00Z","next_action":{"status":"done","summary":"done"}}' "$2" > "$1/state.json"
87
+ }
88
+ field() { node "$TMP/field.cjs" "$1" "$2"; }
89
+
90
+ # --- 1. promote writes a well-formed session-local claim ---------------------
91
+ ensure promoted
92
+ PDIR="$SESSION_ROOT/promoted"
93
+ seed_evidence "$PDIR"
94
+ if flow_agents_node workflow-sidecar promote "$PDIR" --repo-root "$REPO" \
95
+ --evidence-path docs/decisions/promotion-gate.md \
96
+ --evidence-path docs/learnings/note.md \
97
+ --timestamp "2026-07-02T00:02:00Z" >"$TMP/promote.out" 2>"$TMP/promote.err"; then
98
+ pass "promote with existing paths exits 0"
99
+ else
100
+ fail "promote with existing paths should exit 0: $(cat "$TMP/promote.err")"
101
+ fi
102
+ [[ -f "$PDIR/promotion.json" ]] && pass "promote writes auditable promotion.json" || fail "promote writes auditable promotion.json"
103
+
104
+ node "$TMP/inspect.cjs" "$PDIR/trust.bundle" "$TMP/shape.json"
105
+ [[ "$(field "$TMP/shape.json" found)" == "true" ]] && pass "bundle carries a promotion claim (metadata.promotion)" || fail "bundle carries a promotion claim"
106
+ [[ "$(field "$TMP/shape.json" status)" == "verified" ]] && pass "promotion claim derives status verified" || fail "promotion claim derives status verified (got $(field "$TMP/shape.json" status))"
107
+ [[ "$(field "$TMP/shape.json" evidenceType)" == "policy_rule" ]] && pass "promotion evidence is session-local policy_rule" || fail "promotion evidence is session-local policy_rule (got $(field "$TMP/shape.json" evidenceType))"
108
+ [[ "$(field "$TMP/shape.json" hasExecutionLabel)" == "false" ]] && pass "promotion claim carries no execution.label (never a manifest command)" || fail "promotion claim must not carry an execution.label"
109
+ [[ "$(field "$TMP/shape.json" targets)" == "docs/decisions/promotion-gate.md,docs/learnings/note.md" ]] && pass "promotion evidence refs = the durable doc paths written" || fail "promotion targets wrong: $(field "$TMP/shape.json" targets)"
110
+
111
+ # --- 2. promote rejects a nonexistent path -----------------------------------
112
+ if flow_agents_node workflow-sidecar promote "$PDIR" --repo-root "$REPO" \
113
+ --evidence-path docs/decisions/does-not-exist.md \
114
+ --timestamp "2026-07-02T00:03:00Z" >"$TMP/bad.out" 2>"$TMP/bad.err"; then
115
+ fail "promote should reject a nonexistent evidence path"
116
+ else
117
+ grep -q "does not exist on disk" "$TMP/bad.err" && pass "promote rejects a nonexistent evidence path (fail loud)" || fail "promote rejection message unclear: $(cat "$TMP/bad.err")"
118
+ fi
119
+
120
+ # --- 3. explicit no-residue promotion ----------------------------------------
121
+ ensure none-promoted
122
+ NDIR="$SESSION_ROOT/none-promoted"
123
+ seed_evidence "$NDIR"
124
+ if flow_agents_node workflow-sidecar promote "$NDIR" --repo-root "$REPO" \
125
+ --none --reason "no durable residue: pure refactor" \
126
+ --timestamp "2026-07-02T00:04:00Z" >"$TMP/none.out" 2>"$TMP/none.err"; then
127
+ pass "promote --none --reason exits 0"
128
+ else
129
+ fail "promote --none --reason should exit 0: $(cat "$TMP/none.err")"
130
+ fi
131
+ node "$TMP/inspect.cjs" "$NDIR/trust.bundle" "$TMP/noneshape.json"
132
+ [[ "$(field "$TMP/noneshape.json" none)" == "true" ]] && pass "no-residue claim records none=true" || fail "no-residue claim should record none=true"
133
+
134
+ if flow_agents_node workflow-sidecar promote "$NDIR" --none \
135
+ --timestamp "2026-07-02T00:05:00Z" >"$TMP/none2.out" 2>"$TMP/none2.err"; then
136
+ fail "promote --none without --reason should be refused"
137
+ else
138
+ grep -q "requires --reason" "$TMP/none2.err" && pass "promote --none without --reason is refused" || fail "promote --none reason-required message unclear"
139
+ fi
140
+
141
+ # --- 4. cleanup-audit classification with/without the claim ------------------
142
+ AUDIT_ROOT="$TMP/audit-root"
143
+ mkdir -p "$AUDIT_ROOT/promoted" "$AUDIT_ROOT/none-promoted" "$AUDIT_ROOT/unpromoted"
144
+ term_state "$AUDIT_ROOT/promoted" promoted
145
+ term_state "$AUDIT_ROOT/none-promoted" none-promoted
146
+ term_state "$AUDIT_ROOT/unpromoted" unpromoted
147
+ cp "$PDIR/trust.bundle" "$AUDIT_ROOT/promoted/trust.bundle"
148
+ cp "$NDIR/trust.bundle" "$AUDIT_ROOT/none-promoted/trust.bundle"
149
+ # unpromoted: deliberately no trust.bundle
150
+
151
+ flow_agents_node workflow-artifact-cleanup-audit --artifact-root "$AUDIT_ROOT" --json > "$TMP/audit.json" 2>&1
152
+ bucketof() { node "$TMP/bucketof.cjs" "$TMP/audit.json" "$1"; }
153
+ [[ "$(bucketof promoted | cut -d'|' -f1)" == "terminal_done" ]] && pass "promoted session stays terminal_done" || fail "promoted session should be terminal_done (got $(bucketof promoted))"
154
+ [[ "$(bucketof none-promoted | cut -d'|' -f1)" == "terminal_done" ]] && pass "no-residue promoted session stays terminal_done" || fail "no-residue session should be terminal_done (got $(bucketof none-promoted))"
155
+ [[ "$(bucketof unpromoted | cut -d'|' -f1)" == "cleanup_candidate" ]] && pass "unpromoted delivered session is a cleanup_candidate (archive blocked)" || fail "unpromoted session should be cleanup_candidate (got $(bucketof unpromoted))"
156
+ bucketof unpromoted | grep -q "promote" && pass "cleanup_candidate reason names the promote remedy" || fail "cleanup_candidate reason should name the promote step"
157
+
158
+ # --- 5. CRITICAL: trust-reconcile exit 0 on a bundle with a promotion claim --
159
+ if node "$ROOT/scripts/ci/trust-reconcile.js" --bundle "$PDIR/trust.bundle" \
160
+ --commands "true" --repo-root "$REPO" >"$TMP/reconcile.out" 2>&1; then
161
+ pass "trust-reconcile exits 0 on a bundle carrying a promotion claim"
162
+ else
163
+ fail "trust-reconcile should exit 0 on a promotion-claim bundle: $(tail -5 "$TMP/reconcile.out")"
164
+ fi
165
+ if grep -q "ATTESTED" "$TMP/reconcile.out" && ! grep -q "trust divergence" "$TMP/reconcile.out"; then
166
+ pass "promotion claim classifies session-local (ATTESTED), never an unbacked/not-run command"
167
+ else
168
+ fail "promotion claim must classify session-local, not as a command divergence"
169
+ grep -iE "classified|ATTESTED|trust divergence" "$TMP/reconcile.out"
170
+ fi
171
+
172
+ echo ""
173
+ if [[ "$errors" -eq 0 ]]; then
174
+ echo "Promote-then-archive gate checks passed"
175
+ else
176
+ echo "Promote-then-archive gate checks failed: $errors"
177
+ exit 1
178
+ fi
@@ -41,10 +41,10 @@ code_lines = [
41
41
  "const fs = require('fs');",
42
42
  "const [,, dest, label, passingStr] = process.argv;",
43
43
  "const passing = passingStr === 'true';",
44
- "const b = { schemaVersion: 3, source: 'test-fixture',",
44
+ "const b = { schemaVersion: 5, source: 'test-fixture',",
45
45
  " claims: [{ id: 'c1', claimType: 'workflow.check.build',",
46
46
  " value: passing ? 'pass' : 'fail', status: passing ? 'verified' : 'disputed',",
47
- " subjectId: 'ts/build', surface: 'flow-agents.workflow',",
47
+ " subjectId: 'ts/build', facet: 'flow-agents.workflow',",
48
48
  " subjectType: 'workflow-check', fieldOrBehavior: 'build',",
49
49
  " createdAt: '2026-06-27T00:00:00Z', updatedAt: '2026-06-27T00:00:00Z',",
50
50
  " impactLevel: 'high', verificationPolicyId: 'policy:wf.build' }],",
@@ -0,0 +1,386 @@
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" 'a .stale. row \(any actor\).*.reclaimable.' "pull-work documents reclaimable classification (AC1, AC3, AC8)"
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
+ require_text "$PULL" '^1\. a .verified. row for an actor other than self' "pull-work's precedence rule step 1 (held) is numbered verbatim (F2, AC8)"
352
+ require_text "$PULL" '^2\. else, a .verified. row for the resolved self actor' "pull-work's precedence rule step 2 (mine) is numbered verbatim (F2, AC8)"
353
+ require_text "$PULL" '^3\. else, a .stale. row' "pull-work's precedence rule step 3 (reclaimable) is numbered verbatim (F2, AC8)"
354
+ require_text "$PULL" '^4\. else \(a .revoked.-only row' "pull-work's precedence rule step 4 (free) is numbered verbatim (F2, AC8)"
355
+ require_text "$PULL" 'double-hold' "pull-work names the own-actor + other-actor co-existing row combination a double-hold (F2, AC8)"
356
+ 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)"
357
+ require_text "$PULL" 'never silently resolve it to .mine.' "pull-work states a double-hold is never silently resolved to mine (F2, AC8)"
358
+
359
+ require_text "$PULL" 'pin it for reuse' "pull-work pins self_actor for reuse across the pass (F3, AC8)"
360
+ require_text "$PULL" 'Capture the returned .actor. value as .self_actor.' "pull-work captures self_actor exactly once per pass from whoami (F3, AC8)"
361
+ require_text "$PULL" 'liveness claim <subjectId> --actor <self_actor>' "pull-work's claim-on-selection command passes --actor <self_actor> explicitly (F3, AC4, AC8)"
362
+ 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)"
363
+ require_text "$PULL" 'never a fresh derivation per claim call' "pull-work states the actor is never re-derived per claim call (F3, AC8)"
364
+
365
+ 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)"
366
+ require_text "$PULL" '\{skipped: <stderr reason>\}' "pull-work's fail-open shape records {skipped: <stderr reason>} (F4, AC8)"
367
+ 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)"
368
+ require_text "$PULL" 'unresolved-actor failure.? additionally names? the remediation' "pull-work requires unresolved-actor failures to additionally name the remediation (F4, AC8)"
369
+
370
+ require_text "$PULL" '.reclaimable_override.: recorded only when a .reclaimable.' "pull-work's Artifact Contract declares the reclaimable_override field (F5, AC8)"
371
+
372
+ require_text "$PULL" '### Post-Claim Conflict Re-check' "pull-work documents the Post-Claim Conflict Re-check subsection (F6, AC8)"
373
+ 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)"
374
+ 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)"
375
+ 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)"
376
+ require_text "$PULL" 'True mutual exclusion arrives with the provider assignment lease \(#290\) and the .verify-hold. publish gate \(#293\)' "pull-work names #290/#293 as where true mutual exclusion actually arrives (F6, AC8)"
377
+
378
+
379
+ echo ""
380
+ if [[ "$errors" -eq 0 ]]; then
381
+ echo "Pull work liveness preflight integration passed."
382
+ exit 0
383
+ fi
384
+
385
+ echo "Pull work liveness preflight integration failed: $errors issue(s)."
386
+ 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"