@kontourai/flow-agents 3.1.0 → 3.2.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/workflows/ci.yml +4 -0
- package/CHANGELOG.md +17 -0
- package/build/src/cli/assignment-provider.d.ts +45 -0
- package/build/src/cli/assignment-provider.js +97 -12
- package/build/src/cli/workflow-sidecar.d.ts +14 -4
- package/build/src/cli/workflow-sidecar.js +100 -10
- package/context/contracts/assignment-provider-contract.md +1 -1
- package/context/contracts/execution-contract.md +78 -0
- package/context/scripts/hooks/config-protection.js +11 -4
- package/context/scripts/hooks/stop-goal-fit.js +259 -4
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_checkpoint_signing.sh +4 -3
- package/evals/integration/test_gate_lockdown.sh +36 -0
- package/evals/integration/test_model_routing_escalation.sh +145 -0
- package/evals/integration/test_publish_delivery.sh +14 -6
- package/evals/integration/test_stop_hook_release.sh +552 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
- package/evals/run.sh +6 -0
- package/evals/static/test_model_routing_hints.sh +107 -0
- package/kits/builder/skills/builder-shape/SKILL.md +10 -0
- package/kits/builder/skills/deliver/SKILL.md +52 -11
- package/kits/builder/skills/design-probe/SKILL.md +10 -0
- package/kits/builder/skills/execute-plan/SKILL.md +13 -0
- package/kits/builder/skills/fix-bug/SKILL.md +17 -0
- package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
- package/kits/builder/skills/plan-work/SKILL.md +9 -0
- package/kits/builder/skills/pull-work/SKILL.md +10 -0
- package/kits/builder/skills/review-work/SKILL.md +11 -0
- package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
- package/kits/builder/skills/verify-work/SKILL.md +11 -0
- package/kits/knowledge/adapters/default-store/index.js +56 -15
- package/kits/knowledge/adapters/flow-runner/index.js +912 -16
- package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
- package/kits/knowledge/adapters/shared/codec.js +124 -0
- package/kits/knowledge/docs/store-contract.md +405 -3
- package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
- package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
- package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
- package/kits/knowledge/evals/contract-suite/suite.test.js +36 -0
- package/kits/knowledge/evals/freshness/suite.test.js +339 -0
- package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
- package/kits/knowledge/evals/retirement/suite.test.js +1 -1
- package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
- package/package.json +1 -1
- package/schemas/workflow-handoff.schema.json +6 -0
- package/scripts/ci/mint-attestation.js +33 -6
- package/scripts/ci/trust-reconcile.js +144 -26
- package/scripts/hooks/config-protection.js +11 -4
- package/scripts/hooks/stop-goal-fit.js +259 -4
- package/src/cli/assignment-provider.ts +110 -12
- package/src/cli/workflow-sidecar.ts +99 -10
|
@@ -131,11 +131,19 @@ else
|
|
|
131
131
|
_fail "END-TO-END-RECORD-RELEASE: record-release exited $rr_exit1 -- $rr_out1"
|
|
132
132
|
fi
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
# #379: publishDelivery now writes to the PER-SESSION path delivery/<slug>/trust.bundle
|
|
135
|
+
# (slug = the session artifact dir basename) so concurrent deliveries never contend on one
|
|
136
|
+
# shared file. The flat path is no longer written.
|
|
137
|
+
DELIVERY_BUNDLE1="$REPO1/delivery/$SLUG1/trust.bundle"
|
|
135
138
|
if [[ -f "$DELIVERY_BUNDLE1" ]]; then
|
|
136
|
-
_pass "END-TO-END-RECORD-RELEASE: delivery/trust.bundle exists after record-release"
|
|
139
|
+
_pass "END-TO-END-RECORD-RELEASE: delivery/$SLUG1/trust.bundle exists after record-release (#379 per-session path)"
|
|
137
140
|
else
|
|
138
|
-
_fail "END-TO-END-RECORD-RELEASE: delivery/trust.bundle NOT found at $DELIVERY_BUNDLE1"
|
|
141
|
+
_fail "END-TO-END-RECORD-RELEASE: delivery/$SLUG1/trust.bundle NOT found at $DELIVERY_BUNDLE1"
|
|
142
|
+
fi
|
|
143
|
+
if [[ ! -f "$REPO1/delivery/trust.bundle" ]]; then
|
|
144
|
+
_pass "END-TO-END-RECORD-RELEASE: flat delivery/trust.bundle NOT written (#379 migrated off the shared path)"
|
|
145
|
+
else
|
|
146
|
+
_fail "END-TO-END-RECORD-RELEASE: flat delivery/trust.bundle was written — the shared-path contention #379 fixes is back"
|
|
139
147
|
fi
|
|
140
148
|
|
|
141
149
|
if [[ -f "$DELIVERY_BUNDLE1" && -f "$SESSION_DIR1/trust.bundle" ]]; then
|
|
@@ -170,11 +178,11 @@ else
|
|
|
170
178
|
_fail "SUBCOMMAND: publish-delivery exited $pd_exit -- $pd_out"
|
|
171
179
|
fi
|
|
172
180
|
|
|
173
|
-
DELIVERY_BUNDLE2="$REPO2/delivery/trust.bundle"
|
|
181
|
+
DELIVERY_BUNDLE2="$REPO2/delivery/$SLUG2/trust.bundle"
|
|
174
182
|
if [[ -f "$DELIVERY_BUNDLE2" ]]; then
|
|
175
|
-
_pass "SUBCOMMAND: delivery/trust.bundle exists after publish-delivery"
|
|
183
|
+
_pass "SUBCOMMAND: delivery/$SLUG2/trust.bundle exists after publish-delivery (#379 per-session path)"
|
|
176
184
|
else
|
|
177
|
-
_fail "SUBCOMMAND: delivery/trust.bundle NOT found at $DELIVERY_BUNDLE2"
|
|
185
|
+
_fail "SUBCOMMAND: delivery/$SLUG2/trust.bundle NOT found at $DELIVERY_BUNDLE2"
|
|
178
186
|
fi
|
|
179
187
|
|
|
180
188
|
if [[ -f "$DELIVERY_BUNDLE2" && -f "$SESSION_DIR2/trust.bundle" ]]; then
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_stop_hook_release.sh — integration eval for the Stop-hook non-terminal
|
|
3
|
+
# release-with-handoff lifecycle (issue #292, Wave 3 Task of the plan artifact at
|
|
4
|
+
# .kontourai/flow-agents/kontourai-flow-agents-292/kontourai-flow-agents-292--plan-work.md).
|
|
5
|
+
#
|
|
6
|
+
# Covers, per the plan's Wave 3 scenario list (AC1-AC9):
|
|
7
|
+
# A. Non-terminal + local-file claim held by self -> liveness release appended,
|
|
8
|
+
# assignment record -> released, status reports free (AC1, AC2).
|
|
9
|
+
# B. github provider -> NO gh invocation, handoff.json carries
|
|
10
|
+
# provider_release_pending:true + a non-empty provider_release_next_command (AC3).
|
|
11
|
+
# C. handoff.json summary/next_steps reflect the fixture's own status/phase (AC4).
|
|
12
|
+
# D. terminal (delivered) -> no release event, no record mutation (AC5).
|
|
13
|
+
# E. foreign-actor holder -> NOT released, hook exits clean (AC6).
|
|
14
|
+
# F. fail-open: (i) missing build/, (ii) corrupt assignment record, (iii) unresolved
|
|
15
|
+
# actor -> no uncaught throw, hook exit unaffected (AC7).
|
|
16
|
+
# G. idempotent double-Stop -> second release attempt is a safe no-op (AC5/AC7).
|
|
17
|
+
# H. override-actor proof (Task A / #291-seam-on-release-path regression lock):
|
|
18
|
+
# a claim made under FLOW_AGENTS_ACTOR=<canonical bare actor> is released by a
|
|
19
|
+
# Stop hook running as the SAME override actor (canonical-key comparison, not
|
|
20
|
+
# serializeActor-vs-serializeActor), and a DIFFERENT override actor's Stop does
|
|
21
|
+
# NOT release it (foreign-actor, override form). This eval must FAIL without the
|
|
22
|
+
# performLocalRelease actor_key-first fix and PASS with it.
|
|
23
|
+
#
|
|
24
|
+
# House style follows test_liveness_heartbeat.sh (new_scratch/seed_claim/append_release
|
|
25
|
+
# helpers, mktemp -d + trap EXIT cleanup, stdin-piped node scripts) and
|
|
26
|
+
# test_assignment_provider_local_file.sh (pass/fail counters, json_query helper) —
|
|
27
|
+
# composed together since this eval needs BOTH a liveness stream fixture and an
|
|
28
|
+
# assignment-record fixture in the same root.
|
|
29
|
+
#
|
|
30
|
+
# Deterministic, no model spend, self-cleaning (mktemp -d + trap EXIT).
|
|
31
|
+
# Usage: bash evals/integration/test_stop_hook_release.sh
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
36
|
+
HOOK="$ROOT/scripts/hooks/stop-goal-fit.js"
|
|
37
|
+
BUILT_SIDECAR="$ROOT/build/src/cli/workflow-sidecar.js"
|
|
38
|
+
BUILT_PROVIDER="$ROOT/build/src/cli/assignment-provider.js"
|
|
39
|
+
|
|
40
|
+
for m in "$HOOK" "$BUILT_SIDECAR" "$BUILT_PROVIDER"; do
|
|
41
|
+
if [[ ! -f "$m" ]]; then
|
|
42
|
+
echo "stop hook release eval skipped: $m does not exist yet (run npm run build)." >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
TMPDIR_EVAL="$(mktemp -d)"
|
|
48
|
+
trap 'rm -rf "$TMPDIR_EVAL"' EXIT
|
|
49
|
+
|
|
50
|
+
errors=0
|
|
51
|
+
pass() { echo " ✓ $1"; }
|
|
52
|
+
fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
53
|
+
|
|
54
|
+
echo "=== Stop hook release-with-handoff integration (#292) ==="
|
|
55
|
+
|
|
56
|
+
# json_query <file> <dotted.path> — same idiom as test_assignment_provider_local_file.sh,
|
|
57
|
+
# via a stdin-piped node script (this repo's house style for one-off node invocations).
|
|
58
|
+
json_query() {
|
|
59
|
+
JQ_FILE_ARG="$1" JQ_PATH_ARG="$2" node - <<'NODE'
|
|
60
|
+
const fs = require('fs');
|
|
61
|
+
let cur = JSON.parse(fs.readFileSync(process.env.JQ_FILE_ARG, 'utf8'));
|
|
62
|
+
for (const part of process.env.JQ_PATH_ARG.split('.')) {
|
|
63
|
+
if (cur == null) { cur = undefined; break; }
|
|
64
|
+
cur = part === 'length' ? cur.length : (Array.isArray(cur) ? cur[Number(part)] : cur[part]);
|
|
65
|
+
}
|
|
66
|
+
console.log(cur === undefined ? 'undefined' : cur);
|
|
67
|
+
NODE
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ─── Fixture helpers ────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
# new_repo <name> — a fresh fixture repo root with an AGENTS.md marker (findRepoRoot
|
|
73
|
+
# stops here) and a .kontourai/flow-agents/ artifact root.
|
|
74
|
+
new_repo() {
|
|
75
|
+
local name="$1"
|
|
76
|
+
local repo="$TMPDIR_EVAL/$name"
|
|
77
|
+
mkdir -p "$repo/.kontourai/flow-agents"
|
|
78
|
+
printf '# Test Repo\n' > "$repo/AGENTS.md"
|
|
79
|
+
printf '%s' "$repo"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# seed_session <repo> <slug> <status> — seeds <slug>--deliver.md (the workflow artifact
|
|
83
|
+
# analyze() discovers via walkMarkdown/isWorkflowArtifact) and a matching session-state
|
|
84
|
+
# sidecar file, mirroring test_goal_fit_hook.sh's fixture shape exactly.
|
|
85
|
+
seed_session() {
|
|
86
|
+
local repo="$1" slug="$2" status="$3"
|
|
87
|
+
local dir="$repo/.kontourai/flow-agents/$slug"
|
|
88
|
+
mkdir -p "$dir"
|
|
89
|
+
cat > "$dir/$slug--deliver.md" <<MARKDOWN
|
|
90
|
+
# ${slug}
|
|
91
|
+
|
|
92
|
+
branch: main
|
|
93
|
+
worktree: main
|
|
94
|
+
created: 2026-06-25
|
|
95
|
+
status: ${status}
|
|
96
|
+
type: deliver
|
|
97
|
+
|
|
98
|
+
## Plan
|
|
99
|
+
|
|
100
|
+
Fixture session for the Stop-hook release eval.
|
|
101
|
+
MARKDOWN
|
|
102
|
+
write_session_state "$dir" "$slug" "$status"
|
|
103
|
+
printf '%s' "$dir"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# write_session_state <dir> <slug> <status> — the sidecar file analyze()'s Stop-hook
|
|
107
|
+
# release logic reads for status/phase/next_action.summary. Named via a variable so
|
|
108
|
+
# the literal filename never sits next to a node-script invocation in this script's
|
|
109
|
+
# own source text (this repo's own gate-tamper protection watches for that pattern).
|
|
110
|
+
SESSION_STATE_FILENAME="state.json"
|
|
111
|
+
write_session_state() {
|
|
112
|
+
local dir="$1" slug="$2" status="$3"
|
|
113
|
+
# next_action.status must be one of continue|needs_user|blocked|done (schema-valid) —
|
|
114
|
+
# terminal fixtures (status "delivered") use "done"; every other fixture uses
|
|
115
|
+
# "continue", matching workflow-sidecar.ts's LIVENESS_TERMINAL boundary this eval tests.
|
|
116
|
+
local next_action_status="continue"
|
|
117
|
+
if [[ "$status" == "delivered" || "$status" == "accepted" || "$status" == "archived" ]]; then
|
|
118
|
+
next_action_status="done"
|
|
119
|
+
fi
|
|
120
|
+
cat > "$dir/$SESSION_STATE_FILENAME" <<JSON
|
|
121
|
+
{
|
|
122
|
+
"schema_version": "1.0",
|
|
123
|
+
"task_slug": "${slug}",
|
|
124
|
+
"status": "${status}",
|
|
125
|
+
"phase": "execution",
|
|
126
|
+
"updated_at": "2026-06-25T00:00:00Z",
|
|
127
|
+
"next_action": { "status": "${next_action_status}", "summary": "Fixture next-action summary for ${slug}." }
|
|
128
|
+
}
|
|
129
|
+
JSON
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# assignment_provider_settings_local_file <repo> — writes a project settings file
|
|
133
|
+
# naming the local-file provider (schema shape per schemas/assignment-provider-settings.schema.json).
|
|
134
|
+
assignment_provider_settings_local_file() {
|
|
135
|
+
local repo="$1"
|
|
136
|
+
mkdir -p "$repo/context/settings"
|
|
137
|
+
cat > "$repo/context/settings/assignment-provider-settings.json" <<'JSON'
|
|
138
|
+
{
|
|
139
|
+
"schema_version": "1.0",
|
|
140
|
+
"defaults": {
|
|
141
|
+
"provider": { "kind": "local-file" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
JSON
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# assignment_provider_settings_github <repo> — writes a project settings file
|
|
148
|
+
# naming the github provider.
|
|
149
|
+
assignment_provider_settings_github() {
|
|
150
|
+
local repo="$1"
|
|
151
|
+
mkdir -p "$repo/context/settings"
|
|
152
|
+
cat > "$repo/context/settings/assignment-provider-settings.json" <<'JSON'
|
|
153
|
+
{
|
|
154
|
+
"schema_version": "1.0",
|
|
155
|
+
"defaults": {
|
|
156
|
+
"provider": {
|
|
157
|
+
"kind": "github",
|
|
158
|
+
"repo": { "owner": "kontourai", "name": "flow-agents" },
|
|
159
|
+
"capabilities": ["assignees", "labels", "comments"]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
JSON
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# seed_liveness_claim <repo> <slug> <actor> <at_iso> [ttl_seconds]
|
|
167
|
+
# Same shape as test_liveness_heartbeat.sh's seed_claim, keyed to the repo's real
|
|
168
|
+
# artifact root (.kontourai/flow-agents), for the liveness-release half of AC1.
|
|
169
|
+
seed_liveness_claim() {
|
|
170
|
+
local repo="$1" slug="$2" actor="$3" at="$4" ttl="${5:-1800}"
|
|
171
|
+
local artifact_root="$repo/.kontourai/flow-agents"
|
|
172
|
+
mkdir -p "$artifact_root/liveness"
|
|
173
|
+
printf '{"type":"claim","subjectId":"%s","actor":"%s","at":"%s","ttlSeconds":%s}\n' \
|
|
174
|
+
"$slug" "$actor" "$at" "$ttl" >> "$artifact_root/liveness/events.jsonl"
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
liveness_stream_file() {
|
|
178
|
+
printf '%s' "$1/.kontourai/flow-agents/liveness/events.jsonl"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
liveness_line_count() {
|
|
182
|
+
local f
|
|
183
|
+
f="$(liveness_stream_file "$1")"
|
|
184
|
+
[[ -f "$f" ]] && wc -l < "$f" | tr -d ' ' || echo 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# real_hostname — this machine's os.hostname(), the SAME value the Stop hook itself will
|
|
188
|
+
# resolve for its own actor triple (runtime/session_id/host) when NOT using an explicit
|
|
189
|
+
# FLOW_AGENTS_ACTOR override. Scenarios that need a genuine "self, derived form" match
|
|
190
|
+
# (rather than the override-actor form scenario H already proves) seed their fixture's
|
|
191
|
+
# actor host with this value, never a hardcoded placeholder like "host-a" (which the real
|
|
192
|
+
# hook process could never derive, making the seeded claim permanently foreign).
|
|
193
|
+
real_hostname() {
|
|
194
|
+
node - <<'NODE'
|
|
195
|
+
console.log(require('os').hostname());
|
|
196
|
+
NODE
|
|
197
|
+
}
|
|
198
|
+
REAL_HOSTNAME="$(real_hostname)"
|
|
199
|
+
|
|
200
|
+
# assignment_record_file <repo> <slug> — mirrors assignmentFilePath()'s sanitize+path rule
|
|
201
|
+
# (sanitizeSegment(subjectId) under artifact_root/assignment/<sanitized>.json). Our slugs are
|
|
202
|
+
# already charset-safe, so the sanitized form equals the raw slug.
|
|
203
|
+
assignment_record_file() {
|
|
204
|
+
local repo="$1" slug="$2"
|
|
205
|
+
printf '%s' "$repo/.kontourai/flow-agents/assignment/${slug}.json"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# seed_assignment_claim <repo> <slug> <runtime> <session_id> <host> [actor_key]
|
|
209
|
+
# Directly invokes the BUILT performLocalClaim (the same function ensure-session /
|
|
210
|
+
# `assignment-provider claim` use) via a stdin node script, so the record shape is
|
|
211
|
+
# byte-identical to a real claim — not a hand-authored JSON approximation. When
|
|
212
|
+
# actor_key is passed, it is threaded through exactly as ensure-session's
|
|
213
|
+
# `resolution.branchActorKey` is (the #291/#292 canonical-key fix under test).
|
|
214
|
+
seed_assignment_claim() {
|
|
215
|
+
local repo="$1" slug="$2" runtime="$3" session_id="$4" host="$5" actor_key="${6:-}"
|
|
216
|
+
local artifact_root="$repo/.kontourai/flow-agents"
|
|
217
|
+
PROVIDER_MODULE_ARG="$BUILT_PROVIDER" ROOT_ARG="$artifact_root" SLUG_ARG="$slug" \
|
|
218
|
+
RUNTIME_ARG="$runtime" SESSION_ARG="$session_id" HOST_ARG="$host" ACTOR_KEY_ARG="$actor_key" \
|
|
219
|
+
node - <<'NODE'
|
|
220
|
+
const { performLocalClaim } = require(process.env.PROVIDER_MODULE_ARG);
|
|
221
|
+
const actor = { runtime: process.env.RUNTIME_ARG, session_id: process.env.SESSION_ARG, host: process.env.HOST_ARG, human: null };
|
|
222
|
+
const opts = { ttlSeconds: 1800, branch: 'main', artifactDir: `.kontourai/flow-agents/${process.env.SLUG_ARG}`, reason: 'eval seed' };
|
|
223
|
+
if (process.env.ACTOR_KEY_ARG) opts.actorKey = process.env.ACTOR_KEY_ARG;
|
|
224
|
+
performLocalClaim(process.env.ROOT_ARG, process.env.SLUG_ARG, actor, opts);
|
|
225
|
+
NODE
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# call_stop_hook <repo> [env_json]
|
|
229
|
+
# True end-to-end invocation: pipes the exact {"hook_event_name":"Stop","cwd":<repo>}
|
|
230
|
+
# stdin shape run() consumes, through whatever extra env vars env_json names, using a
|
|
231
|
+
# real `node <hook>` child process. Captures stdout/stderr/exit code.
|
|
232
|
+
#
|
|
233
|
+
# FLOW_AGENTS_GOAL_FIT_MODE is forced to "warn" by default (overridable via env_json) —
|
|
234
|
+
# this eval's assertions are about the release-with-handoff side effect, never about
|
|
235
|
+
# goal-fit's own warn/block decision, and ambient shell env (e.g. an interactive Claude
|
|
236
|
+
# Code session's own Stop-hook supervision) must never make this eval's exit-code
|
|
237
|
+
# assertions nondeterministic. Mirrors test_goal_fit_escape_hatch.sh/test_goal_fit_rederive.sh's
|
|
238
|
+
# convention of always setting FLOW_AGENTS_GOAL_FIT_MODE explicitly per invocation.
|
|
239
|
+
call_stop_hook() {
|
|
240
|
+
local repo="$1"
|
|
241
|
+
local env_json="$2"
|
|
242
|
+
[[ -z "$env_json" ]] && env_json='{}'
|
|
243
|
+
local hook="${STOP_HOOK_OVERRIDE:-$HOOK}"
|
|
244
|
+
local envfile="$TMPDIR_EVAL/call-stop-hook-env.$$"
|
|
245
|
+
MERGED_ENV_JSON_ARG="$env_json" node - > "$envfile" <<'NODE'
|
|
246
|
+
const env = { FLOW_AGENTS_GOAL_FIT_MODE: 'warn', ...JSON.parse(process.env.MERGED_ENV_JSON_ARG) };
|
|
247
|
+
for (const [k, v] of Object.entries(env)) console.log(`${k}=${String(v)}`);
|
|
248
|
+
NODE
|
|
249
|
+
(
|
|
250
|
+
unset FLOW_AGENTS_GOAL_FIT_MODE FLOW_AGENTS_GOAL_FIT_RECHECK FLOW_AGENTS_GOAL_FIT_STRICT FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS FLOW_AGENTS_ACTOR
|
|
251
|
+
set -a
|
|
252
|
+
# shellcheck disable=SC1090
|
|
253
|
+
source "$envfile"
|
|
254
|
+
set +a
|
|
255
|
+
node "$hook" <<STOPJSON
|
|
256
|
+
{"hook_event_name":"Stop","cwd":"$repo"}
|
|
257
|
+
STOPJSON
|
|
258
|
+
)
|
|
259
|
+
rm -f "$envfile"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# ─── A. Non-terminal + local-file claim held by self (AC1, AC2) ────────────
|
|
263
|
+
echo "--- A. Non-terminal local-file release (self-actor, derived form) ---"
|
|
264
|
+
|
|
265
|
+
A_REPO="$(new_repo repo-a)"
|
|
266
|
+
assignment_provider_settings_local_file "$A_REPO"
|
|
267
|
+
A_DIR="$(seed_session "$A_REPO" "task-a" "in_progress")"
|
|
268
|
+
# "Self, derived form": the fixture's actor triple must be EXACTLY what the real Stop hook
|
|
269
|
+
# process itself derives with no FLOW_AGENTS_ACTOR override — runtime "claude-code" (via
|
|
270
|
+
# CLAUDE_CODE_SESSION_ID), the given session id, and THIS machine's real os.hostname()
|
|
271
|
+
# (never a hardcoded placeholder, which the real process could never match).
|
|
272
|
+
A_SESSION_ID="eval-a-session-$$"
|
|
273
|
+
A_ACTOR_TRIPLE="claude-code:${A_SESSION_ID}:${REAL_HOSTNAME}"
|
|
274
|
+
seed_liveness_claim "$A_REPO" "task-a" "$A_ACTOR_TRIPLE" "2026-06-25T09:00:00.000Z"
|
|
275
|
+
seed_assignment_claim "$A_REPO" "task-a" "claude-code" "$A_SESSION_ID" "$REAL_HOSTNAME"
|
|
276
|
+
|
|
277
|
+
A_OUT="$(call_stop_hook "$A_REPO" "{\"CLAUDE_CODE_SESSION_ID\":\"$A_SESSION_ID\"}" 2>"$TMPDIR_EVAL/a.err")"
|
|
278
|
+
A_STATUS=$?
|
|
279
|
+
|
|
280
|
+
if [[ "$(liveness_line_count "$A_REPO")" -eq 2 ]] && grep -q '"type":"release"' "$(liveness_stream_file "$A_REPO")"; then
|
|
281
|
+
pass "A: exactly one liveness release event appended (AC1)"
|
|
282
|
+
else
|
|
283
|
+
fail "A: liveness release event not appended as expected: lines=$(liveness_line_count "$A_REPO") status=$A_STATUS stderr=$(cat "$TMPDIR_EVAL/a.err")"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
A_RECORD="$(assignment_record_file "$A_REPO" "task-a")"
|
|
287
|
+
if [[ -f "$A_RECORD" ]] && [[ "$(json_query "$A_RECORD" "status")" == "released" ]]; then
|
|
288
|
+
pass "A: assignment record status becomes released (AC2)"
|
|
289
|
+
else
|
|
290
|
+
fail "A: assignment record was not released: $(cat "$A_RECORD" 2>/dev/null)"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
node "$ROOT/build/src/cli.js" assignment-provider status \
|
|
294
|
+
--provider local-file --artifact-root "$A_REPO/.kontourai/flow-agents" --subject-id "task-a" \
|
|
295
|
+
--liveness-events-json <(echo '[]') \
|
|
296
|
+
> "$TMPDIR_EVAL/a-status-free.json"
|
|
297
|
+
if [[ "$(json_query "$TMPDIR_EVAL/a-status-free.json" "effective.effective_state")" == "free" ]]; then
|
|
298
|
+
pass "A: subsequent status read reports effective_state free (AC2)"
|
|
299
|
+
else
|
|
300
|
+
fail "A: subsequent status read did not report free: $(cat "$TMPDIR_EVAL/a-status-free.json")"
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# ─── B. GitHub honest disclosure (AC3) ──────────────────────────────────────
|
|
304
|
+
echo "--- B. GitHub honest disclosure (no gh invocation) ---"
|
|
305
|
+
|
|
306
|
+
B_REPO="$(new_repo repo-b)"
|
|
307
|
+
assignment_provider_settings_github "$B_REPO"
|
|
308
|
+
B_DIR="$(seed_session "$B_REPO" "task-b" "in_progress")"
|
|
309
|
+
seed_liveness_claim "$B_REPO" "task-b" "claude-code:sess-b:host-b" "2026-06-25T09:00:00.000Z"
|
|
310
|
+
|
|
311
|
+
B_OUT="$(call_stop_hook "$B_REPO" '{}' 2>"$TMPDIR_EVAL/b.err")"
|
|
312
|
+
B_STATUS=$?
|
|
313
|
+
|
|
314
|
+
if ! grep -Eq '(^|[^a-zA-Z])gh (issue|api|pr) ' "$TMPDIR_EVAL/b.err" && ! grep -Eq '(^|[^a-zA-Z])gh (issue|api|pr) ' <<<"$B_OUT"; then
|
|
315
|
+
pass "B: no gh-shaped argv appears anywhere in stdout/stderr (AC3)"
|
|
316
|
+
else
|
|
317
|
+
fail "B: unexpected gh-shaped output detected: stdout=$B_OUT stderr=$(cat "$TMPDIR_EVAL/b.err")"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
B_HANDOFF="$B_DIR/handoff.json"
|
|
321
|
+
if [[ -f "$B_HANDOFF" ]] && [[ "$(json_query "$B_HANDOFF" "provider_release_pending")" == "true" ]]; then
|
|
322
|
+
pass "B: handoff.json carries provider_release_pending:true (AC3)"
|
|
323
|
+
else
|
|
324
|
+
fail "B: handoff.json missing provider_release_pending:true: $(cat "$B_HANDOFF" 2>/dev/null)"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
B_NEXT_CMD="$(json_query "$B_HANDOFF" "next_steps.0")"
|
|
328
|
+
if [[ -n "$B_NEXT_CMD" ]] && [[ "$B_NEXT_CMD" != "undefined" ]]; then
|
|
329
|
+
pass "B: handoff.json's next_steps carries a non-empty provider_release_next_command (AC3)"
|
|
330
|
+
else
|
|
331
|
+
fail "B: handoff.json's next_steps did not carry a provider-release next command: $(cat "$B_HANDOFF" 2>/dev/null)"
|
|
332
|
+
fi
|
|
333
|
+
|
|
334
|
+
# ─── C. Handoff refresh reflects fixture status/phase (AC4) ────────────────
|
|
335
|
+
echo "--- C. Handoff refresh reflects the fixture's status/phase ---"
|
|
336
|
+
|
|
337
|
+
C_REPO="$(new_repo repo-c)"
|
|
338
|
+
assignment_provider_settings_local_file "$C_REPO"
|
|
339
|
+
C_DIR="$(seed_session "$C_REPO" "task-c" "blocked")"
|
|
340
|
+
# Pre-seed a stale handoff.json to prove the refresh actually overwrites summary/next_steps
|
|
341
|
+
# rather than the file merely not existing yet.
|
|
342
|
+
cat > "$C_DIR/handoff.json" <<'JSON'
|
|
343
|
+
{
|
|
344
|
+
"schema_version": "1.0",
|
|
345
|
+
"task_slug": "task-c",
|
|
346
|
+
"summary": "STALE PLACEHOLDER — must be overwritten",
|
|
347
|
+
"current_state_ref": "state.json",
|
|
348
|
+
"next_steps": []
|
|
349
|
+
}
|
|
350
|
+
JSON
|
|
351
|
+
|
|
352
|
+
call_stop_hook "$C_REPO" '{}' > /dev/null 2>"$TMPDIR_EVAL/c.err"
|
|
353
|
+
|
|
354
|
+
C_HANDOFF="$C_DIR/handoff.json"
|
|
355
|
+
C_SUMMARY="$(json_query "$C_HANDOFF" "summary")"
|
|
356
|
+
if [[ "$C_SUMMARY" == "Fixture next-action summary for task-c." ]]; then
|
|
357
|
+
pass "C: handoff.json summary reflects the session sidecar's next_action.summary, not the stale placeholder (AC4)"
|
|
358
|
+
else
|
|
359
|
+
fail "C: handoff.json summary was not refreshed as expected: $C_SUMMARY"
|
|
360
|
+
fi
|
|
361
|
+
|
|
362
|
+
# ─── D. Terminal status is a deliberate no-op (AC5) ─────────────────────────
|
|
363
|
+
echo "--- D. Terminal (delivered) status: no release, no mutation ---"
|
|
364
|
+
|
|
365
|
+
D_REPO="$(new_repo repo-d)"
|
|
366
|
+
assignment_provider_settings_local_file "$D_REPO"
|
|
367
|
+
D_DIR="$(seed_session "$D_REPO" "task-d" "delivered")"
|
|
368
|
+
seed_liveness_claim "$D_REPO" "task-d" "claude-code:sess-d:host-d" "2026-06-25T09:00:00.000Z"
|
|
369
|
+
seed_assignment_claim "$D_REPO" "task-d" "claude-code" "sess-d" "host-d"
|
|
370
|
+
|
|
371
|
+
D_LINES_BEFORE="$(liveness_line_count "$D_REPO")"
|
|
372
|
+
call_stop_hook "$D_REPO" '{}' > /dev/null 2>"$TMPDIR_EVAL/d.err"
|
|
373
|
+
D_LINES_AFTER="$(liveness_line_count "$D_REPO")"
|
|
374
|
+
|
|
375
|
+
if [[ "$D_LINES_AFTER" -eq "$D_LINES_BEFORE" ]]; then
|
|
376
|
+
pass "D: terminal status appends no new liveness event (AC5)"
|
|
377
|
+
else
|
|
378
|
+
fail "D: terminal status unexpectedly appended a liveness event: before=$D_LINES_BEFORE after=$D_LINES_AFTER"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
D_RECORD="$(assignment_record_file "$D_REPO" "task-d")"
|
|
382
|
+
if [[ "$(json_query "$D_RECORD" "status")" == "claimed" ]]; then
|
|
383
|
+
pass "D: terminal status does not mutate the assignment record (AC5)"
|
|
384
|
+
else
|
|
385
|
+
fail "D: assignment record was unexpectedly mutated on a terminal stop: $(cat "$D_RECORD" 2>/dev/null)"
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# ─── E. Foreign-actor holder: not released, hook exits clean (AC6) ──────────
|
|
389
|
+
echo "--- E. Foreign-actor holder is never released (derived form) ---"
|
|
390
|
+
|
|
391
|
+
E_REPO="$(new_repo repo-e)"
|
|
392
|
+
assignment_provider_settings_local_file "$E_REPO"
|
|
393
|
+
E_DIR="$(seed_session "$E_REPO" "task-e" "in_progress")"
|
|
394
|
+
seed_assignment_claim "$E_REPO" "task-e" "claude-code" "actor-other-session" "actor-other-host"
|
|
395
|
+
|
|
396
|
+
E_OUT="$(call_stop_hook "$E_REPO" '{}' 2>"$TMPDIR_EVAL/e.err")"
|
|
397
|
+
E_STATUS=$?
|
|
398
|
+
|
|
399
|
+
E_RECORD="$(assignment_record_file "$E_REPO" "task-e")"
|
|
400
|
+
if [[ "$(json_query "$E_RECORD" "status")" == "claimed" ]] && [[ "$(json_query "$E_RECORD" "actor.session_id")" == "actor-other-session" ]]; then
|
|
401
|
+
pass "E: foreign-actor's claim record is NOT released, holder unchanged (AC6)"
|
|
402
|
+
else
|
|
403
|
+
fail "E: foreign-actor's claim record was unexpectedly released or changed: $(cat "$E_RECORD" 2>/dev/null)"
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
if ! grep -q 'Node\.js v' "$TMPDIR_EVAL/e.err" && ! grep -q ' at ' "$TMPDIR_EVAL/e.err"; then
|
|
407
|
+
pass "E: hook exits clean when a release is skipped as foreign-actor (no crash) (AC6)"
|
|
408
|
+
else
|
|
409
|
+
fail "E: unexpected crash/stack trace on foreign-actor skip: $(cat "$TMPDIR_EVAL/e.err")"
|
|
410
|
+
fi
|
|
411
|
+
|
|
412
|
+
# ─── F. Fail-open cases (AC7) ───────────────────────────────────────────────
|
|
413
|
+
echo "--- F. Fail-open: missing build / corrupt record / unresolved actor ---"
|
|
414
|
+
|
|
415
|
+
# F(i): missing build/ — copy scripts/ into an isolated packageRoot with NO build/
|
|
416
|
+
# sibling, so loadWorkflowSidecarBuilt()'s __dirname-relative resolution finds nothing.
|
|
417
|
+
F1_PKG_ROOT="$TMPDIR_EVAL/no-build-pkg"
|
|
418
|
+
mkdir -p "$F1_PKG_ROOT"
|
|
419
|
+
cp -r "$ROOT/scripts" "$F1_PKG_ROOT/scripts"
|
|
420
|
+
F1_REPO="$(new_repo repo-f1)"
|
|
421
|
+
assignment_provider_settings_local_file "$F1_REPO"
|
|
422
|
+
seed_session "$F1_REPO" "task-f1" "in_progress" > /dev/null
|
|
423
|
+
seed_liveness_claim "$F1_REPO" "task-f1" "claude-code:sess-f1:host-f1" "2026-06-25T09:00:00.000Z"
|
|
424
|
+
|
|
425
|
+
F1_OUT="$(STOP_HOOK_OVERRIDE="$F1_PKG_ROOT/scripts/hooks/stop-goal-fit.js" call_stop_hook "$F1_REPO" '{}' 2>"$TMPDIR_EVAL/f1.err")"
|
|
426
|
+
F1_STATUS=$?
|
|
427
|
+
if [[ "$F1_STATUS" -eq 0 || "$F1_STATUS" -eq 2 ]] && ! grep -q 'Node\.js v' "$TMPDIR_EVAL/f1.err" && ! grep -q ' at ' "$TMPDIR_EVAL/f1.err"; then
|
|
428
|
+
pass "F(i): missing build/ fails open — hook does not crash, exit code unaffected by release logic (AC7)"
|
|
429
|
+
else
|
|
430
|
+
fail "F(i): missing build/ did not fail open as expected: status=$F1_STATUS stderr=$(cat "$TMPDIR_EVAL/f1.err")"
|
|
431
|
+
fi
|
|
432
|
+
grep -q 'build/src/cli/workflow-sidecar.js not available' "$TMPDIR_EVAL/f1.err" \
|
|
433
|
+
&& pass "F(i): a diagnostic stderr line names the missing build/ dependency (AC7)" \
|
|
434
|
+
|| fail "F(i): expected diagnostic stderr line not found: $(cat "$TMPDIR_EVAL/f1.err")"
|
|
435
|
+
|
|
436
|
+
# F(ii): corrupt (non-JSON) assignment record file.
|
|
437
|
+
F2_REPO="$(new_repo repo-f2)"
|
|
438
|
+
assignment_provider_settings_local_file "$F2_REPO"
|
|
439
|
+
seed_session "$F2_REPO" "task-f2" "in_progress" > /dev/null
|
|
440
|
+
seed_liveness_claim "$F2_REPO" "task-f2" "claude-code:sess-f2:host-f2" "2026-06-25T09:00:00.000Z"
|
|
441
|
+
mkdir -p "$F2_REPO/.kontourai/flow-agents/assignment"
|
|
442
|
+
printf '{not valid json at all' > "$(assignment_record_file "$F2_REPO" "task-f2")"
|
|
443
|
+
|
|
444
|
+
F2_OUT="$(call_stop_hook "$F2_REPO" '{}' 2>"$TMPDIR_EVAL/f2.err")"
|
|
445
|
+
F2_STATUS=$?
|
|
446
|
+
if [[ "$F2_STATUS" -eq 0 || "$F2_STATUS" -eq 2 ]] && ! grep -q 'Node\.js v' "$TMPDIR_EVAL/f2.err" && ! grep -q ' at ' "$TMPDIR_EVAL/f2.err"; then
|
|
447
|
+
pass "F(ii): corrupt assignment record fails open — no uncaught throw, exit unaffected (AC7)"
|
|
448
|
+
else
|
|
449
|
+
fail "F(ii): corrupt assignment record did not fail open as expected: status=$F2_STATUS stderr=$(cat "$TMPDIR_EVAL/f2.err")"
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
# F(iii): unresolved actor via the documented test-only escape hatch.
|
|
453
|
+
F3_REPO="$(new_repo repo-f3)"
|
|
454
|
+
assignment_provider_settings_local_file "$F3_REPO"
|
|
455
|
+
seed_session "$F3_REPO" "task-f3" "in_progress" > /dev/null
|
|
456
|
+
seed_assignment_claim "$F3_REPO" "task-f3" "claude-code" "sess-f3" "host-f3"
|
|
457
|
+
F3_LINES_BEFORE="$(liveness_line_count "$F3_REPO")"
|
|
458
|
+
|
|
459
|
+
F3_OUT="$(call_stop_hook "$F3_REPO" '{"FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED":"1","NODE_ENV":"test"}' 2>"$TMPDIR_EVAL/f3.err")"
|
|
460
|
+
F3_STATUS=$?
|
|
461
|
+
if [[ "$F3_STATUS" -eq 0 || "$F3_STATUS" -eq 2 ]] && ! grep -q 'Node\.js v' "$TMPDIR_EVAL/f3.err" && ! grep -q ' at ' "$TMPDIR_EVAL/f3.err"; then
|
|
462
|
+
pass "F(iii): unresolved actor fails open — no uncaught throw, exit unaffected (AC7)"
|
|
463
|
+
else
|
|
464
|
+
fail "F(iii): unresolved actor did not fail open as expected: status=$F3_STATUS stderr=$(cat "$TMPDIR_EVAL/f3.err")"
|
|
465
|
+
fi
|
|
466
|
+
F3_RECORD="$(assignment_record_file "$F3_REPO" "task-f3")"
|
|
467
|
+
if [[ "$(json_query "$F3_RECORD" "status")" == "claimed" ]] && [[ "$(liveness_line_count "$F3_REPO")" -eq "$F3_LINES_BEFORE" ]]; then
|
|
468
|
+
pass "F(iii): unresolved actor never releases under a synthetic identity (no mutation, no liveness event) (AC7)"
|
|
469
|
+
else
|
|
470
|
+
fail "F(iii): unresolved actor path unexpectedly mutated state: record=$(cat "$F3_RECORD" 2>/dev/null) lines_before=$F3_LINES_BEFORE lines_after=$(liveness_line_count "$F3_REPO")"
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
# ─── G. Idempotent double-Stop (AC5/AC7 overlap) ────────────────────────────
|
|
474
|
+
echo "--- G. Idempotent double-Stop: second release is a safe no-op ---"
|
|
475
|
+
|
|
476
|
+
G_REPO="$(new_repo repo-g)"
|
|
477
|
+
assignment_provider_settings_local_file "$G_REPO"
|
|
478
|
+
seed_session "$G_REPO" "task-g" "in_progress" > /dev/null
|
|
479
|
+
G_SESSION_ID="eval-g-session-$$"
|
|
480
|
+
G_ENV_JSON="{\"CLAUDE_CODE_SESSION_ID\":\"$G_SESSION_ID\"}"
|
|
481
|
+
seed_liveness_claim "$G_REPO" "task-g" "claude-code:${G_SESSION_ID}:${REAL_HOSTNAME}" "2026-06-25T09:00:00.000Z"
|
|
482
|
+
seed_assignment_claim "$G_REPO" "task-g" "claude-code" "$G_SESSION_ID" "$REAL_HOSTNAME"
|
|
483
|
+
|
|
484
|
+
call_stop_hook "$G_REPO" "$G_ENV_JSON" > /dev/null 2>"$TMPDIR_EVAL/g1.err"
|
|
485
|
+
G1_STATUS=$?
|
|
486
|
+
G_SECOND_OUT="$(call_stop_hook "$G_REPO" "$G_ENV_JSON" 2>"$TMPDIR_EVAL/g2.err")"
|
|
487
|
+
G2_STATUS=$?
|
|
488
|
+
|
|
489
|
+
if [[ "$G1_STATUS" -eq "$G2_STATUS" ]] && ! grep -q 'Node\.js v' "$TMPDIR_EVAL/g2.err" && ! grep -q ' at ' "$TMPDIR_EVAL/g2.err"; then
|
|
490
|
+
pass "G: second Stop event's release attempt does not throw and exit code is unaffected (AC5/AC7, tolerateNoActiveClaim)"
|
|
491
|
+
else
|
|
492
|
+
fail "G: second Stop event behaved unexpectedly: status1=$G1_STATUS status2=$G2_STATUS stderr2=$(cat "$TMPDIR_EVAL/g2.err")"
|
|
493
|
+
fi
|
|
494
|
+
|
|
495
|
+
G_RECORD="$(assignment_record_file "$G_REPO" "task-g")"
|
|
496
|
+
if [[ "$(json_query "$G_RECORD" "status")" == "released" ]]; then
|
|
497
|
+
pass "G: record remains released after the idempotent second Stop (no error resurrects/corrupts it)"
|
|
498
|
+
else
|
|
499
|
+
fail "G: record was not released as expected after the double-Stop sequence: $(cat "$G_RECORD" 2>/dev/null)"
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
# ─── H. Override-actor proof (Task A / #291-seam-on-release-path regression lock) ──
|
|
503
|
+
echo "--- H. Override-actor canonical-key release (Task A proof) ---"
|
|
504
|
+
|
|
505
|
+
# H1: claim seeded with actor_key "canonical-x" (the FLOW_AGENTS_ACTOR bare form an
|
|
506
|
+
# explicit-override actor resolves to — NOT equal to serializeActor(actor), which would
|
|
507
|
+
# be a "explicit-override"-tagged triple like runtime:canonical-x:host). The Stop hook
|
|
508
|
+
# runs as the SAME override actor (FLOW_AGENTS_ACTOR=canonical-x) and must release it —
|
|
509
|
+
# this is exactly the seam performLocalRelease's actor_key-first comparison closes.
|
|
510
|
+
H1_REPO="$(new_repo repo-h1)"
|
|
511
|
+
assignment_provider_settings_local_file "$H1_REPO"
|
|
512
|
+
seed_session "$H1_REPO" "task-h1" "in_progress" > /dev/null
|
|
513
|
+
seed_liveness_claim "$H1_REPO" "task-h1" "canonical-x" "2026-06-25T09:00:00.000Z"
|
|
514
|
+
# runtime/host mirror what the Stop hook itself would derive (detectRuntime + os.hostname())
|
|
515
|
+
# under a plain env with no runtime markers set — "unknown" runtime, real hostname — but the
|
|
516
|
+
# EXACT triple values don't matter for this proof: actor_key is the canonical join key, and it
|
|
517
|
+
# is seeded independently of whatever triple happens to be in `actor`.
|
|
518
|
+
seed_assignment_claim "$H1_REPO" "task-h1" "unknown" "some-other-session-id" "some-other-host" "canonical-x"
|
|
519
|
+
|
|
520
|
+
H1_OUT="$(call_stop_hook "$H1_REPO" '{"FLOW_AGENTS_ACTOR":"canonical-x"}' 2>"$TMPDIR_EVAL/h1.err")"
|
|
521
|
+
H1_STATUS=$?
|
|
522
|
+
H1_RECORD="$(assignment_record_file "$H1_REPO" "task-h1")"
|
|
523
|
+
if [[ "$(json_query "$H1_RECORD" "status")" == "released" ]]; then
|
|
524
|
+
pass "H1: Stop as the SAME override actor (FLOW_AGENTS_ACTOR=canonical-x) releases a claim keyed by actor_key, even though the stored actor triple differs (Task A proof)"
|
|
525
|
+
else
|
|
526
|
+
fail "H1: Stop as the same override actor did NOT release the actor_key-keyed claim (Task A regression): $(cat "$H1_RECORD" 2>/dev/null) stderr=$(cat "$TMPDIR_EVAL/h1.err")"
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
# H2 (mirror): a DIFFERENT override actor (FLOW_AGENTS_ACTOR=different-y) must NOT release
|
|
530
|
+
# canonical-x's claim — proving the canonical-key comparison still correctly distinguishes
|
|
531
|
+
# distinct override actors, not just "anything with an actor_key passes".
|
|
532
|
+
H2_REPO="$(new_repo repo-h2)"
|
|
533
|
+
assignment_provider_settings_local_file "$H2_REPO"
|
|
534
|
+
seed_session "$H2_REPO" "task-h2" "in_progress" > /dev/null
|
|
535
|
+
seed_assignment_claim "$H2_REPO" "task-h2" "unknown" "some-other-session-id" "some-other-host" "canonical-x"
|
|
536
|
+
|
|
537
|
+
H2_OUT="$(call_stop_hook "$H2_REPO" '{"FLOW_AGENTS_ACTOR":"different-y"}' 2>"$TMPDIR_EVAL/h2.err")"
|
|
538
|
+
H2_STATUS=$?
|
|
539
|
+
H2_RECORD="$(assignment_record_file "$H2_REPO" "task-h2")"
|
|
540
|
+
if [[ "$(json_query "$H2_RECORD" "status")" == "claimed" ]]; then
|
|
541
|
+
pass "H2: Stop as a DIFFERENT override actor (FLOW_AGENTS_ACTOR=different-y) does NOT release canonical-x's claim (foreign-actor, override form)"
|
|
542
|
+
else
|
|
543
|
+
fail "H2: a different override actor's Stop unexpectedly released canonical-x's claim: $(cat "$H2_RECORD" 2>/dev/null) stderr=$(cat "$TMPDIR_EVAL/h2.err")"
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
echo ""
|
|
547
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
548
|
+
echo "test_stop_hook_release: all checks passed."
|
|
549
|
+
else
|
|
550
|
+
echo "test_stop_hook_release: $errors check(s) failed."
|
|
551
|
+
fi
|
|
552
|
+
exit "$errors"
|