@kontourai/flow-agents 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +12 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/CHANGELOG.md +23 -0
  5. package/CONTEXT.md +21 -0
  6. package/build/src/cli/assignment-provider.d.ts +1 -0
  7. package/build/src/cli/assignment-provider.js +748 -0
  8. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  9. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  10. package/build/src/cli/validate-workflow-artifacts.js +5 -1
  11. package/build/src/cli/workflow-sidecar.js +157 -110
  12. package/build/src/cli.js +6 -0
  13. package/build/src/tools/validate-source-tree.js +1 -0
  14. package/context/contracts/artifact-contract.md +2 -0
  15. package/context/contracts/assignment-provider-contract.md +239 -0
  16. package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
  17. package/context/contracts/decision-registry-contract.md +2 -0
  18. package/context/contracts/delivery-contract.md +2 -0
  19. package/context/contracts/execution-contract.md +25 -0
  20. package/context/contracts/governance-adapter-contract.md +2 -0
  21. package/context/contracts/knowledge-store-contract.md +197 -0
  22. package/context/contracts/planning-contract.md +2 -0
  23. package/context/contracts/review-contract.md +2 -0
  24. package/context/contracts/sandbox-policy.md +2 -0
  25. package/context/contracts/standing-directives.md +13 -0
  26. package/context/contracts/verification-contract.md +2 -0
  27. package/context/contracts/work-item-contract.md +2 -0
  28. package/context/settings/assignment-provider-settings.json +33 -0
  29. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +180 -0
  30. package/docs/context-map.md +1 -0
  31. package/docs/decisions/index.md +3 -0
  32. package/docs/decisions/knowledge-store-provider.md +51 -0
  33. package/docs/decisions/model-routing.md +63 -0
  34. package/docs/decisions/standing-directives.md +66 -0
  35. package/docs/fixture-ownership.md +1 -0
  36. package/docs/workflow-shared-contracts.md +2 -1
  37. package/docs/workflow-usage-guide.md +6 -0
  38. package/evals/ci/run-baseline.sh +6 -0
  39. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  40. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  41. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  42. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  43. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  44. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  45. package/evals/integration/test_assignment_provider_github.sh +318 -0
  46. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  47. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  48. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  49. package/evals/integration/test_publish_delivery.sh +21 -4
  50. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  51. package/evals/integration/test_pull_work_liveness_preflight.sh +14 -6
  52. package/evals/integration/test_reconcile_soundness.sh +33 -9
  53. package/evals/integration/test_trust_reconcile.sh +9 -8
  54. package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
  55. package/evals/integration/test_workflow_sidecar_writer.sh +79 -0
  56. package/evals/run.sh +10 -0
  57. package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
  58. package/evals/static/test_knowledge_providers.sh +23 -0
  59. package/kits/builder/skills/deliver/SKILL.md +19 -0
  60. package/kits/builder/skills/pull-work/SKILL.md +78 -10
  61. package/kits/knowledge/kit.json +35 -0
  62. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  63. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  64. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  65. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  66. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  67. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  68. package/kits/knowledge/providers/conformance/suite.test.js +125 -0
  69. package/kits/knowledge/providers/git-repo/index.js +236 -0
  70. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  71. package/kits/knowledge/providers/health/index.js +153 -0
  72. package/kits/knowledge/providers/index.js +24 -0
  73. package/kits/knowledge/providers/lib/model.js +91 -0
  74. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  75. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  76. package/kits/knowledge/providers/work-item/index.js +204 -0
  77. package/package.json +4 -2
  78. package/schemas/assignment-provider-settings.schema.json +125 -0
  79. package/schemas/knowledge/edge.schema.json +54 -0
  80. package/schemas/knowledge/health-report.schema.json +45 -0
  81. package/schemas/knowledge/node.schema.json +49 -0
  82. package/schemas/knowledge/proposal.schema.json +53 -0
  83. package/scripts/ci/trust-reconcile.js +521 -24
  84. package/src/cli/assignment-provider.ts +845 -0
  85. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  86. package/src/cli/validate-workflow-artifacts.ts +5 -1
  87. package/src/cli/workflow-sidecar.ts +147 -106
  88. package/src/cli.ts +6 -0
  89. package/src/tools/validate-source-tree.ts +1 -0
@@ -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
@@ -21,9 +21,9 @@ json_query() {
21
21
  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=Array.isArray(cur) ? cur[Number(part)] : cur[part]; console.log(cur);' "$1" "$2"
22
22
  }
23
23
 
24
- [[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.scanned")" == "14" ]] && pass "audit scans all fixture groups" || fail "audit scans all fixture groups"
24
+ [[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.scanned")" == "15" ]] && pass "audit scans all fixture groups" || fail "audit scans all fixture groups"
25
25
  [[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.retire_candidates")" == "0" ]] && pass "audit finds no unowned retire candidates" || fail "audit finds no unowned retire candidates"
26
- [[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.kept")" == "14" ]] && pass "audit keeps all owned fixture groups" || fail "audit keeps all owned fixture groups"
26
+ [[ "$(json_query "$TMPDIR_EVAL/audit.json" "totals.kept")" == "15" ]] && pass "audit keeps all owned fixture groups" || fail "audit keeps all owned fixture groups"
27
27
 
28
28
  node - "$TMPDIR_EVAL/audit.json" <<'NODE'
29
29
  const fs = require("node:fs");
@@ -4,8 +4,10 @@
4
4
  # Proves that:
5
5
  # 1. END-TO-END-RECORD-RELEASE: record-release auto-publishes trust.bundle.
6
6
  # 2. SUBCOMMAND: publish-delivery subcommand copies bundle to delivery/.
7
- # 3. RECONCILE-DIVERGENCE: delivery trust.bundle + CI fail -> exit 1.
8
- # 4. RECONCILE-MATCHING: delivery trust.bundle + CI pass -> exit 0.
7
+ # 3. RECONCILE-DIVERGENCE: delivery trust.bundle (+ matching-sha checkpoint sibling, ADR
8
+ # 0022 addendum part 2 bundle-ownership binding) + CI fail -> exit 1.
9
+ # 4. RECONCILE-MATCHING: delivery trust.bundle (+ matching-sha checkpoint sibling) + CI
10
+ # pass -> exit 0.
9
11
  # 5. FAIL-SOFT: no trust.bundle -> publishDelivery skips, record-release exits 0.
10
12
  #
11
13
  # Deterministic, no model spend, self-cleaning.
@@ -63,6 +65,19 @@ PY
63
65
  node "$helper" "$dest" "$label" "$passing"
64
66
  }
65
67
 
68
+ # write_checkpoint_to <delivery_dir> <sha>
69
+ # Writes a minimal, well-formed trust.checkpoint.json naming <sha> as commit_sha, so a
70
+ # bare fixture trust.bundle written directly into delivery/ (TEST 3/4 below -- these do
71
+ # NOT go through the real seal-checkpoint pipeline) still carries the commit-identity
72
+ # binding trust-reconcile.js's bundle-ownership staleness check now requires (ADR 0022
73
+ # addendum, part 2) for an auto-discovered bundle to be treated as owned by the change
74
+ # under test, not stale.
75
+ write_checkpoint_to() {
76
+ local delivery_dir="$1" sha="$2"
77
+ printf '{"schema_version":"1.0","slug":"publish-delivery-fixture","work_item":null,"status":"delivered","phase":"release","sealed_at":"2026-06-27T00:00:00Z","commit_sha":"%s","checkpoint":{"asOf":"2026-06-27T00:00:00.000Z","statusByClaimId":{}}}' \
78
+ "$sha" > "$delivery_dir/trust.checkpoint.json"
79
+ }
80
+
66
81
  # Session setup helper
67
82
  setup_session() {
68
83
  local aroot="$1" slug="$2" bundle_src="$3"
@@ -181,8 +196,9 @@ mkdir -p "$REPO3/delivery"
181
196
  # -> claimed cmd not in canonical set -> not-run divergence, AND canonical fails
182
197
  DELIVERY3="$REPO3/delivery/trust.bundle"
183
198
  write_bundle_to "$DELIVERY3" "node --version" "true"
199
+ write_checkpoint_to "$REPO3/delivery" "1111111111111111111111111111111111111111"
184
200
 
185
- recon3_out=$(TRUST_RECONCILE_COMMANDS="false" \
201
+ recon3_out=$(TRUST_RECONCILE_SHA="1111111111111111111111111111111111111111" TRUST_RECONCILE_COMMANDS="false" \
186
202
  node "$RECONCILE" --repo-root "$REPO3" 2>&1)
187
203
  recon3_exit=$?
188
204
 
@@ -208,8 +224,9 @@ mkdir -p "$REPO4/delivery"
208
224
  # Bundle claims "node --version" passed; canonical verify is ALSO "node --version" (passes)
209
225
  DELIVERY4="$REPO4/delivery/trust.bundle"
210
226
  write_bundle_to "$DELIVERY4" "node --version" "true"
227
+ write_checkpoint_to "$REPO4/delivery" "2222222222222222222222222222222222222222"
211
228
 
212
- recon4_out=$(TRUST_RECONCILE_COMMANDS="node --version" \
229
+ recon4_out=$(TRUST_RECONCILE_SHA="2222222222222222222222222222222222222222" TRUST_RECONCILE_COMMANDS="node --version" \
213
230
  node "$RECONCILE" --repo-root "$REPO4" 2>&1)
214
231
  recon4_exit=$?
215
232
 
@@ -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"
@@ -329,7 +329,7 @@ require_text "$PULL" 'liveness claim <subjectId>' "pull-work references liveness
329
329
  require_text "$PULL" 'reading each row.s raw .status. field \(never .label.\)' "pull-work instructs consuming raw status, never label (AC1, AC8)"
330
330
  require_text "$PULL" 'a .verified. row for an actor other than self.*.held.' "pull-work documents held classification (AC1, AC2, AC8)"
331
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)"
332
+ require_text "$PULL" 'effective_state: .reclaimable.' "pull-work documents reclaimable classification via the assignment-provider join (AC1, AC3, AC8, #290)"
333
333
  require_text "$PULL" 'requires an explicit recorded opt-in' "pull-work requires an explicit recorded opt-in for reclaimable selection (AC3, AC8)"
334
334
  require_text "$PULL" 'never a silent normal pick' "pull-work states reclaimable is never a silent normal pick (AC3, AC8)"
335
335
  require_text "$PULL" 'a .verified. row for the resolved self actor.*.mine.' "pull-work documents mine classification (AC1, AC5, AC8)"
@@ -348,10 +348,15 @@ require_text "$PICKUP_PROBE" 'flags the selected item .reclaimable., treat the r
348
348
  # ─── F2-F6 (fix-plan iteration 1) static assertions ─────────────────────────
349
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
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)"
351
+ # F2's original numbered precedence list was liveness-only (#166); #290 (Wave 3) replaced it with
352
+ # the full ADR 0021 §1 assignment liveness join, computed via `assignment-provider status`'s
353
+ # `effective_state`/`reason` fields. These assertions are updated to match the new five-row list
354
+ # (mine/held/reclaimable/human-held/free) rather than the old four-row liveness-only list.
355
+ require_text "$PULL" '^1\. .effective_state: .held.. with .reason: .self_is_holder..' "pull-work's precedence rule step 1 (mine, via self_is_holder) is numbered verbatim (F2, #290, AC8)"
356
+ require_text "$PULL" '^2\. else .effective_state: .held..' "pull-work's precedence rule step 2 (held, excluded) is numbered verbatim (F2, #290, AC8)"
357
+ require_text "$PULL" '^3\. else .effective_state: .reclaimable..' "pull-work's precedence rule step 3 (reclaimable) is numbered verbatim (F2, #290, AC8)"
358
+ require_text "$PULL" '^4\. else .effective_state: .human-held..' "pull-work's precedence rule step 4 (human-held) is numbered verbatim (F2, #290, AC11, AC8)"
359
+ require_text "$PULL" '^5\. else .effective_state: .free..' "pull-work's precedence rule step 5 (free) is numbered verbatim (F2, #290, AC8)"
355
360
  require_text "$PULL" 'double-hold' "pull-work names the own-actor + other-actor co-existing row combination a double-hold (F2, AC8)"
356
361
  require_text "$PULL" 'explicit conflict warning \(per ADR 0012 §4 detection' "pull-work cites ADR 0012 §4 detection for the double-hold conflict warning (F2, AC8)"
357
362
  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 +378,10 @@ require_text "$PULL" '### Post-Claim Conflict Re-check' "pull-work documents the
373
378
  require_text "$PULL" 'coexists with this session.s own just-emitted claim on the same subject' "pull-work's post-claim re-check detects a double-hold against the just-emitted claim (F6, AC8)"
374
379
  require_text "$PULL" 'surface the conflict prominently in the user-facing output and instruct the user to coordinate before proceeding' "pull-work instructs surfacing the post-claim conflict prominently and coordinating before proceeding, not silently continuing (F6, AC8)"
375
380
  require_text "$PULL" 'does not provide true mutual exclusion across the read-then-write race window itself' "pull-work's honesty note states the exclusion guarantee still does not provide true mutual exclusion across the read-then-write race window (F6, AC8; updated #320 wording — see also test_liveness_verdict.sh)"
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)"
381
+ require_text "$PULL" '.#290.\x27s provider assignment lease closes this gap for the .{0,2}local-file.{0,2} provider only' "pull-work scopes true mutual exclusion to the local-file provider only, via #290's lock (fix-plan iteration 1, F5, AC8)"
382
+ require_text "$PULL" 'two concurrent local-file .claim. calls on the same subject genuinely cannot both win' "pull-work states local-file claim/claim races genuinely cannot both win under #290's lock (fix-plan iteration 1, F5, AC8)"
383
+ require_text "$PULL" 'The .{0,2}GitHub.{0,2} provider \(assignee/label/claim-comment\) remains advisory/last-writer' "pull-work does NOT overclaim true mutual exclusion for the GitHub provider — advisory/last-writer, detect not prevent (fix-plan iteration 1, F5, AC8)"
384
+ require_text "$PULL" 'Do not read .provider assignment lease. as closing the GitHub race; only the .verify-hold. publish gate \(#293\)' "pull-work explicitly warns not to read #290 as closing the GitHub race — only #293's verify-hold gate would (fix-plan iteration 1, F5, AC8)"
377
385
 
378
386
 
379
387
  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