@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.
- package/.github/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +12 -0
- package/.github/workflows/trust-reconcile.yml +62 -4
- package/CHANGELOG.md +23 -0
- package/CONTEXT.md +21 -0
- package/build/src/cli/assignment-provider.d.ts +1 -0
- package/build/src/cli/assignment-provider.js +748 -0
- package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
- package/build/src/cli/effective-assignment-provider-settings.js +125 -0
- package/build/src/cli/validate-workflow-artifacts.js +5 -1
- package/build/src/cli/workflow-sidecar.js +157 -110
- package/build/src/cli.js +6 -0
- package/build/src/tools/validate-source-tree.js +1 -0
- package/context/contracts/artifact-contract.md +2 -0
- package/context/contracts/assignment-provider-contract.md +239 -0
- package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
- package/context/contracts/decision-registry-contract.md +2 -0
- package/context/contracts/delivery-contract.md +2 -0
- package/context/contracts/execution-contract.md +25 -0
- package/context/contracts/governance-adapter-contract.md +2 -0
- package/context/contracts/knowledge-store-contract.md +197 -0
- package/context/contracts/planning-contract.md +2 -0
- package/context/contracts/review-contract.md +2 -0
- package/context/contracts/sandbox-policy.md +2 -0
- package/context/contracts/standing-directives.md +13 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/contracts/work-item-contract.md +2 -0
- package/context/settings/assignment-provider-settings.json +33 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +180 -0
- package/docs/context-map.md +1 -0
- package/docs/decisions/index.md +3 -0
- package/docs/decisions/knowledge-store-provider.md +51 -0
- package/docs/decisions/model-routing.md +63 -0
- package/docs/decisions/standing-directives.md +66 -0
- package/docs/fixture-ownership.md +1 -0
- package/docs/workflow-shared-contracts.md +2 -1
- package/docs/workflow-usage-guide.md +6 -0
- package/evals/ci/run-baseline.sh +6 -0
- package/evals/fixtures/assignment-provider/actor-a.json +6 -0
- package/evals/fixtures/assignment-provider/actor-b.json +6 -0
- package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
- package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
- package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
- package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
- package/evals/integration/test_assignment_provider_github.sh +318 -0
- package/evals/integration/test_assignment_provider_local_file.sh +222 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_publish_delivery.sh +21 -4
- package/evals/integration/test_pull_work_assignment_join.sh +132 -0
- package/evals/integration/test_pull_work_liveness_preflight.sh +14 -6
- package/evals/integration/test_reconcile_soundness.sh +33 -9
- package/evals/integration/test_trust_reconcile.sh +9 -8
- package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +79 -0
- package/evals/run.sh +10 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
- package/evals/static/test_knowledge_providers.sh +23 -0
- package/kits/builder/skills/deliver/SKILL.md +19 -0
- package/kits/builder/skills/pull-work/SKILL.md +78 -10
- package/kits/knowledge/kit.json +35 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
- package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
- package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
- package/kits/knowledge/providers/conformance/suite.test.js +125 -0
- package/kits/knowledge/providers/git-repo/index.js +236 -0
- package/kits/knowledge/providers/health/health-pass.test.js +99 -0
- package/kits/knowledge/providers/health/index.js +153 -0
- package/kits/knowledge/providers/index.js +24 -0
- package/kits/knowledge/providers/lib/model.js +91 -0
- package/kits/knowledge/providers/lib/schema-validate.js +119 -0
- package/kits/knowledge/providers/markdown-vault/index.js +169 -0
- package/kits/knowledge/providers/work-item/index.js +204 -0
- package/package.json +4 -2
- package/schemas/assignment-provider-settings.schema.json +125 -0
- package/schemas/knowledge/edge.schema.json +54 -0
- package/schemas/knowledge/health-report.schema.json +45 -0
- package/schemas/knowledge/node.schema.json +49 -0
- package/schemas/knowledge/proposal.schema.json +53 -0
- package/scripts/ci/trust-reconcile.js +521 -24
- package/src/cli/assignment-provider.ts +845 -0
- package/src/cli/effective-assignment-provider-settings.ts +112 -0
- package/src/cli/validate-workflow-artifacts.ts +5 -1
- package/src/cli/workflow-sidecar.ts +147 -106
- package/src/cli.ts +6 -0
- 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")" == "
|
|
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")" == "
|
|
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 +
|
|
8
|
-
#
|
|
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" '
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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" '
|
|
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
|
|
13
|
-
#
|
|
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
|
|
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 -
|
|
271
|
-
_pass "REAL-VERIFY-PASSES:
|
|
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
|
|
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
|
|
10
|
-
# Exit
|
|
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 -
|
|
162
|
-
_pass "NO-CHECKPOINT: exits
|
|
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
|
|
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 "
|
|
168
|
-
_pass "NO-CHECKPOINT: output
|
|
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 '
|
|
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
|