@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,132 @@
1
+ #!/usr/bin/env bash
2
+ # test_pull_work_assignment_join.sh — Two-session disjoint-selection simulation (#290).
3
+ #
4
+ # HONESTY NOTE (plan Design Decision / Stop-Short Risks): this eval simulates the assignment ⋈
5
+ # liveness join's exclusion property with two actor structs evaluating the same candidate
6
+ # subject id fixture in sequence, inside ONE process. It does NOT stand up two real concurrent
7
+ # agent runtimes, and does not prove true concurrency (a real race between two simultaneous
8
+ # writers). It is the closest *local* proof available for the issue's literal acceptance line
9
+ # ("two sessions running pull-work against the same backlog select disjoint issues") without a
10
+ # multi-runtime test harness this repo does not have — see the plan's Stop-Short Risks section.
11
+ # What IS proven here, deterministically: once session A's assignment claim is recorded (local-
12
+ # file or a rendered-then-status-confirmed GitHub fixture), session B's subsequent
13
+ # `assignment-provider status` / join check for the SAME subject reports it `held` (excluded),
14
+ # never `free` — so `pull-work`'s selection loop (kits/builder/skills/pull-work/SKILL.md,
15
+ # "### 1. Read Board State") would skip it for session B.
16
+ #
17
+ # Supports AC12 (disjoint-selection-simulated), AC10 (docs pointer — see
18
+ # docs/workflow-usage-guide.md's new Assignment Ownership subsection).
19
+ set -uo pipefail
20
+
21
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22
+ CLI="$ROOT/build/src/cli.js"
23
+ FIXTURES="$ROOT/evals/fixtures/assignment-provider"
24
+ ACTOR_A="$FIXTURES/actor-a.json"
25
+ ACTOR_B="$FIXTURES/actor-b.json"
26
+ ISSUE_CLAIMED="$FIXTURES/github-issue-claimed.json"
27
+ LIVENESS_FRESH="$FIXTURES/liveness-fresh.json"
28
+
29
+ TMPDIR_EVAL="$(mktemp -d)"
30
+ trap 'rm -rf "$TMPDIR_EVAL"' EXIT
31
+ ARTIFACT_ROOT="$TMPDIR_EVAL/artifact-root"
32
+
33
+ errors=0
34
+ pass() { echo " ✓ $1"; }
35
+ fail() { echo " ✗ $1"; errors=$((errors + 1)); }
36
+
37
+ json_query() {
38
+ 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"
39
+ }
40
+
41
+ if [[ ! -f "$CLI" ]]; then
42
+ echo "build/src/cli.js not found — run 'npm run build' first" >&2
43
+ exit 1
44
+ fi
45
+
46
+ echo "=== Two-session disjoint-selection simulation (assignment ⋈ liveness join) ==="
47
+
48
+ EMPTY_LIVENESS="$TMPDIR_EVAL/liveness-empty.json"
49
+ echo '[]' > "$EMPTY_LIVENESS"
50
+
51
+ # --- local-file leg: session A claims, session B's join check on the same subject is excluded ---
52
+
53
+ SUBJECT_ID="kontourai/flow-agents#9301"
54
+
55
+ # "Session A" (actor A) evaluates the candidate subject before anyone has claimed it: free.
56
+ node "$CLI" assignment-provider status \
57
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
58
+ --self-actor "claude-code:eval-actor-a-session:eval-host" --liveness-events-json "$EMPTY_LIVENESS" \
59
+ > "$TMPDIR_EVAL/session-a-preclaim.json"
60
+ [[ "$(json_query "$TMPDIR_EVAL/session-a-preclaim.json" "effective.effective_state")" == "free" ]] && pass "local-file: candidate subject is free before either session claims" || fail "local-file: candidate subject is free before either session claims"
61
+
62
+ # Session A selects and claims it (pull-work's "Assignment Claim On Selection" step).
63
+ node "$CLI" assignment-provider claim \
64
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
65
+ --actor-json "$ACTOR_A" --branch "agent/claude-code-eval-actor-a-session-eval-host/flow-agents-9301" \
66
+ --artifact-dir ".kontourai/flow-agents/flow-agents-9301" > /dev/null
67
+ status=$?
68
+ [[ "$status" -eq 0 ]] && pass "local-file: session A's claim on the candidate subject succeeds" || fail "local-file: session A's claim on the candidate subject succeeds"
69
+
70
+ # "Session B" (actor B, a distinct actor struct — the second concurrent pull-work session)
71
+ # evaluates the SAME candidate subject next. Its own liveness heartbeat is fresh (it is alive),
72
+ # but that must not matter: the join is computed against the HOLDER's freshness, not the
73
+ # reader's — session B must see the subject excluded.
74
+ NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
75
+ cat > "$TMPDIR_EVAL/liveness-actor-a-fresh.json" <<JSON
76
+ [
77
+ {"type":"claim","subjectId":"$SUBJECT_ID","actor":"claude-code:eval-actor-a-session:eval-host","at":"$NOW_ISO","ttlSeconds":1800}
78
+ ]
79
+ JSON
80
+ node "$CLI" assignment-provider status \
81
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID" \
82
+ --self-actor "claude-code:eval-actor-b-session:eval-host" \
83
+ --liveness-events-json "$TMPDIR_EVAL/liveness-actor-a-fresh.json" > "$TMPDIR_EVAL/session-b-postclaim.json"
84
+ [[ "$(json_query "$TMPDIR_EVAL/session-b-postclaim.json" "assignment.assignee")" == "claude-code:eval-actor-a-session:eval-host" ]] && pass "local-file: session B's status read shows actor A as the holder" || fail "local-file: session B's status read shows actor A as the holder"
85
+ [[ "$(json_query "$TMPDIR_EVAL/session-b-postclaim.json" "effective.effective_state")" == "held" ]] && pass "local-file: session B's join check reports held (excluded) for the same subject session A just claimed" || fail "local-file: session B's join check reports held (excluded) for the same subject session A just claimed"
86
+ [[ "$(json_query "$TMPDIR_EVAL/session-b-postclaim.json" "effective.effective_state")" != "free" ]] && pass "local-file: subject is definitively NOT free for session B (disjoint selection holds)" || fail "local-file: subject is definitively NOT free for session B (disjoint selection holds)"
87
+
88
+ # Session B, correctly excluding the held subject, selects a DIFFERENT subject instead — the
89
+ # literal "select disjoint issues" property, one selection loop iteration at a time.
90
+ SUBJECT_ID_2="kontourai/flow-agents#9302"
91
+ node "$CLI" assignment-provider status \
92
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID_2" \
93
+ --self-actor "claude-code:eval-actor-b-session:eval-host" --liveness-events-json "$EMPTY_LIVENESS" \
94
+ > "$TMPDIR_EVAL/session-b-alternate.json"
95
+ [[ "$(json_query "$TMPDIR_EVAL/session-b-alternate.json" "effective.effective_state")" == "free" ]] && pass "local-file: session B's alternate candidate is free — it can select a disjoint subject" || fail "local-file: session B's alternate candidate is free — it can select a disjoint subject"
96
+ node "$CLI" assignment-provider claim \
97
+ --provider local-file --artifact-root "$ARTIFACT_ROOT" --subject-id "$SUBJECT_ID_2" \
98
+ --actor-json "$ACTOR_B" --branch "agent/claude-code-eval-actor-b-session-eval-host/flow-agents-9302" \
99
+ --artifact-dir ".kontourai/flow-agents/flow-agents-9302" > /dev/null
100
+ [[ $? -eq 0 ]] && pass "local-file: session B claims the disjoint subject successfully" || fail "local-file: session B claims the disjoint subject successfully"
101
+
102
+ # Final proof: the two sessions' claimed subject sets are disjoint.
103
+ node "$CLI" assignment-provider list --provider local-file --artifact-root "$ARTIFACT_ROOT" --actor-json "$ACTOR_A" > "$TMPDIR_EVAL/list-a.json"
104
+ node "$CLI" assignment-provider list --provider local-file --artifact-root "$ARTIFACT_ROOT" --actor-json "$ACTOR_B" > "$TMPDIR_EVAL/list-b.json"
105
+ DISJOINT="$(node -e '
106
+ const fs = require("fs");
107
+ const a = JSON.parse(fs.readFileSync(process.argv[1], "utf8")).subject_ids;
108
+ const b = JSON.parse(fs.readFileSync(process.argv[2], "utf8")).subject_ids;
109
+ const overlap = a.filter((id) => b.includes(id));
110
+ console.log(overlap.length === 0 && a.length === 1 && b.length === 1 ? "yes" : "no");
111
+ ' "$TMPDIR_EVAL/list-a.json" "$TMPDIR_EVAL/list-b.json")"
112
+ [[ "$DISJOINT" == "yes" ]] && pass "session A and session B's claimed subject sets are disjoint (one each, no overlap)" || fail "session A and session B's claimed subject sets are disjoint (one each, no overlap)"
113
+
114
+ # --- GitHub leg: a rendered-and-status-confirmed claim on a fixture also excludes a second reader ---
115
+
116
+ # The already-claimed GitHub fixture stands in for "session A already claimed and the render
117
+ # was executed + status-confirmed" (SKILL.md's "Assignment Claim On Selection" round trip).
118
+ # "Session B" reads the SAME fixture next with a fresh liveness fixture for the holder: held.
119
+ node "$CLI" assignment-provider status --provider github --issue-json "$ISSUE_CLAIMED" \
120
+ --self-actor "claude-code:eval-actor-b-session:eval-host" \
121
+ --liveness-events-json "$LIVENESS_FRESH" --now "2026-06-01T12:20:00Z" \
122
+ > "$TMPDIR_EVAL/github-session-b.json"
123
+ [[ "$(json_query "$TMPDIR_EVAL/github-session-b.json" "effective.effective_state")" == "held" ]] && pass "github: session B's join check on an already-claimed fixture issue reports held (excluded)" || fail "github: session B's join check on an already-claimed fixture issue reports held (excluded)"
124
+ [[ "$(json_query "$TMPDIR_EVAL/github-session-b.json" "effective.effective_state")" != "free" ]] && pass "github: subject is definitively NOT free for session B" || fail "github: subject is definitively NOT free for session B"
125
+
126
+ echo ""
127
+ if [[ "$errors" -eq 0 ]]; then
128
+ echo "test_pull_work_assignment_join: all checks passed."
129
+ else
130
+ echo "test_pull_work_assignment_join: $errors check(s) failed."
131
+ fi
132
+ exit "$errors"
@@ -151,7 +151,12 @@ if flow_agents_node "$WRITER" ensure-session \
151
151
  --title "Parity Check" \
152
152
  --summary "resolve-slug parity fixture." \
153
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)"
154
+ # #291: ensure-session (when an ambient actor resolves, even without an explicit --actor)
155
+ # now also creates artifact-root-level "assignment/" (the ownership-guard's durable local-file
156
+ # claim record dir) and "current/" (the per-actor current.json projection dir) sibling
157
+ # directories, alongside the pre-existing "liveness/" dir -- all three must be excluded here so
158
+ # this find still lands on the real session slug directory, not a sibling artifact dir.
159
+ B_SESSION_DIR="$(find "$B_ROOT" -mindepth 1 -maxdepth 1 -type d ! -name liveness ! -name assignment ! -name current | head -n1)"
155
160
  B_SLUG_FROM_DIR="$(basename "$B_SESSION_DIR")"
156
161
  B_SLUG_FROM_RESOLVE="$(flow_agents_node "$WRITER" resolve-slug 'owner/repo#42' 2>"$TMPDIR_EVAL/b-resolve.err")"
157
162
  if [[ -n "$B_SESSION_DIR" ]] && [[ "$B_SLUG_FROM_DIR" == "$B_SLUG_FROM_RESOLVE" ]]; then
@@ -329,7 +334,7 @@ require_text "$PULL" 'liveness claim <subjectId>' "pull-work references liveness
329
334
  require_text "$PULL" 'reading each row.s raw .status. field \(never .label.\)' "pull-work instructs consuming raw status, never label (AC1, AC8)"
330
335
  require_text "$PULL" 'a .verified. row for an actor other than self.*.held.' "pull-work documents held classification (AC1, AC2, AC8)"
331
336
  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)"
337
+ require_text "$PULL" 'effective_state: .reclaimable.' "pull-work documents reclaimable classification via the assignment-provider join (AC1, AC3, AC8, #290)"
333
338
  require_text "$PULL" 'requires an explicit recorded opt-in' "pull-work requires an explicit recorded opt-in for reclaimable selection (AC3, AC8)"
334
339
  require_text "$PULL" 'never a silent normal pick' "pull-work states reclaimable is never a silent normal pick (AC3, AC8)"
335
340
  require_text "$PULL" 'a .verified. row for the resolved self actor.*.mine.' "pull-work documents mine classification (AC1, AC5, AC8)"
@@ -348,10 +353,15 @@ require_text "$PICKUP_PROBE" 'flags the selected item .reclaimable., treat the r
348
353
  # ─── F2-F6 (fix-plan iteration 1) static assertions ─────────────────────────
349
354
  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
355
  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)"
356
+ # F2's original numbered precedence list was liveness-only (#166); #290 (Wave 3) replaced it with
357
+ # the full ADR 0021 §1 assignment liveness join, computed via `assignment-provider status`'s
358
+ # `effective_state`/`reason` fields. These assertions are updated to match the new five-row list
359
+ # (mine/held/reclaimable/human-held/free) rather than the old four-row liveness-only list.
360
+ require_text "$PULL" '^1\. .effective_state: .held.. with .reason: .self_is_holder..' "pull-work's precedence rule step 1 (mine, via self_is_holder) is numbered verbatim (F2, #290, AC8)"
361
+ require_text "$PULL" '^2\. else .effective_state: .held..' "pull-work's precedence rule step 2 (held, excluded) is numbered verbatim (F2, #290, AC8)"
362
+ require_text "$PULL" '^3\. else .effective_state: .reclaimable..' "pull-work's precedence rule step 3 (reclaimable) is numbered verbatim (F2, #290, AC8)"
363
+ require_text "$PULL" '^4\. else .effective_state: .human-held..' "pull-work's precedence rule step 4 (human-held) is numbered verbatim (F2, #290, AC11, AC8)"
364
+ require_text "$PULL" '^5\. else .effective_state: .free..' "pull-work's precedence rule step 5 (free) is numbered verbatim (F2, #290, AC8)"
355
365
  require_text "$PULL" 'double-hold' "pull-work names the own-actor + other-actor co-existing row combination a double-hold (F2, AC8)"
356
366
  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
367
  require_text "$PULL" 'never silently resolve it to .mine.' "pull-work states a double-hold is never silently resolved to mine (F2, AC8)"
@@ -373,7 +383,10 @@ require_text "$PULL" '### Post-Claim Conflict Re-check' "pull-work documents the
373
383
  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
384
  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
385
  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)"
386
+ require_text "$PULL" '.#290.\x27s provider assignment lease closes this gap for the .{0,2}local-file.{0,2} provider only' "pull-work scopes true mutual exclusion to the local-file provider only, via #290's lock (fix-plan iteration 1, F5, AC8)"
387
+ require_text "$PULL" 'two concurrent local-file .claim. calls on the same subject genuinely cannot both win' "pull-work states local-file claim/claim races genuinely cannot both win under #290's lock (fix-plan iteration 1, F5, AC8)"
388
+ require_text "$PULL" 'The .{0,2}GitHub.{0,2} provider \(assignee/label/claim-comment\) remains advisory/last-writer' "pull-work does NOT overclaim true mutual exclusion for the GitHub provider — advisory/last-writer, detect not prevent (fix-plan iteration 1, F5, AC8)"
389
+ require_text "$PULL" 'Do not read .provider assignment lease. as closing the GitHub race; only the .verify-hold. publish gate \(#293\)' "pull-work explicitly warns not to read #290 as closing the GitHub race — only #293's verify-hold gate would (fix-plan iteration 1, F5, AC8)"
377
390
 
378
391
 
379
392
  echo ""
@@ -9,8 +9,13 @@
9
9
  # B. REAL-VERIFY-CLOSED: build passes but a fake "real verify" (eval:static substitute)
10
10
  # fails → trust-reconcile exits 1 (PRE-FIX this would exit 0 because only build ran).
11
11
  #
12
- # C. REAL-VERIFY-PASSES: the comprehensive verify resolves green exits 0 (legit
13
- # work is not false-blocked when package.json trust-reconcile-verify is present).
12
+ # C. REAL-VERIFY-PASSES: the comprehensive verify resolves green, no bundle, but a
13
+ # well-formed in-scope delivery/DECLARED marker exempts Step 2 (ADR 0022 §1) → exits 0
14
+ # with the DECLARED (no-agent-delivery) line (legit work is not false-blocked; the
15
+ # bundle-absence branch is bundle-required by default post-ADR-0022, so this case is
16
+ # now anchored by a DECLARED marker rather than by bundle absence alone — see
17
+ # evals/integration/test_trust_reconcile_negatives.sh section 7 for the no-marker /
18
+ # out-of-scope-marker fail-closed cases this test does NOT re-cover).
14
19
  #
15
20
  # D. CHECKPOINT-BYPASS-CLOSED: a checkpoint-only bundle (no evidence/claims, statusByClaimId
16
21
  # all-passed) → exits 1 with checkpoint-bypass divergence (not a silent skip).
@@ -56,8 +61,14 @@ fs.writeFileSync('$PKG_NO_VERIFY/package.json', JSON.stringify(pkg, null, 2));
56
61
  "
57
62
 
58
63
  # ─── Minimal package.json WITH trust-reconcile-verify ─────────────────────────
64
+ # ADR 0022 §1 (bundle-required by default): no bundle is written for this fixture, so a
65
+ # well-formed, in-scope delivery/DECLARED marker (ADR 0022 §2) is seeded alongside it —
66
+ # this is the new legitimate no-bundle path (a real human/bot PR author with no agent
67
+ # delivery), preserving TEST C's original "no bundle" shape more faithfully than
68
+ # switching it to a --bundle fixture would (a bundle would test Step 2 reconcile, which
69
+ # TEST C was never about — it proves Step 1 fresh-verify success is honored).
59
70
  PKG_WITH_VERIFY="$TMP/pkg_with_verify"
60
- mkdir -p "$PKG_WITH_VERIFY"
71
+ mkdir -p "$PKG_WITH_VERIFY/delivery"
61
72
  node -e "
62
73
  const fs = require('fs');
63
74
  const pkg = {
@@ -68,6 +79,13 @@ const pkg = {
68
79
  }
69
80
  };
70
81
  fs.writeFileSync('$PKG_WITH_VERIFY/package.json', JSON.stringify(pkg, null, 2));
82
+ const declared = {
83
+ scope: 'ref:test-verify-passes',
84
+ reason: 'test fixture: real verify passes, no agent delivery bundle',
85
+ approved_by: 'test-harness',
86
+ declared_at: '2026-06-27T00:00:00Z'
87
+ };
88
+ fs.writeFileSync('$PKG_WITH_VERIFY/delivery/DECLARED', JSON.stringify(declared, null, 2));
71
89
  "
72
90
 
73
91
  # ─── Minimal package.json with failing verify ─────────────────────────────────
@@ -255,22 +273,28 @@ fi
255
273
  # package.json has passing trust-reconcile-verify → exits 0 (not false-blocked)
256
274
  # ═══════════════════════════════════════════════════════════════════════════════
257
275
  echo ""
258
- echo "=== TEST C: REAL-VERIFY-PASSES — passing verify + no bundle → exits 0 ==="
276
+ echo "=== TEST C: REAL-VERIFY-PASSES — passing verify + in-scope delivery/DECLARED marker (no bundle) → exits 0 with DECLARED exemption ==="
259
277
 
260
- outC=$(node "$RECONCILE" \
278
+ outC=$(TRUST_RECONCILE_REF="test-verify-passes" node "$RECONCILE" \
261
279
  --repo-root "$PKG_WITH_VERIFY" 2>&1)
262
280
  exitC=$?
263
281
 
264
282
  if [[ $exitC -eq 0 ]]; then
265
- _pass "REAL-VERIFY-PASSES: exits 0 when real verify passes and no bundle (got $exitC)"
283
+ _pass "REAL-VERIFY-PASSES: exits 0 when real verify passes and a well-formed, in-scope delivery/DECLARED marker exempts Step 2 (got $exitC)"
266
284
  else
267
285
  _fail "REAL-VERIFY-PASSES: expected exit 0, got $exitC — output: $outC"
268
286
  fi
269
287
 
270
- if echo "$outC" | grep -q "fresh verify passed"; then
271
- _pass "REAL-VERIFY-PASSES: 'fresh verify passed' message present"
288
+ if echo "$outC" | grep -qF "DECLARED (no-agent-delivery): ref:test-verify-passes — test fixture: real verify passes, no agent delivery bundle (approved by test-harness, declared 2026-06-27T00:00:00Z)"; then
289
+ _pass "REAL-VERIFY-PASSES: exact DECLARED (no-agent-delivery) exemption line present"
290
+ else
291
+ _fail "REAL-VERIFY-PASSES: expected the exact DECLARED (no-agent-delivery) line, got: $outC"
292
+ fi
293
+
294
+ if echo "$outC" | grep -q "running: npm run trust-reconcile-verify"; then
295
+ _pass "REAL-VERIFY-PASSES: Step 1 (fresh verify) still ran the real trust-reconcile-verify command (not skipped by the exemption)"
272
296
  else
273
- _fail "REAL-VERIFY-PASSES: expected 'fresh verify passed' in output, got: $outC"
297
+ _fail "REAL-VERIFY-PASSES: expected Step 1 to have run 'npm run trust-reconcile-verify', got: $outC"
274
298
  fi
275
299
 
276
300
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -6,8 +6,9 @@
6
6
  # Exit 1 with "trust divergence" message naming the command.
7
7
  # 2. MATCHING-PASSES: bundle claims a command passed; CI re-runs it and it PASSES.
8
8
  # Exit 0 (no divergence).
9
- # 3. NO-CHECKPOINT: no bundle present; canonical verify passes.
10
- # Exit 0 (fail-open on bundle absence, enforce fresh verify only).
9
+ # 3. NO-CHECKPOINT: no bundle present, no delivery/DECLARED marker; canonical
10
+ # verify passes. Exit 1 bundle required by default (ADR 0022 §1); the
11
+ # 'bundle-required-no-declared-marker' issue fires (no more fail-open on absence).
11
12
  # 4. LAUNDERING-CAUGHT: bundle claims "something || true" passed.
12
13
  # Exit 1 with laundering message (checked before "CI never ran" check).
13
14
  # YAML-VALID: .github/workflows/trust-reconcile.yml parses as valid YAML.
@@ -158,16 +159,16 @@ out3=$(TRUST_RECONCILE_COMMANDS="node -e 'process.exit(0)'" \
158
159
  --repo-root "$TMP" 2>&1)
159
160
  exit3=$?
160
161
 
161
- if [[ $exit3 -eq 0 ]]; then
162
- _pass "NO-CHECKPOINT: exits 0 (fresh verify passes, no bundle)"
162
+ if [[ $exit3 -ne 0 ]]; then
163
+ _pass "NO-CHECKPOINT: exits 1 (bundle required by default, no bundle, no marker) — got $exit3"
163
164
  else
164
- _fail "NO-CHECKPOINT: expected exit 0, got $exit3 — output: $out3"
165
+ _fail "NO-CHECKPOINT: expected exit 1 (bundle-required by default, ADR 0022 §1), got 0 — output: $out3"
165
166
  fi
166
167
 
167
- if echo "$out3" | grep -q "fail-open"; then
168
- _pass "NO-CHECKPOINT: output notes fail-open on bundle absence"
168
+ if echo "$out3" | grep -q "bundle-required-no-declared-marker"; then
169
+ _pass "NO-CHECKPOINT: output contains 'bundle-required-no-declared-marker'"
169
170
  else
170
- _fail "NO-CHECKPOINT: expected 'fail-open' in output, got: $out3"
171
+ _fail "NO-CHECKPOINT: expected 'bundle-required-no-declared-marker' in output, got: $out3"
171
172
  fi
172
173
 
173
174
  # Also verify: no-bundle + failing fresh verify still exits 1