@kontourai/flow-agents 2.4.0 → 3.1.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 (195) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +20 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/CHANGELOG.md +38 -0
  5. package/CONTEXT.md +88 -1
  6. package/README.md +4 -0
  7. package/build/src/cli/assignment-provider.d.ts +144 -0
  8. package/build/src/cli/assignment-provider.js +805 -0
  9. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  10. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  11. package/build/src/cli/validate-workflow-artifacts.js +5 -1
  12. package/build/src/cli/workflow-sidecar.d.ts +2 -2
  13. package/build/src/cli/workflow-sidecar.js +549 -145
  14. package/build/src/cli.js +6 -0
  15. package/build/src/lib/flow-resolver.d.ts +12 -6
  16. package/build/src/lib/flow-resolver.js +30 -14
  17. package/build/src/tools/validate-source-tree.js +2 -0
  18. package/context/contracts/artifact-contract.md +2 -0
  19. package/context/contracts/assignment-provider-contract.md +248 -0
  20. package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
  21. package/context/contracts/decision-registry-contract.md +2 -0
  22. package/context/contracts/delivery-contract.md +2 -0
  23. package/context/contracts/execution-contract.md +25 -0
  24. package/context/contracts/governance-adapter-contract.md +2 -0
  25. package/context/contracts/knowledge-store-contract.md +197 -0
  26. package/context/contracts/planning-contract.md +2 -0
  27. package/context/contracts/probe-docs-write-contract.md +187 -0
  28. package/context/contracts/review-contract.md +2 -0
  29. package/context/contracts/sandbox-policy.md +2 -0
  30. package/context/contracts/standing-directives.md +13 -0
  31. package/context/contracts/verification-contract.md +2 -0
  32. package/context/contracts/work-item-contract.md +2 -0
  33. package/context/scripts/hooks/config-protection.js +14 -1
  34. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  35. package/context/scripts/hooks/stop-goal-fit.js +4 -2
  36. package/context/scripts/hooks/workflow-steering.js +42 -0
  37. package/context/settings/assignment-provider-settings.json +33 -0
  38. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  39. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  40. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  41. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  42. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  43. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  44. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  45. package/docs/adr/0007-skill-audit.md +2 -0
  46. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  47. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  48. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  49. package/docs/adr/0011-mcp-posture.md +2 -0
  50. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  51. package/docs/adr/0013-context-lifecycle.md +2 -0
  52. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  54. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  55. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  56. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  57. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  58. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  59. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  60. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +182 -0
  61. package/docs/adr/README.md +49 -0
  62. package/docs/adr/index.md +34 -0
  63. package/docs/context-map.md +1 -0
  64. package/docs/decisions/agent-coordination.md +20 -0
  65. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  66. package/docs/decisions/context-lifecycle.md +18 -0
  67. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  68. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  69. package/docs/decisions/flow-kit.md +20 -0
  70. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  71. package/docs/decisions/graph-knowledge-provider.md +63 -0
  72. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  73. package/docs/decisions/index.md +22 -0
  74. package/docs/decisions/kit-dependency-ownership.md +18 -0
  75. package/docs/decisions/kit-operation-boundary.md +18 -0
  76. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  77. package/docs/decisions/knowledge-store-provider.md +51 -0
  78. package/docs/decisions/kontour-resource-contract.md +18 -0
  79. package/docs/decisions/mcp-posture.md +18 -0
  80. package/docs/decisions/model-routing.md +63 -0
  81. package/docs/decisions/standing-directives.md +66 -0
  82. package/docs/decisions/three-hard-boundary-model.md +18 -0
  83. package/docs/decisions/trust-reconcile.md +20 -0
  84. package/docs/decisions/typescript-source-policy.md +48 -0
  85. package/docs/decisions/workflow-enforcement.md +18 -0
  86. package/docs/decisions/workflow-trust-state.md +20 -0
  87. package/docs/fixture-ownership.md +1 -0
  88. package/docs/workflow-shared-contracts.md +2 -1
  89. package/docs/workflow-usage-guide.md +7 -1
  90. package/evals/ci/run-baseline.sh +10 -0
  91. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  92. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  93. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  94. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  95. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  96. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  97. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  98. package/evals/integration/test_assignment_provider_github.sh +318 -0
  99. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  100. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  101. package/evals/integration/test_current_json_per_actor.sh +516 -0
  102. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  103. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  104. package/evals/integration/test_gate_lockdown.sh +10 -0
  105. package/evals/integration/test_publish_delivery.sh +21 -4
  106. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  107. package/evals/integration/test_pull_work_liveness_preflight.sh +20 -7
  108. package/evals/integration/test_reconcile_soundness.sh +33 -9
  109. package/evals/integration/test_trust_reconcile.sh +9 -8
  110. package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
  111. package/evals/integration/test_workflow_sidecar_writer.sh +91 -3
  112. package/evals/run.sh +14 -0
  113. package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
  114. package/evals/static/test_knowledge_providers.sh +32 -0
  115. package/evals/static/test_workflow_skills.sh +15 -2
  116. package/kits/builder/skills/deliver/SKILL.md +36 -0
  117. package/kits/builder/skills/design-probe/SKILL.md +37 -0
  118. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  119. package/kits/builder/skills/pull-work/SKILL.md +78 -10
  120. package/kits/knowledge/adapters/default-store/index.js +92 -4
  121. package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
  122. package/kits/knowledge/adapters/shared/codec.js +141 -0
  123. package/kits/knowledge/docs/README.md +121 -2
  124. package/kits/knowledge/docs/store-contract.md +112 -4
  125. package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
  126. package/kits/knowledge/flows/promote.flow.json +84 -0
  127. package/kits/knowledge/kit.json +50 -0
  128. package/kits/knowledge/promote/distill.js +96 -0
  129. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  130. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  131. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  132. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  133. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  134. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  135. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  136. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  137. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  138. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  139. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  140. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  141. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  142. package/kits/knowledge/promote/health.js +137 -0
  143. package/kits/knowledge/promote/index.js +176 -0
  144. package/kits/knowledge/promote/ingest.js +103 -0
  145. package/kits/knowledge/promote/lib.js +132 -0
  146. package/kits/knowledge/promote/link.js +84 -0
  147. package/kits/knowledge/promote/promote.test.js +174 -0
  148. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  149. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  150. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  151. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  152. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  153. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  154. package/kits/knowledge/providers/conformance/suite.test.js +143 -0
  155. package/kits/knowledge/providers/git-repo/index.js +236 -0
  156. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  157. package/kits/knowledge/providers/health/index.js +153 -0
  158. package/kits/knowledge/providers/index.js +25 -0
  159. package/kits/knowledge/providers/lib/model.js +91 -0
  160. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  161. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  162. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  163. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  164. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  165. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  166. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  167. package/kits/knowledge/providers/neo4j/index.js +280 -0
  168. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  169. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  170. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  171. package/kits/knowledge/providers/work-item/index.js +204 -0
  172. package/package.json +7 -3
  173. package/schemas/assignment-provider-settings.schema.json +125 -0
  174. package/schemas/knowledge/edge.schema.json +54 -0
  175. package/schemas/knowledge/health-report.schema.json +45 -0
  176. package/schemas/knowledge/node.schema.json +49 -0
  177. package/schemas/knowledge/proposal.schema.json +53 -0
  178. package/scripts/README.md +1 -0
  179. package/scripts/ci/trust-reconcile.js +521 -24
  180. package/scripts/freeze-adrs.mjs +364 -0
  181. package/scripts/hooks/config-protection.js +14 -1
  182. package/scripts/hooks/evidence-capture.js +4 -1
  183. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  184. package/scripts/hooks/lib/current-pointer.js +123 -0
  185. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  186. package/scripts/hooks/stop-goal-fit.js +4 -2
  187. package/scripts/hooks/workflow-steering.js +42 -0
  188. package/scripts/statusline/flow-agents-statusline.js +3 -1
  189. package/src/cli/assignment-provider.ts +927 -0
  190. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  191. package/src/cli/validate-workflow-artifacts.ts +5 -1
  192. package/src/cli/workflow-sidecar.ts +577 -139
  193. package/src/cli.ts +6 -0
  194. package/src/lib/flow-resolver.ts +35 -14
  195. package/src/tools/validate-source-tree.ts +2 -0
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bash
2
+ # test_assignment_provider_local_file.sh — AssignmentProvider local-file round-trip (#290).
3
+ #
4
+ # Fully deterministic: exercises `assignment-provider claim|status|supersede|release|list
5
+ # --provider local-file` against a temp artifact root and two fixed test-actor fixtures. No
6
+ # network, no `gh` process, no live provider. Follows test_pull_work_provider.sh's
7
+ # pass/fail/json_query idiom exactly.
8
+ #
9
+ # Supports AC5 (local-file round-trip), AC6 (list --actor), AC7 (concurrent claim never
10
+ # silently overwrites), AC9 (no live gh process — none invoked here at all).
11
+ set -uo pipefail
12
+
13
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
14
+ CLI="$ROOT/build/src/cli.js"
15
+ FIXTURES="$ROOT/evals/fixtures/assignment-provider"
16
+ ACTOR_A="$FIXTURES/actor-a.json"
17
+ ACTOR_B="$FIXTURES/actor-b.json"
18
+ SUBJECT_ID="kontourai/flow-agents#9001"
19
+ BRANCH_A="agent/claude-code-eval-actor-a-session-eval-host/flow-agents-9001"
20
+ ARTIFACT_DIR_A=".kontourai/flow-agents/flow-agents-9001"
21
+
22
+ TMPDIR_EVAL="$(mktemp -d)"
23
+ trap 'rm -rf "$TMPDIR_EVAL"' EXIT
24
+ ARTIFACT_ROOT="$TMPDIR_EVAL/artifact-root"
25
+
26
+ errors=0
27
+ pass() { echo " ✓ $1"; }
28
+ fail() { echo " ✗ $1"; errors=$((errors + 1)); }
29
+
30
+ json_query() {
31
+ node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); for (const part of process.argv[2].split(".")) cur=part==="length" ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]); console.log(cur);' "$1" "$2"
32
+ }
33
+
34
+ if [[ ! -f "$CLI" ]]; then
35
+ echo "build/src/cli.js not found — run 'npm run build' first" >&2
36
+ exit 1
37
+ fi
38
+
39
+ echo "=== AssignmentProvider: local-file round-trip ==="
40
+
41
+ # 1. claim writes the exact versioned record shape (Design Decision 2) for a fixed test actor.
42
+ node "$CLI" assignment-provider claim \
43
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
44
+ --subject-id "$SUBJECT_ID" \
45
+ --actor-json "$ACTOR_A" \
46
+ --branch "$BRANCH_A" \
47
+ --artifact-dir "$ARTIFACT_DIR_A" \
48
+ --ttl-seconds 1800 \
49
+ > "$TMPDIR_EVAL/claim-a.json"
50
+ status=$?
51
+ [[ "$status" -eq 0 ]] && pass "claim exits successfully" || fail "claim exits successfully"
52
+
53
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.schema_version")" == "1.0" ]] && pass "claim record schema_version is 1.0" || fail "claim record schema_version is 1.0"
54
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.role")" == "AssignmentClaimRecord" ]] && pass "claim record role is AssignmentClaimRecord" || fail "claim record role is AssignmentClaimRecord"
55
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.subject_id")" == "$SUBJECT_ID" ]] && pass "claim record subject_id matches" || fail "claim record subject_id matches"
56
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.actor.session_id")" == "eval-actor-a-session" ]] && pass "claim record actor matches fixed test actor A" || fail "claim record actor matches fixed test actor A"
57
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.ttl_seconds")" == "1800" ]] && pass "claim record ttl_seconds matches" || fail "claim record ttl_seconds matches"
58
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.branch")" == "$BRANCH_A" ]] && pass "claim record branch matches" || fail "claim record branch matches"
59
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.artifact_dir")" == "$ARTIFACT_DIR_A" ]] && pass "claim record artifact_dir matches" || fail "claim record artifact_dir matches"
60
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.status")" == "claimed" ]] && pass "claim record status is claimed" || fail "claim record status is claimed"
61
+ [[ "$(json_query "$TMPDIR_EVAL/claim-a.json" "record.audit_trail.0.transition")" == "claim" ]] && pass "claim record audit_trail records the claim transition" || fail "claim record audit_trail records the claim transition"
62
+
63
+ CLAIMED_AT="$(json_query "$TMPDIR_EVAL/claim-a.json" "record.claimed_at")"
64
+
65
+ # 2. status returns actor/claimedAt matching what claim wrote.
66
+ node "$CLI" assignment-provider status \
67
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
68
+ > "$TMPDIR_EVAL/status-a.json"
69
+ [[ "$(json_query "$TMPDIR_EVAL/status-a.json" "assignment.assignee")" == "claude-code:eval-actor-a-session:eval-host" ]] && pass "status reports actor A as assignee" || fail "status reports actor A as assignee"
70
+ [[ "$(json_query "$TMPDIR_EVAL/status-a.json" "assignment.record.claimed_at")" == "$CLAIMED_AT" ]] && pass "status claimed_at matches claim's claimed_at" || fail "status claimed_at matches claim's claimed_at"
71
+
72
+ # 3. supersede A -> B updates the actor and records an audit trail entry.
73
+ node "$CLI" assignment-provider supersede \
74
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
75
+ --from-actor-json "$ACTOR_A" --to-actor-json "$ACTOR_B" --reason "handoff-eval" \
76
+ > "$TMPDIR_EVAL/supersede.json"
77
+ status=$?
78
+ [[ "$status" -eq 0 ]] && pass "supersede exits successfully" || fail "supersede exits successfully"
79
+ [[ "$(json_query "$TMPDIR_EVAL/supersede.json" "record.actor.session_id")" == "eval-actor-b-session" ]] && pass "supersede updates the record's actor to B" || fail "supersede updates the record's actor to B"
80
+ [[ "$(json_query "$TMPDIR_EVAL/supersede.json" "record.audit_trail.length")" == "2" ]] && pass "supersede appends to the audit trail (claim + supersede)" || fail "supersede appends to the audit trail (claim + supersede)"
81
+ [[ "$(json_query "$TMPDIR_EVAL/supersede.json" "record.audit_trail.1.transition")" == "supersede" ]] && pass "audit trail records the supersede transition" || fail "audit trail records the supersede transition"
82
+ [[ "$(json_query "$TMPDIR_EVAL/supersede.json" "record.audit_trail.1.from_actor.session_id")" == "eval-actor-a-session" ]] && pass "audit trail supersede entry records from_actor A" || fail "audit trail supersede entry records from_actor A"
83
+ [[ "$(json_query "$TMPDIR_EVAL/supersede.json" "record.audit_trail.1.to_actor.session_id")" == "eval-actor-b-session" ]] && pass "audit trail supersede entry records to_actor B" || fail "audit trail supersede entry records to_actor B"
84
+ [[ "$(json_query "$TMPDIR_EVAL/supersede.json" "record.audit_trail.1.reason")" == "handoff-eval" ]] && pass "audit trail supersede entry records reason" || fail "audit trail supersede entry records reason"
85
+
86
+ node "$CLI" assignment-provider status \
87
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
88
+ > "$TMPDIR_EVAL/status-b.json"
89
+ [[ "$(json_query "$TMPDIR_EVAL/status-b.json" "assignment.assignee")" == "claude-code:eval-actor-b-session:eval-host" ]] && pass "status reports actor B after supersede" || fail "status reports actor B after supersede"
90
+
91
+ # 4. release clears the claim; subsequent status reports free.
92
+ node "$CLI" assignment-provider release \
93
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
94
+ --actor-json "$ACTOR_B" \
95
+ > "$TMPDIR_EVAL/release.json"
96
+ status=$?
97
+ [[ "$status" -eq 0 ]] && pass "release exits successfully" || fail "release exits successfully"
98
+ [[ "$(json_query "$TMPDIR_EVAL/release.json" "record.status")" == "released" ]] && pass "release sets record status to released" || fail "release sets record status to released"
99
+
100
+ node "$CLI" assignment-provider status \
101
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
102
+ --liveness-events-json <(echo '[]') \
103
+ > "$TMPDIR_EVAL/status-free.json"
104
+ [[ "$(json_query "$TMPDIR_EVAL/status-free.json" "assignment.assignee")" == "null" ]] && pass "status reports no assignee after release" || fail "status reports no assignee after release"
105
+ [[ "$(json_query "$TMPDIR_EVAL/status-free.json" "effective.effective_state")" == "free" ]] && pass "status reports effective_state free after release" || fail "status reports effective_state free after release"
106
+
107
+ # 5. list --actor returns exactly that actor's claimed subject ids (claim a second subject
108
+ # under actor A, then verify list --actor A returns exactly that one subject).
109
+ SUBJECT_ID_2="kontourai/flow-agents#9002"
110
+ node "$CLI" assignment-provider claim \
111
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
112
+ --subject-id "$SUBJECT_ID_2" \
113
+ --actor-json "$ACTOR_A" \
114
+ --branch "agent/claude-code-eval-actor-a-session-eval-host/flow-agents-9002" \
115
+ --artifact-dir ".kontourai/flow-agents/flow-agents-9002" \
116
+ > /dev/null
117
+ node "$CLI" assignment-provider list \
118
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --actor-json "$ACTOR_A" \
119
+ > "$TMPDIR_EVAL/list-a.json"
120
+ [[ "$(json_query "$TMPDIR_EVAL/list-a.json" "subject_ids.length")" == "1" ]] && pass "list --actor A returns exactly one subject id" || fail "list --actor A returns exactly one subject id"
121
+ [[ "$(json_query "$TMPDIR_EVAL/list-a.json" "subject_ids.0")" == "$SUBJECT_ID_2" ]] && pass "list --actor A returns actor A's currently-claimed subject (released subject excluded)" || fail "list --actor A returns actor A's currently-claimed subject (released subject excluded)"
122
+
123
+ node "$CLI" assignment-provider list \
124
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --actor-json "$ACTOR_B" \
125
+ > "$TMPDIR_EVAL/list-b.json"
126
+ [[ "$(json_query "$TMPDIR_EVAL/list-b.json" "subject_ids.length")" == "0" ]] && pass "list --actor B returns no subject ids (its only claim was released)" || fail "list --actor B returns no subject ids (its only claim was released)"
127
+
128
+ # 6. Two concurrent claims on the same subject from different actors: the second call must
129
+ # fail loud (non-zero exit + holder-identifying error), never silently overwrite the first
130
+ # (AC7; artifact-contract.md's "fail loud, never fail-open" persistence rule).
131
+ node "$CLI" assignment-provider claim \
132
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
133
+ --subject-id "$SUBJECT_ID_2" \
134
+ --actor-json "$ACTOR_B" \
135
+ --branch "agent/claude-code-eval-actor-b-session-eval-host/flow-agents-9002" \
136
+ --artifact-dir ".kontourai/flow-agents/flow-agents-9002" \
137
+ > "$TMPDIR_EVAL/concurrent-claim.json" 2> "$TMPDIR_EVAL/concurrent-claim.err"
138
+ concurrent_status=$?
139
+ [[ "$concurrent_status" -ne 0 ]] && pass "second claim from a different actor fails loud (non-zero exit)" || fail "second claim from a different actor fails loud (non-zero exit)"
140
+ grep -q "already claimed by a different actor" "$TMPDIR_EVAL/concurrent-claim.err" && pass "second claim's error identifies the existing holder" || fail "second claim's error identifies the existing holder"
141
+
142
+ node "$CLI" assignment-provider status \
143
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID_2" \
144
+ > "$TMPDIR_EVAL/status-after-concurrent.json"
145
+ [[ "$(json_query "$TMPDIR_EVAL/status-after-concurrent.json" "assignment.assignee")" == "claude-code:eval-actor-a-session:eval-host" ]] && pass "record still shows actor A as holder — no silent overwrite" || fail "record still shows actor A as holder — no silent overwrite"
146
+
147
+ # Same-actor re-claim (refresh before TTL expiry) is allowed and idempotent — must not error.
148
+ node "$CLI" assignment-provider claim \
149
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
150
+ --subject-id "$SUBJECT_ID_2" \
151
+ --actor-json "$ACTOR_A" \
152
+ --branch "agent/claude-code-eval-actor-a-session-eval-host/flow-agents-9002" \
153
+ --artifact-dir ".kontourai/flow-agents/flow-agents-9002" \
154
+ > /dev/null 2>&1
155
+ [[ $? -eq 0 ]] && pass "same-actor re-claim (refresh) succeeds without error" || fail "same-actor re-claim (refresh) succeeds without error"
156
+
157
+ echo ""
158
+
159
+ # 7. F1 fix-plan iteration 1 (CRITICAL): GENUINE OS-process concurrency — two REAL,
160
+ # concurrently-launched `claim` processes (background `&` + `wait`, not the sequential case in
161
+ # section 6 above which never actually races) targeting the SAME fresh subject from DIFFERENT
162
+ # actors. Before the withSubjectLock fix, both processes could read "no existing claim" before
163
+ # either wrote, and one write would silently clobber the other (reproduced 29/40 races against
164
+ # the built CLI pre-fix). Assert EXACTLY ONE process exits 0, and the on-disk record shows
165
+ # exactly one holder with an intact, single-entry audit trail (no lost write).
166
+ SUBJECT_ID_RACE="kontourai/flow-agents#9003"
167
+ BRANCH_RACE_A="agent/claude-code-eval-actor-a-session-eval-host/flow-agents-9003"
168
+ BRANCH_RACE_B="agent/claude-code-eval-actor-b-session-eval-host/flow-agents-9003"
169
+ ARTIFACT_DIR_RACE=".kontourai/flow-agents/flow-agents-9003"
170
+
171
+ (
172
+ node "$CLI" assignment-provider claim \
173
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
174
+ --subject-id "$SUBJECT_ID_RACE" \
175
+ --actor-json "$ACTOR_A" \
176
+ --branch "$BRANCH_RACE_A" \
177
+ --artifact-dir "$ARTIFACT_DIR_RACE" \
178
+ > "$TMPDIR_EVAL/race-a.out" 2> "$TMPDIR_EVAL/race-a.err"
179
+ echo $? > "$TMPDIR_EVAL/race-a.rc"
180
+ ) &
181
+ PID_A=$!
182
+ (
183
+ node "$CLI" assignment-provider claim \
184
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" \
185
+ --subject-id "$SUBJECT_ID_RACE" \
186
+ --actor-json "$ACTOR_B" \
187
+ --branch "$BRANCH_RACE_B" \
188
+ --artifact-dir "$ARTIFACT_DIR_RACE" \
189
+ > "$TMPDIR_EVAL/race-b.out" 2> "$TMPDIR_EVAL/race-b.err"
190
+ echo $? > "$TMPDIR_EVAL/race-b.rc"
191
+ ) &
192
+ PID_B=$!
193
+ wait "$PID_A" "$PID_B"
194
+
195
+ RACE_RC_A="$(cat "$TMPDIR_EVAL/race-a.rc")"
196
+ RACE_RC_B="$(cat "$TMPDIR_EVAL/race-b.rc")"
197
+
198
+ if { [[ "$RACE_RC_A" -eq 0 && "$RACE_RC_B" -ne 0 ]] || [[ "$RACE_RC_A" -ne 0 && "$RACE_RC_B" -eq 0 ]]; }; then
199
+ pass "genuine concurrent claim race: exactly one process wins (rc_a=$RACE_RC_A rc_b=$RACE_RC_B)"
200
+ else
201
+ fail "genuine concurrent claim race: exactly one process wins (rc_a=$RACE_RC_A rc_b=$RACE_RC_B)"
202
+ fi
203
+
204
+ node "$CLI" assignment-provider status \
205
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID_RACE" \
206
+ > "$TMPDIR_EVAL/status-race.json"
207
+ [[ "$(json_query "$TMPDIR_EVAL/status-race.json" "assignment.record.audit_trail.length")" == "1" ]] && pass "race winner's on-disk record has exactly one audit_trail entry (no lost/merged write)" || fail "race winner's on-disk record has exactly one audit_trail entry (no lost/merged write)"
208
+
209
+ if [[ "$RACE_RC_A" -eq 0 ]]; then
210
+ EXPECTED_RACE_HOLDER="claude-code:eval-actor-a-session:eval-host"
211
+ else
212
+ EXPECTED_RACE_HOLDER="claude-code:eval-actor-b-session:eval-host"
213
+ fi
214
+ [[ "$(json_query "$TMPDIR_EVAL/status-race.json" "assignment.assignee")" == "$EXPECTED_RACE_HOLDER" ]] && pass "on-disk record holder matches the process that actually exited 0 (no silent overwrite by the loser)" || fail "on-disk record holder matches the process that actually exited 0 (no silent overwrite by the loser)"
215
+
216
+ echo ""
217
+ if [[ "$errors" -eq 0 ]]; then
218
+ echo "test_assignment_provider_local_file: all checks passed."
219
+ else
220
+ echo "test_assignment_provider_local_file: $errors check(s) failed."
221
+ fi
222
+ exit "$errors"
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env bash
2
+ # test_critique_supersession_roundtrip.sh — Regression evals for the trust-ledger defects
3
+ # fixed under #267 (critique supersession), #268 (lossless, type-stable check/critique
4
+ # round-trip), and #282 (first-class superseded_by affordance).
5
+ #
6
+ # Proves:
7
+ # (a) record-critique twice for the same id (fail → pass, SAME reviewer) supersedes the fail:
8
+ # the effective/live state is the pass, the historical fail is retained (status=superseded,
9
+ # metadata.superseded_by) but excluded from evaluation, no duplicate claim ids, and CI
10
+ # trust-reconcile exits 0.
11
+ # (b) HEADLINE: a --flow-id (builder.build/verify) session — ensure-session → record-evidence
12
+ # (mixed session-local kinds) → record-critique → trust-reconcile — exits 0. This exact flow
13
+ # could never converge before (critique claims were re-absorbed as command-less test_output
14
+ # checks → permanent [not-run] divergence; check kinds collapsed to the declared claimType).
15
+ # (c) record-evidence AFTER a critique preserves the critique history (previously hardcoded []).
16
+ # (d) ANTI-GAMING: a DIFFERENT reviewer cannot supersede a reviewer's fail — the disputed
17
+ # critique stays live and trust-reconcile still exits 1 (worker cannot bury a reviewer finding).
18
+ # (e) VALIDATOR (#282): a top-level pass tolerates a superseded historical fail member, but a
19
+ # LIVE (non-superseded) fail still triggers "required critique must pass".
20
+ #
21
+ # Deterministic, no model spend, self-cleaning.
22
+ # Usage: bash evals/integration/test_critique_supersession_roundtrip.sh
23
+
24
+ set -uo pipefail
25
+
26
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
27
+ source "$ROOT/evals/lib/node.sh"
28
+ RECON="$ROOT/scripts/ci/trust-reconcile.js"
29
+ WRITER="workflow-sidecar"
30
+
31
+ TMP="$(mktemp -d)"
32
+ errors=0
33
+ _pass() { echo " PASS: $1"; }
34
+ _fail() { echo " FAIL: $1"; errors=$((errors + 1)); }
35
+ cleanup() { rm -rf "$TMP"; }
36
+ trap cleanup EXIT
37
+
38
+ # seed <aroot> <slug> [flow-args...] — ensure-session + init-plan
39
+ seed() {
40
+ local aroot="$1"; local slug="$2"; shift 2
41
+ mkdir -p "$aroot"
42
+ flow_agents_node "$WRITER" ensure-session --artifact-root "$aroot" --task-slug "$slug" \
43
+ --title "T" --summary "S" --timestamp "2026-07-01T00:00:00Z" "$@" >/dev/null 2>&1
44
+ flow_agents_node "$WRITER" init-plan "$aroot/$slug/$slug--deliver.md" \
45
+ --source-request "R" --summary "S" --timestamp "2026-07-01T00:00:00Z" >/dev/null 2>&1
46
+ }
47
+
48
+ # ─── (a) same-reviewer supersession: fail → pass ──────────────────────────────
49
+ echo ""
50
+ echo "=== (a) record-critique fail→pass (same reviewer) supersedes, history intact, reconcile 0 ==="
51
+ A_AROOT="$TMP/a/aroot"; A_SLUG="supersede-same"; A_DIR="$A_AROOT/$A_SLUG"
52
+ seed "$A_AROOT" "$A_SLUG"
53
+ flow_agents_node "$WRITER" record-evidence "$A_DIR" --verdict pass \
54
+ --check-json '{"id":"c1","kind":"diff","status":"pass","summary":"diff check"}' \
55
+ --timestamp "2026-07-01T00:01:00Z" >/dev/null 2>&1
56
+ flow_agents_node "$WRITER" record-critique "$A_DIR" --id rv --reviewer alice --verdict fail \
57
+ --summary "found a bug" --timestamp "2026-07-01T00:02:00Z" >/dev/null 2>&1
58
+ flow_agents_node "$WRITER" record-critique "$A_DIR" --id rv --reviewer alice --verdict pass \
59
+ --summary "bug fixed" --timestamp "2026-07-01T00:03:00Z" >/dev/null 2>&1
60
+
61
+ node - "$A_DIR/trust.bundle" << 'NODE'
62
+ const fs = require('fs');
63
+ const b = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
64
+ const crit = (b.claims||[]).filter(c => c.metadata && c.metadata.origin === 'critique');
65
+ const live = crit.filter(c => !c.metadata.superseded_by);
66
+ const hist = crit.filter(c => c.metadata.superseded_by);
67
+ if (crit.length !== 2) throw new Error('expected 2 critique claims (1 live + 1 history), got ' + crit.length);
68
+ if (live.length !== 1 || live[0].value !== 'pass') throw new Error('expected exactly 1 LIVE critique with value=pass, got ' + JSON.stringify(live.map(c=>c.value)));
69
+ if (hist.length !== 1 || hist[0].value !== 'fail' || hist[0].status !== 'superseded') throw new Error('expected historical fail with status=superseded');
70
+ if (!hist[0].metadata.superseded_by) throw new Error('history missing first-class metadata.superseded_by');
71
+ const ids = (b.claims||[]).map(c=>c.id); const dup = ids.filter((x,i)=>ids.indexOf(x)!==i);
72
+ if (dup.length) throw new Error('duplicate claim ids in bundle: ' + dup.join(','));
73
+ console.log('live=pass, history=fail(superseded), no duplicate ids');
74
+ NODE
75
+ if [[ $? -eq 0 ]]; then _pass "(a) effective state=pass, historical fail retained+superseded, no dup ids"; else _fail "(a) supersession/history assertion failed"; fi
76
+
77
+ TRUST_RECONCILE_COMMANDS="true" node "$RECON" --bundle "$A_DIR/trust.bundle" --repo-root "$TMP/a" >"$TMP/a-recon.log" 2>&1
78
+ if [[ $? -eq 0 ]]; then _pass "(a) trust-reconcile exits 0 (resolved session converges)"; else _fail "(a) trust-reconcile did NOT converge: $(cat "$TMP/a-recon.log")"; fi
79
+
80
+ # ─── (b) HEADLINE: --flow-id session converges ────────────────────────────────
81
+ echo ""
82
+ echo "=== (b) HEADLINE --flow-id builder.build/verify: mixed evidence + critique → reconcile 0 ==="
83
+ B_AROOT="$TMP/b/aroot"; B_SLUG="flowid-converge"; B_DIR="$B_AROOT/$B_SLUG"
84
+ seed "$B_AROOT" "$B_SLUG" --flow-id builder.build --step-id verify
85
+ flow_agents_node "$WRITER" record-evidence "$B_DIR" --verdict pass \
86
+ --check-json '{"id":"k-diff","kind":"diff","status":"pass","summary":"diff excerpt"}' \
87
+ --check-json '{"id":"k-policy","kind":"policy","status":"pass","summary":"policy rule"}' \
88
+ --check-json '{"id":"k-ext","kind":"external","status":"pass","summary":"attested"}' \
89
+ --timestamp "2026-07-01T00:01:00Z" >/dev/null 2>&1
90
+ flow_agents_node "$WRITER" record-critique "$B_DIR" --id code-review --reviewer alice --verdict pass \
91
+ --summary "looks good" --timestamp "2026-07-01T00:02:00Z" >/dev/null 2>&1
92
+
93
+ node - "$B_DIR/trust.bundle" << 'NODE'
94
+ const fs = require('fs');
95
+ const b = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
96
+ // Type stability: no critique claim may be re-absorbed as a check, and check kinds/evidenceTypes
97
+ // must survive the round-trip (a session-local kind must NOT flip to test_output).
98
+ const checks = (b.claims||[]).filter(c => c.metadata && c.metadata.origin === 'check');
99
+ const crit = (b.claims||[]).filter(c => c.metadata && c.metadata.origin === 'critique');
100
+ if (crit.length !== 1) throw new Error('expected exactly 1 critique claim (no re-absorption/duplication), got ' + crit.length);
101
+ const evByClaim = {}; for (const e of (b.evidence||[])) (evByClaim[e.claimId] ||= []).push(e);
102
+ for (const c of checks) {
103
+ const ets = (evByClaim[c.id]||[]).map(e=>e.evidenceType);
104
+ if (ets.includes('test_output')) throw new Error('session-local check ' + c.id + ' flipped to test_output evidence (round-trip kind instability)');
105
+ }
106
+ console.log(checks.length + ' check claims, ' + crit.length + ' critique claim; no test_output flip');
107
+ NODE
108
+ if [[ $? -eq 0 ]]; then _pass "(b) round-trip type-stable under --flow-id (no critique→check absorption, no evidenceType flip)"; else _fail "(b) round-trip type instability under --flow-id"; fi
109
+
110
+ TRUST_RECONCILE_COMMANDS="true" node "$RECON" --bundle "$B_DIR/trust.bundle" --repo-root "$TMP/b" >"$TMP/b-recon.log" 2>&1
111
+ if [[ $? -eq 0 ]]; then _pass "(b) HEADLINE: --flow-id session trust-reconcile exits 0 (converges)"; else _fail "(b) HEADLINE --flow-id session did NOT converge: $(cat "$TMP/b-recon.log")"; fi
112
+
113
+ # ─── (c) record-evidence after critiques preserves history ────────────────────
114
+ echo ""
115
+ echo "=== (c) record-evidence AFTER a critique preserves critique history (#268) ==="
116
+ flow_agents_node "$WRITER" record-evidence "$B_DIR" --verdict pass \
117
+ --check-json '{"id":"k-diff2","kind":"diff","status":"pass","summary":"another diff"}' \
118
+ --timestamp "2026-07-01T00:04:00Z" >/dev/null 2>&1
119
+ node - "$B_DIR/trust.bundle" << 'NODE'
120
+ const fs = require('fs');
121
+ const b = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
122
+ const crit = (b.claims||[]).filter(c => c.metadata && c.metadata.origin === 'critique');
123
+ if (crit.length !== 1) throw new Error('critique history dropped by record-evidence — expected 1, got ' + crit.length);
124
+ if (crit[0].value !== 'pass') throw new Error('critique value changed');
125
+ console.log('critique survived record-evidence');
126
+ NODE
127
+ if [[ $? -eq 0 ]]; then _pass "(c) critique history survives a later record-evidence call"; else _fail "(c) record-evidence dropped critique history"; fi
128
+
129
+ # ─── (d) ANTI-GAMING: cross-reviewer cannot supersede a reviewer fail ─────────
130
+ echo ""
131
+ echo "=== (d) ANTI-GAMING: a different reviewer cannot supersede a reviewer's fail ==="
132
+ D_AROOT="$TMP/d/aroot"; D_SLUG="supersede-cross"; D_DIR="$D_AROOT/$D_SLUG"
133
+ seed "$D_AROOT" "$D_SLUG"
134
+ flow_agents_node "$WRITER" record-evidence "$D_DIR" --verdict pass \
135
+ --check-json '{"id":"c1","kind":"diff","status":"pass","summary":"diff check"}' \
136
+ --timestamp "2026-07-01T00:01:00Z" >/dev/null 2>&1
137
+ flow_agents_node "$WRITER" record-critique "$D_DIR" --id rv --reviewer reviewer-bob --verdict fail \
138
+ --summary "reviewer found a real bug" --timestamp "2026-07-01T00:02:00Z" >/dev/null 2>&1
139
+ flow_agents_node "$WRITER" record-critique "$D_DIR" --id rv --reviewer worker-mallory --verdict pass \
140
+ --summary "worker claims fixed" --timestamp "2026-07-01T00:03:00Z" >/dev/null 2>&1
141
+ node - "$D_DIR/trust.bundle" << 'NODE'
142
+ const fs = require('fs');
143
+ const b = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
144
+ const crit = (b.claims||[]).filter(c => c.metadata && c.metadata.origin === 'critique');
145
+ const live = crit.filter(c => !c.metadata.superseded_by);
146
+ const liveFail = live.filter(c => c.value === 'fail' || c.status === 'disputed');
147
+ if (!liveFail.length) throw new Error('ANTI-GAMING VIOLATION: reviewer fail was superseded by a different reviewer');
148
+ if (crit.some(c => c.metadata.superseded_by)) throw new Error('unexpected supersession across reviewers');
149
+ console.log('reviewer fail stays LIVE (' + liveFail.length + '); cross-reviewer supersession refused');
150
+ NODE
151
+ if [[ $? -eq 0 ]]; then _pass "(d) reviewer's fail is NOT superseded by a different reviewer (stays live)"; else _fail "(d) ANTI-GAMING regression: cross-reviewer supersession occurred"; fi
152
+ TRUST_RECONCILE_COMMANDS="true" node "$RECON" --bundle "$D_DIR/trust.bundle" --repo-root "$TMP/d" >"$TMP/d-recon.log" 2>&1
153
+ if [[ $? -ne 0 ]] && grep -q "session-local-failed" "$TMP/d-recon.log"; then _pass "(d) trust-reconcile still exits 1 (live disputed critique blocks)"; else _fail "(d) reconcile should have blocked on the live reviewer fail"; fi
154
+
155
+ # ─── (e) VALIDATOR (#282): superseded fail tolerated, live fail blocks ────────
156
+ echo ""
157
+ echo "=== (e) VALIDATOR: superseded historical fail tolerated; live fail blocks (#282) ==="
158
+ # Reuse (a)'s superseded bundle: pass top-level with a superseded fail member.
159
+ va_out="$(flow_agents_node validate-workflow-artifacts "$A_AROOT" --require-sidecars --require-critique 2>&1)"
160
+ if echo "$va_out" | grep -q "required critique must pass"; then
161
+ _fail "(e) validator wrongly rejected a superseded historical fail"
162
+ else
163
+ _pass "(e) validator tolerates a superseded historical fail member (no 'required critique must pass')"
164
+ fi
165
+ # Live fail (d's cross-reviewer bundle has a live disputed critique) must still be rejected.
166
+ vd_out="$(flow_agents_node validate-workflow-artifacts "$D_AROOT" --require-sidecars --require-critique 2>&1)"
167
+ if echo "$vd_out" | grep -q "required critique must pass"; then
168
+ _pass "(e) validator still rejects a LIVE (non-superseded) fail critique"
169
+ else
170
+ _fail "(e) validator failed to reject a live fail critique"
171
+ fi
172
+
173
+ # ─── Summary ──────────────────────────────────────────────────────────────────
174
+ echo ""
175
+ echo "────────────────────────────────────────────"
176
+ if [[ $errors -eq 0 ]]; then
177
+ echo "test_critique_supersession_roundtrip: all checks passed."
178
+ exit 0
179
+ else
180
+ echo "test_critique_supersession_roundtrip: $errors check(s) failed."
181
+ exit 1
182
+ fi