@kontourai/flow-agents 3.4.2 → 3.5.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/CHANGELOG.md +14 -0
- package/agents/dev.json +0 -5
- package/build/src/builder-flow-runtime.js +18 -12
- package/build/src/cli/assignment-provider.d.ts +3 -0
- package/build/src/cli/assignment-provider.js +59 -10
- package/build/src/cli/workflow-sidecar.js +186 -26
- package/build/src/lib/flow-resolver.js +7 -2
- package/build/src/tools/build-universal-bundles.js +0 -2
- package/context/contracts/assignment-provider-contract.md +5 -2
- package/context/scripts/hooks/workflow-steering.js +2 -40
- package/docs/spec/builder-flow-runtime.md +19 -7
- package/docs/spec/runtime-hook-surface.md +4 -4
- package/evals/integration/test_assignment_provider_local_file.sh +54 -0
- package/evals/integration/test_builder_entry_enforcement.sh +369 -22
- package/evals/integration/test_builder_step_producers.sh +1 -1
- package/evals/integration/test_bundle_install.sh +46 -3
- package/evals/integration/test_current_json_per_actor.sh +3 -2
- package/evals/integration/test_dual_emit_flow_step.sh +43 -9
- package/evals/integration/test_flowdef_session_activation.sh +15 -6
- package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
- package/evals/integration/test_install_merge.sh +24 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +14 -10
- package/evals/integration/test_resolvefirststep_security.sh +1 -1
- package/evals/integration/test_sidecar_field_preservation.sh +4 -2
- package/evals/integration/test_takeover_protocol.sh +1 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +18 -6
- package/evals/integration/test_workflow_steering_hook.sh +0 -72
- package/package.json +1 -1
- package/schemas/workflow-state.schema.json +1 -0
- package/scripts/hooks/workflow-steering.js +2 -40
- package/scripts/install-merge.js +2 -0
- package/src/builder-flow-runtime.ts +23 -12
- package/src/cli/assignment-provider.ts +55 -11
- package/src/cli/builder-flow-runtime.test.mjs +32 -0
- package/src/cli/workflow-sidecar.ts +183 -28
- package/src/lib/flow-resolver.ts +6 -2
- package/src/tools/build-universal-bundles.ts +0 -2
|
@@ -17,6 +17,142 @@ WRITER="workflow-sidecar"
|
|
|
17
17
|
|
|
18
18
|
echo "=== Builder workflow entry enforcement ==="
|
|
19
19
|
|
|
20
|
+
NONCANONICAL_ROOT="$TMP/noncanonical-root"
|
|
21
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
22
|
+
--artifact-root "$NONCANONICAL_ROOT" \
|
|
23
|
+
--task-slug noncanonical \
|
|
24
|
+
--title "Noncanonical root" \
|
|
25
|
+
--summary "Builder state must use the product artifact root." \
|
|
26
|
+
--flow-id builder.build >"$TMP/noncanonical.out" 2>&1; then
|
|
27
|
+
fail "noncanonical Builder artifact root should be rejected"
|
|
28
|
+
elif [[ ! -e "$NONCANONICAL_ROOT" ]] \
|
|
29
|
+
&& grep -q 'requires --artifact-root <project>/.kontourai/flow-agents' "$TMP/noncanonical.out"; then
|
|
30
|
+
pass "noncanonical Builder artifact root is rejected before any sidecar write"
|
|
31
|
+
else
|
|
32
|
+
fail "noncanonical Builder root left partial state or the wrong diagnostic: $(cat "$TMP/noncanonical.out")"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
SYMLINK_PROJECT="$TMP/symlink-project"
|
|
36
|
+
SYMLINK_EXTERNAL="$TMP/symlink-external"
|
|
37
|
+
mkdir -p "$SYMLINK_PROJECT/.kontourai" "$SYMLINK_EXTERNAL"
|
|
38
|
+
ln -s "$SYMLINK_EXTERNAL" "$SYMLINK_PROJECT/.kontourai/flow-agents"
|
|
39
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
40
|
+
--artifact-root "$SYMLINK_PROJECT/.kontourai/flow-agents" \
|
|
41
|
+
--task-slug symlink-root \
|
|
42
|
+
--title "Symlink root" \
|
|
43
|
+
--summary "Builder state must not escape the project." \
|
|
44
|
+
--flow-id builder.build >"$TMP/symlink-root.out" 2>&1; then
|
|
45
|
+
fail "symlinked Builder artifact root should be rejected"
|
|
46
|
+
elif [[ -z "$(find "$SYMLINK_EXTERNAL" -mindepth 1 -print -quit)" ]] \
|
|
47
|
+
&& grep -q 'requires a non-symlink Flow Agents artifact root' "$TMP/symlink-root.out"; then
|
|
48
|
+
pass "symlinked Builder artifact root is rejected before any external write"
|
|
49
|
+
else
|
|
50
|
+
fail "symlinked Builder root wrote externally or returned the wrong diagnostic: $(cat "$TMP/symlink-root.out")"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
SESSION_SYMLINK_PROJECT="$TMP/session-symlink-project"
|
|
54
|
+
SESSION_SYMLINK_ROOT="$SESSION_SYMLINK_PROJECT/.kontourai/flow-agents"
|
|
55
|
+
SESSION_SYMLINK_EXTERNAL="$TMP/session-symlink-external"
|
|
56
|
+
mkdir -p "$SESSION_SYMLINK_ROOT" "$SESSION_SYMLINK_EXTERNAL"
|
|
57
|
+
ln -s "$SESSION_SYMLINK_EXTERNAL" "$SESSION_SYMLINK_ROOT/session-symlink"
|
|
58
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
59
|
+
--artifact-root "$SESSION_SYMLINK_ROOT" \
|
|
60
|
+
--task-slug session-symlink \
|
|
61
|
+
--actor builder-entry-session-symlink \
|
|
62
|
+
--title "Session symlink" \
|
|
63
|
+
--summary "Session state must not escape the artifact root." \
|
|
64
|
+
--flow-id builder.build >"$TMP/session-symlink.out" 2>&1; then
|
|
65
|
+
fail "symlinked session directory should be rejected"
|
|
66
|
+
elif [[ -z "$(find "$SESSION_SYMLINK_EXTERNAL" -mindepth 1 -print -quit)" ]] \
|
|
67
|
+
&& grep -q 'session directory must be a real directory under the artifact root' "$TMP/session-symlink.out"; then
|
|
68
|
+
pass "symlinked session directory is rejected before external reads or writes"
|
|
69
|
+
else
|
|
70
|
+
fail "symlinked session directory escaped or returned the wrong diagnostic: $(cat "$TMP/session-symlink.out")"
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
NESTED_PROJECT="$TMP/nested-symlink-project"
|
|
74
|
+
NESTED_ROOT="$NESTED_PROJECT/.kontourai/flow-agents"
|
|
75
|
+
NESTED_EXTERNAL="$TMP/nested-symlink-external.json"
|
|
76
|
+
mkdir -p "$NESTED_ROOT/assignment"
|
|
77
|
+
printf 'external must survive\n' >"$NESTED_EXTERNAL"
|
|
78
|
+
ln -s "$NESTED_EXTERNAL" "$NESTED_ROOT/assignment/nested-symlink.json"
|
|
79
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
80
|
+
--artifact-root "$NESTED_ROOT" \
|
|
81
|
+
--task-slug nested-symlink \
|
|
82
|
+
--actor builder-entry-nested-symlink \
|
|
83
|
+
--title "Nested assignment symlink" \
|
|
84
|
+
--summary "Assignment evidence must stay inside the product root." \
|
|
85
|
+
--flow-id builder.build >"$TMP/nested-symlink.out" 2>&1; then
|
|
86
|
+
fail "symlinked assignment record should be rejected"
|
|
87
|
+
elif [[ "$(cat "$NESTED_EXTERNAL")" == "external must survive" ]] \
|
|
88
|
+
&& [[ ! -e "$NESTED_ROOT/nested-symlink" ]] \
|
|
89
|
+
&& grep -q 'assignment record must be a regular file, not a symlink' "$TMP/nested-symlink.out"; then
|
|
90
|
+
pass "nested assignment symlink is rejected before external write or trust evidence"
|
|
91
|
+
else
|
|
92
|
+
fail "nested assignment symlink escaped or returned the wrong diagnostic: $(cat "$TMP/nested-symlink.out")"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
RACE_PROJECT="$TMP/race-project"
|
|
96
|
+
RACE_ROOT="$RACE_PROJECT/.kontourai/flow-agents"
|
|
97
|
+
RACE_MOVED="$RACE_PROJECT/.kontourai/flow-agents-acquired"
|
|
98
|
+
RACE_EXTERNAL="$TMP/race-external"
|
|
99
|
+
mkdir -p "$RACE_ROOT" "$RACE_EXTERNAL"
|
|
100
|
+
FLOW_AGENTS_WORKFLOW_SIDECAR_LOCK_DELAY=1 flow_agents_node "$WRITER" ensure-session \
|
|
101
|
+
--artifact-root "$RACE_ROOT" \
|
|
102
|
+
--task-slug lock-swap \
|
|
103
|
+
--title "Lock swap" \
|
|
104
|
+
--summary "Lock cleanup must remain on the acquired root." \
|
|
105
|
+
--flow-id builder.build >"$TMP/lock-swap.out" 2>&1 &
|
|
106
|
+
RACE_PID=$!
|
|
107
|
+
node - "$RACE_ROOT/.workflow-sidecar.lockdir" <<'NODE'
|
|
108
|
+
const fs = require('node:fs');
|
|
109
|
+
const lock = process.argv[2];
|
|
110
|
+
const deadline = Date.now() + 5000;
|
|
111
|
+
(function wait() {
|
|
112
|
+
if (fs.existsSync(lock)) process.exit(0);
|
|
113
|
+
if (Date.now() > deadline) process.exit(1);
|
|
114
|
+
setTimeout(wait, 10);
|
|
115
|
+
})();
|
|
116
|
+
NODE
|
|
117
|
+
mv "$RACE_ROOT" "$RACE_MOVED"
|
|
118
|
+
mkdir -p "$RACE_EXTERNAL/.workflow-sidecar.lockdir"
|
|
119
|
+
printf 'outside must survive\n' >"$RACE_EXTERNAL/.workflow-sidecar.lockdir/sentinel"
|
|
120
|
+
ln -s "$RACE_EXTERNAL" "$RACE_ROOT"
|
|
121
|
+
set +e
|
|
122
|
+
wait "$RACE_PID"
|
|
123
|
+
RACE_STATUS=$?
|
|
124
|
+
set -e
|
|
125
|
+
if [[ "$RACE_STATUS" -ne 0 ]] \
|
|
126
|
+
&& [[ -f "$RACE_EXTERNAL/.workflow-sidecar.lockdir/sentinel" ]] \
|
|
127
|
+
&& [[ -d "$RACE_MOVED/.workflow-sidecar.lockdir" ]] \
|
|
128
|
+
&& grep -q 'lock cleanup skipped because root or lock identity changed' "$TMP/lock-swap.out"; then
|
|
129
|
+
pass "lock cleanup refuses a swapped root and preserves external content"
|
|
130
|
+
else
|
|
131
|
+
fail "lock cleanup followed a swapped root or lost its identity diagnostic: $(cat "$TMP/lock-swap.out")"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
RELEASE_STATE="$TMP/release-3.4.2-state.json"
|
|
135
|
+
cat >"$RELEASE_STATE" <<'JSON'
|
|
136
|
+
{
|
|
137
|
+
"schema_version": "1.0",
|
|
138
|
+
"task_slug": "release-3-4-2",
|
|
139
|
+
"status": "new",
|
|
140
|
+
"phase": "pickup",
|
|
141
|
+
"updated_at": "2026-07-10T00:00:00Z",
|
|
142
|
+
"next_action": {
|
|
143
|
+
"status": "continue",
|
|
144
|
+
"summary": "Start the canonical Flow run.",
|
|
145
|
+
"command": "flow-agents builder-run start --session-dir .kontourai/flow-agents/release-3-4-2",
|
|
146
|
+
"enforcement": "before_tool_use"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
JSON
|
|
150
|
+
if flow_agents_node "validate-workflow-artifacts" --skip-markdown-validation "$RELEASE_STATE" >/dev/null 2>&1; then
|
|
151
|
+
pass "3.4.2 sidecars remain schema-valid while deprecated enforcement is ignored"
|
|
152
|
+
else
|
|
153
|
+
fail "3.4.2 sidecar compatibility regressed"
|
|
154
|
+
fi
|
|
155
|
+
|
|
20
156
|
REFUSED_ROOT="$TMP/refused/.kontourai/flow-agents"
|
|
21
157
|
if flow_agents_node "$WRITER" ensure-session \
|
|
22
158
|
--artifact-root "$REFUSED_ROOT" \
|
|
@@ -52,6 +188,7 @@ LOCAL_ROOT="$TMP/local/.kontourai/flow-agents"
|
|
|
52
188
|
if flow_agents_node "$WRITER" ensure-session \
|
|
53
189
|
--artifact-root "$LOCAL_ROOT" \
|
|
54
190
|
--task-slug local-request \
|
|
191
|
+
--actor builder-entry-local \
|
|
55
192
|
--title "Local request" \
|
|
56
193
|
--summary "Providerless work still needs an anchor." \
|
|
57
194
|
--flow-id builder.build \
|
|
@@ -63,18 +200,25 @@ const root = process.argv[2];
|
|
|
63
200
|
const current = JSON.parse(fs.readFileSync(path.join(root, 'current.json'), 'utf8'));
|
|
64
201
|
const state = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'state.json'), 'utf8'));
|
|
65
202
|
const workItem = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'work-item.json'), 'utf8'));
|
|
66
|
-
|
|
67
|
-
if (
|
|
203
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(path.dirname(root), 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
|
|
204
|
+
if (current.active_flow_id !== 'builder.build' || current.active_step_id !== 'design-probe') process.exit(1);
|
|
205
|
+
if (state.status !== 'in_progress' || state.phase !== 'pickup') process.exit(1);
|
|
68
206
|
if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['local:local-request'])) process.exit(1);
|
|
69
207
|
if (workItem.id !== 'local-request' || workItem.title !== 'Local request') process.exit(1);
|
|
70
208
|
if (workItem.source_provider?.kind !== 'local' || workItem.source_provider?.path !== 'work-item.json') process.exit(1);
|
|
71
|
-
if (
|
|
72
|
-
if (state.
|
|
73
|
-
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['
|
|
74
|
-
if (!state.next_action?.
|
|
209
|
+
if (flowState.current_step !== 'design-probe' || flowState.subject !== 'local:local-request') process.exit(1);
|
|
210
|
+
if (state.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
211
|
+
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
212
|
+
if (!state.next_action?.command?.includes('builder-run sync')) process.exit(1);
|
|
213
|
+
if ('enforcement' in state.next_action) process.exit(1);
|
|
214
|
+
const bundle = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'trust.bundle'), 'utf8'));
|
|
215
|
+
const selected = (bundle.claims || []).find((claim) => claim.claimType === 'builder.pull-work.selected');
|
|
216
|
+
if (selected?.status !== 'verified') process.exit(1);
|
|
217
|
+
if (selected?.metadata?.workflow_subject_ref !== 'local:local-request') process.exit(1);
|
|
218
|
+
if (!(selected?.metadata?.artifact_refs || []).some((ref) => ref.file === '.kontourai/flow-agents/assignment/local-request.json')) process.exit(1);
|
|
75
219
|
NODE
|
|
76
220
|
then
|
|
77
|
-
pass "
|
|
221
|
+
pass "durably acquired local Work Item satisfies pull-work through the trust bundle and Flow advances"
|
|
78
222
|
else
|
|
79
223
|
fail "local Work Item or first-step state is invalid"
|
|
80
224
|
fi
|
|
@@ -83,7 +227,16 @@ else
|
|
|
83
227
|
fi
|
|
84
228
|
|
|
85
229
|
LOCAL_SESSION="$LOCAL_ROOT/local-request"
|
|
86
|
-
|
|
230
|
+
FLOW_DIGEST_BEFORE="$(find "$TMP/local/.kontourai/flow/runs/local-request" -type f -print0 | sort -z | xargs -0 shasum -a 256)"
|
|
231
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
232
|
+
--artifact-root "$LOCAL_ROOT" \
|
|
233
|
+
--task-slug local-request \
|
|
234
|
+
--actor builder-entry-local \
|
|
235
|
+
--title "Local request" \
|
|
236
|
+
--summary "Providerless work still needs an anchor." \
|
|
237
|
+
--flow-id builder.build \
|
|
238
|
+
--timestamp "2026-07-10T00:00:00Z" >"$TMP/builder-ensure-again.out" 2>&1 \
|
|
239
|
+
&& [[ "$FLOW_DIGEST_BEFORE" == "$(find "$TMP/local/.kontourai/flow/runs/local-request" -type f -print0 | sort -z | xargs -0 shasum -a 256)" ]] \
|
|
87
240
|
&& node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
|
|
88
241
|
const fs = require('node:fs');
|
|
89
242
|
const path = require('node:path');
|
|
@@ -91,24 +244,69 @@ const project = process.argv[2];
|
|
|
91
244
|
const session = process.argv[3];
|
|
92
245
|
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
|
|
93
246
|
const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
|
|
94
|
-
if (flowState.current_step !== '
|
|
95
|
-
if (sidecar.flow_run?.current_step !== '
|
|
96
|
-
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['
|
|
247
|
+
if (flowState.current_step !== 'design-probe' || flowState.subject !== 'local:local-request') process.exit(1);
|
|
248
|
+
if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
249
|
+
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
97
250
|
if (!sidecar.next_action?.command?.includes('builder-run sync')) process.exit(1);
|
|
98
251
|
NODE
|
|
99
252
|
then
|
|
100
|
-
pass "
|
|
253
|
+
pass "repeated ensure-session loads the canonical Flow run without resetting its history"
|
|
101
254
|
else
|
|
102
|
-
fail "canonical Builder
|
|
255
|
+
fail "canonical Builder ensure was not idempotent: $(cat "$TMP/builder-ensure-again.out")"
|
|
103
256
|
fi
|
|
104
257
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
258
|
+
BROKEN_PROJECT="$TMP/broken-start"
|
|
259
|
+
BROKEN_ROOT="$BROKEN_PROJECT/.kontourai/flow-agents"
|
|
260
|
+
mkdir -p "$BROKEN_PROJECT/.kontourai"
|
|
261
|
+
printf 'not a run-store directory\n' > "$BROKEN_PROJECT/.kontourai/flow"
|
|
262
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
263
|
+
--artifact-root "$BROKEN_ROOT" \
|
|
264
|
+
--task-slug broken-start \
|
|
265
|
+
--actor builder-entry-broken \
|
|
266
|
+
--title "Broken start" \
|
|
267
|
+
--summary "Flow startup must fail visibly." \
|
|
268
|
+
--flow-id builder.build >"$TMP/broken-start.out" 2>&1; then
|
|
269
|
+
fail "invalid canonical Flow startup should fail ensure-session"
|
|
270
|
+
elif [[ -f "$BROKEN_ROOT/broken-start/state.json" ]] \
|
|
271
|
+
&& [[ ! -e "$BROKEN_PROJECT/.kontourai/flow/runs/broken-start" ]] \
|
|
272
|
+
&& grep -q 'canonical Builder Flow entry failed' "$TMP/broken-start.out" \
|
|
273
|
+
&& grep -q 'Re-run the same ensure-session command' "$TMP/broken-start.out" \
|
|
274
|
+
&& node - "$BROKEN_ROOT/broken-start/state.json" <<'NODE'
|
|
275
|
+
const fs = require('node:fs');
|
|
276
|
+
const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
277
|
+
if (state.flow_run) process.exit(1);
|
|
278
|
+
if (!state.next_action?.command?.includes('builder-run start')) process.exit(1);
|
|
279
|
+
NODE
|
|
280
|
+
then
|
|
281
|
+
pass "failed canonical startup is visible and leaves only retryable sidecar guidance"
|
|
282
|
+
else
|
|
283
|
+
fail "failed canonical startup forged state or lost recovery guidance: $(cat "$TMP/broken-start.out")"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
rm -f "$BROKEN_PROJECT/.kontourai/flow"
|
|
287
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
288
|
+
--artifact-root "$BROKEN_ROOT" \
|
|
289
|
+
--task-slug broken-start \
|
|
290
|
+
--actor builder-entry-broken \
|
|
291
|
+
--title "Broken start" \
|
|
292
|
+
--summary "Flow startup must recover from persisted acquisition provenance." \
|
|
293
|
+
--flow-id builder.build >"$TMP/broken-start-retry.out" 2>&1 \
|
|
294
|
+
&& node - "$BROKEN_PROJECT" <<'NODE'
|
|
295
|
+
const fs = require('node:fs');
|
|
296
|
+
const path = require('node:path');
|
|
297
|
+
const project = process.argv[2];
|
|
298
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'broken-start', 'state.json'), 'utf8'));
|
|
299
|
+
const bundle = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'broken-start', 'trust.bundle'), 'utf8'));
|
|
300
|
+
if (flowState.current_step !== 'design-probe') process.exit(1);
|
|
301
|
+
if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) process.exit(1);
|
|
302
|
+
NODE
|
|
303
|
+
then
|
|
304
|
+
pass "interrupted Flow startup retries from exact persisted acquisition provenance"
|
|
305
|
+
else
|
|
306
|
+
fail "interrupted acquisition could not recover: $(cat "$TMP/broken-start-retry.out")"
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
|
|
112
310
|
const fs = require('node:fs');
|
|
113
311
|
const path = require('node:path');
|
|
114
312
|
const project = process.argv[2];
|
|
@@ -120,9 +318,158 @@ if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
|
120
318
|
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
121
319
|
NODE
|
|
122
320
|
then
|
|
123
|
-
pass "
|
|
321
|
+
pass "automatic selected-work claim synchronizes Flow and projects the next skill"
|
|
322
|
+
else
|
|
323
|
+
fail "automatic selected-work claim did not advance the canonical run"
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
SKIPPED_ROOT="$TMP/skipped/.kontourai/flow-agents"
|
|
327
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
328
|
+
--artifact-root "$SKIPPED_ROOT" \
|
|
329
|
+
--task-slug skipped-ownership \
|
|
330
|
+
--actor builder-entry-skipped \
|
|
331
|
+
--skip-ownership-guard \
|
|
332
|
+
--title "Skipped ownership" \
|
|
333
|
+
--summary "Unproven ownership must remain at pull-work." \
|
|
334
|
+
--flow-id builder.build >"$TMP/skipped.out" 2>&1 \
|
|
335
|
+
&& node - "$TMP/skipped" <<'NODE'
|
|
336
|
+
const fs = require('node:fs');
|
|
337
|
+
const path = require('node:path');
|
|
338
|
+
const project = process.argv[2];
|
|
339
|
+
const session = path.join(project, '.kontourai', 'flow-agents', 'skipped-ownership');
|
|
340
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'skipped-ownership', 'state.json'), 'utf8'));
|
|
341
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
|
|
342
|
+
if (flowState.current_step !== 'pull-work' || sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
|
|
343
|
+
if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
|
|
344
|
+
if (fs.existsSync(path.join(session, 'trust.bundle'))) process.exit(1);
|
|
345
|
+
if (fs.existsSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', 'skipped-ownership.json'))) process.exit(1);
|
|
346
|
+
NODE
|
|
347
|
+
then
|
|
348
|
+
pass "skipped ownership remains at pull-work without selected-work evidence"
|
|
349
|
+
else
|
|
350
|
+
fail "unproven ownership advanced the canonical run: $(cat "$TMP/skipped.out")"
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
MISMATCH_ROOT="$TMP/mismatched-subject/.kontourai/flow-agents"
|
|
354
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
355
|
+
--artifact-root "$MISMATCH_ROOT" \
|
|
356
|
+
--task-slug arbitrary-session-name \
|
|
357
|
+
--work-item "kontourai/flow-agents#541" \
|
|
358
|
+
--actor builder-entry-mismatch \
|
|
359
|
+
--title "Mismatched assignment subject" \
|
|
360
|
+
--summary "An arbitrary slug is not exact Work Item evidence." \
|
|
361
|
+
--flow-id builder.build >"$TMP/mismatched-subject.out" 2>&1 \
|
|
362
|
+
&& node - "$TMP/mismatched-subject" <<'NODE'
|
|
363
|
+
const fs = require('node:fs');
|
|
364
|
+
const path = require('node:path');
|
|
365
|
+
const project = process.argv[2];
|
|
366
|
+
const session = path.join(project, '.kontourai', 'flow-agents', 'arbitrary-session-name');
|
|
367
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'arbitrary-session-name', 'state.json'), 'utf8'));
|
|
368
|
+
const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
|
|
369
|
+
if (flowState.subject !== 'kontourai/flow-agents#541' || flowState.current_step !== 'pull-work') process.exit(1);
|
|
370
|
+
if (sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
|
|
371
|
+
if (fs.existsSync(path.join(session, 'trust.bundle'))) process.exit(1);
|
|
372
|
+
if (!fs.existsSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', 'arbitrary-session-name.json'))) process.exit(1);
|
|
373
|
+
NODE
|
|
374
|
+
then
|
|
375
|
+
pass "assignment subject must canonically match the exact Work Item before pull-work can pass"
|
|
376
|
+
else
|
|
377
|
+
fail "mismatched assignment subject was treated as exact Work Item evidence: $(cat "$TMP/mismatched-subject.out")"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
COLLISION_ROOT="$TMP/collision/.kontourai/flow-agents"
|
|
381
|
+
if ! flow_agents_node "$WRITER" ensure-session \
|
|
382
|
+
--artifact-root "$COLLISION_ROOT" \
|
|
383
|
+
--work-item "owner/a.b#541" \
|
|
384
|
+
--actor builder-entry-collision \
|
|
385
|
+
--title "Collision seed" \
|
|
386
|
+
--summary "Bind the first exact Work Item." >/dev/null 2>&1; then
|
|
387
|
+
fail "collision fixture could not seed the first exact Work Item"
|
|
388
|
+
else
|
|
389
|
+
COLLISION_ASSIGNMENT="$COLLISION_ROOT/assignment/owner-a-b-541.json"
|
|
390
|
+
COLLISION_DIGEST="$(shasum -a 256 "$COLLISION_ASSIGNMENT")"
|
|
391
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
392
|
+
--artifact-root "$COLLISION_ROOT" \
|
|
393
|
+
--work-item "owner/a-b#541" \
|
|
394
|
+
--actor builder-entry-collision \
|
|
395
|
+
--title "Collision attempt" \
|
|
396
|
+
--summary "A colliding slug must not change the exact Work Item." \
|
|
397
|
+
--flow-id builder.build >"$TMP/collision.out" 2>&1; then
|
|
398
|
+
fail "colliding exact Work Item refs should be rejected"
|
|
399
|
+
elif grep -q 'already bound to Work Item "owner/a.b#541", not "owner/a-b#541"' "$TMP/collision.out" \
|
|
400
|
+
&& [[ "$COLLISION_DIGEST" == "$(shasum -a 256 "$COLLISION_ASSIGNMENT")" ]] \
|
|
401
|
+
&& [[ ! -e "$TMP/collision/.kontourai/flow/runs/owner-a-b-541" ]]; then
|
|
402
|
+
pass "colliding slugs cannot change an existing session's exact Work Item binding"
|
|
403
|
+
else
|
|
404
|
+
fail "slug collision mutated ownership or returned the wrong diagnostic: $(cat "$TMP/collision.out")"
|
|
405
|
+
fi
|
|
406
|
+
fi
|
|
407
|
+
|
|
408
|
+
ASSIGNMENT_ONLY_ROOT="$TMP/assignment-only-collision/.kontourai/flow-agents"
|
|
409
|
+
mkdir -p "$ASSIGNMENT_ONLY_ROOT/assignment"
|
|
410
|
+
cat >"$ASSIGNMENT_ONLY_ROOT/assignment/owner-a-b-542.json" <<'JSON'
|
|
411
|
+
{
|
|
412
|
+
"schema_version": "1.0",
|
|
413
|
+
"role": "AssignmentClaimRecord",
|
|
414
|
+
"subject_id": "owner-a-b-542",
|
|
415
|
+
"actor": { "runtime": "codex", "session_id": "interrupted", "host": "eval-host", "human": null },
|
|
416
|
+
"actor_key": "interrupted",
|
|
417
|
+
"work_item_ref": "owner/a.b#542",
|
|
418
|
+
"claimed_at": "2020-01-01T00:00:00Z",
|
|
419
|
+
"ttl_seconds": 1,
|
|
420
|
+
"branch": "agent/interrupted/owner-a-b-542",
|
|
421
|
+
"artifact_dir": "owner-a-b-542",
|
|
422
|
+
"status": "claimed",
|
|
423
|
+
"audit_trail": []
|
|
424
|
+
}
|
|
425
|
+
JSON
|
|
426
|
+
ASSIGNMENT_ONLY_FILE="$ASSIGNMENT_ONLY_ROOT/assignment/owner-a-b-542.json"
|
|
427
|
+
ASSIGNMENT_ONLY_DIGEST="$(shasum -a 256 "$ASSIGNMENT_ONLY_FILE")"
|
|
428
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
429
|
+
--artifact-root "$ASSIGNMENT_ONLY_ROOT" \
|
|
430
|
+
--work-item "owner/a-b#542" \
|
|
431
|
+
--actor builder-entry-assignment-only-collision \
|
|
432
|
+
--supersede-stale \
|
|
433
|
+
--now "2026-07-10T00:00:00Z" \
|
|
434
|
+
--title "Assignment-only collision" \
|
|
435
|
+
--summary "A crash before state creation must preserve exact assignment identity." \
|
|
436
|
+
--flow-id builder.build >"$TMP/assignment-only-collision.out" 2>&1; then
|
|
437
|
+
fail "assignment-only colliding Work Item should be rejected"
|
|
438
|
+
elif grep -q 'already bound to Work Item "owner/a.b#542", not "owner/a-b#542"' "$TMP/assignment-only-collision.out" \
|
|
439
|
+
&& [[ "$ASSIGNMENT_ONLY_DIGEST" == "$(shasum -a 256 "$ASSIGNMENT_ONLY_FILE")" ]] \
|
|
440
|
+
&& [[ ! -e "$ASSIGNMENT_ONLY_ROOT/owner-a-b-542" ]]; then
|
|
441
|
+
pass "assignment provenance blocks slug collisions before session state exists"
|
|
442
|
+
else
|
|
443
|
+
fail "assignment-only collision mutated provenance or returned the wrong diagnostic: $(cat "$TMP/assignment-only-collision.out")"
|
|
444
|
+
fi
|
|
445
|
+
|
|
446
|
+
PREEXISTING_ROOT="$TMP/preexisting/.kontourai/flow-agents"
|
|
447
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
448
|
+
--artifact-root "$PREEXISTING_ROOT" \
|
|
449
|
+
--task-slug preexisting-selection \
|
|
450
|
+
--actor builder-entry-preexisting \
|
|
451
|
+
--title "Preexisting selection" \
|
|
452
|
+
--summary "Seed an assignment before the canonical Builder run." >/dev/null 2>&1 \
|
|
453
|
+
&& flow_agents_node "$WRITER" ensure-session \
|
|
454
|
+
--artifact-root "$PREEXISTING_ROOT" \
|
|
455
|
+
--task-slug preexisting-selection \
|
|
456
|
+
--actor builder-entry-preexisting \
|
|
457
|
+
--title "Preexisting selection" \
|
|
458
|
+
--summary "An older self-held assignment is not new selection evidence." \
|
|
459
|
+
--flow-id builder.build >"$TMP/preexisting.out" 2>&1 \
|
|
460
|
+
&& node - "$TMP/preexisting" <<'NODE'
|
|
461
|
+
const fs = require('node:fs');
|
|
462
|
+
const path = require('node:path');
|
|
463
|
+
const project = process.argv[2];
|
|
464
|
+
const session = path.join(project, '.kontourai', 'flow-agents', 'preexisting-selection');
|
|
465
|
+
const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'preexisting-selection', 'state.json'), 'utf8'));
|
|
466
|
+
if (flowState.current_step !== 'pull-work') process.exit(1);
|
|
467
|
+
if (fs.existsSync(path.join(session, 'trust.bundle'))) process.exit(1);
|
|
468
|
+
NODE
|
|
469
|
+
then
|
|
470
|
+
pass "preexisting self-held assignments do not retroactively satisfy pull-work"
|
|
124
471
|
else
|
|
125
|
-
fail "
|
|
472
|
+
fail "preexisting assignment was treated as current selection evidence: $(cat "$TMP/preexisting.out")"
|
|
126
473
|
fi
|
|
127
474
|
|
|
128
475
|
if FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
|
|
@@ -139,7 +139,7 @@ test_produce_claim() {
|
|
|
139
139
|
|
|
140
140
|
local slug
|
|
141
141
|
slug="$(echo "prod-$step-$expectation" | tr '/' '-' | tr '.' '-')"
|
|
142
|
-
local aroot="$TMP/$slug"
|
|
142
|
+
local aroot="$TMP/$slug/.kontourai/flow-agents"
|
|
143
143
|
setup_session_for_produce "$aroot" "$slug" "$step"
|
|
144
144
|
|
|
145
145
|
if flow_agents_node "workflow-sidecar" record-gate-claim "$aroot/$slug" \
|
|
@@ -592,14 +592,14 @@ function hasWorkflowSteering(file, ...eventNames) {
|
|
|
592
592
|
}
|
|
593
593
|
for (const file of process.argv.slice(2)) {
|
|
594
594
|
if (!hasWorkflowSteering(file, "UserPromptSubmit", "userPromptSubmit")) throw new Error(`missing prompt-submit workflow steering: ${file}`);
|
|
595
|
-
if (
|
|
595
|
+
if (hasWorkflowSteering(file, "PreToolUse", "preToolUse")) throw new Error(`workflow bootstrap must not be model-mediated at pre-tool time: ${file}`);
|
|
596
596
|
}
|
|
597
597
|
console.log("ok");
|
|
598
598
|
NODE
|
|
599
599
|
then
|
|
600
|
-
_pass "installed bundles
|
|
600
|
+
_pass "installed bundles keep workflow steering advisory and session bootstrap product-owned"
|
|
601
601
|
else
|
|
602
|
-
_fail "installed bundles do not wire workflow steering consistently"
|
|
602
|
+
_fail "installed bundles do not wire prompt-submit workflow steering consistently"
|
|
603
603
|
fi
|
|
604
604
|
|
|
605
605
|
if [[ -f "$OPENCODE_DEST/.opencode/plugins/flow-agents.js" ]] && node - "$OPENCODE_DEST/.opencode/plugins/flow-agents.js" <<'NODE'
|
|
@@ -881,6 +881,49 @@ else
|
|
|
881
881
|
_fail "Codex full install lost Builder specialist role routing"
|
|
882
882
|
fi
|
|
883
883
|
|
|
884
|
+
echo ""
|
|
885
|
+
echo "--- Packed Package Builder Entry ---"
|
|
886
|
+
PACKAGE_CONSUMER="$TMPDIR_EVAL/package-consumer"
|
|
887
|
+
PACKAGE_PROJECT="$TMPDIR_EVAL/package-project"
|
|
888
|
+
PACKAGE_AMBIENT="$TMPDIR_EVAL/package-ambient"
|
|
889
|
+
mkdir -p "$PACKAGE_CONSUMER" "$PACKAGE_PROJECT/.kontourai/flow-agents" "$PACKAGE_AMBIENT/kits/builder/flows"
|
|
890
|
+
cat >"$PACKAGE_AMBIENT/kits/builder/flows/build.flow.json" <<'JSON'
|
|
891
|
+
{
|
|
892
|
+
"id": "builder.build",
|
|
893
|
+
"version": "poison-ambient-cwd",
|
|
894
|
+
"steps": [{ "id": "ambient-poison", "next": null }],
|
|
895
|
+
"gates": {}
|
|
896
|
+
}
|
|
897
|
+
JSON
|
|
898
|
+
PACKAGE_PACK_LOG="$TMPDIR_EVAL/package-pack.log"
|
|
899
|
+
if (cd "$ROOT_DIR" && npm pack --silent --pack-destination "$TMPDIR_EVAL" >"$PACKAGE_PACK_LOG") \
|
|
900
|
+
&& PACKAGE_TARBALL="$(find "$TMPDIR_EVAL" -maxdepth 1 -type f -name 'kontourai-flow-agents-*.tgz' -print -quit)" \
|
|
901
|
+
&& [[ -n "$PACKAGE_TARBALL" ]] \
|
|
902
|
+
&& npm install --silent --no-audit --no-fund --ignore-scripts --prefix "$PACKAGE_CONSUMER" "$PACKAGE_TARBALL" \
|
|
903
|
+
&& (cd "$PACKAGE_AMBIENT" && node "$PACKAGE_CONSUMER/node_modules/@kontourai/flow-agents/build/src/cli/workflow-sidecar.js" ensure-session \
|
|
904
|
+
--artifact-root "$PACKAGE_PROJECT/.kontourai/flow-agents" \
|
|
905
|
+
--task-slug packed-builder-entry \
|
|
906
|
+
--actor packed-package-consumer \
|
|
907
|
+
--title "Packed Builder entry" \
|
|
908
|
+
--summary "Installed package should project pickup-probe." \
|
|
909
|
+
--flow-id builder.build >/dev/null 2>&1) \
|
|
910
|
+
&& node - "$PACKAGE_PROJECT" <<'NODE'
|
|
911
|
+
const fs = require('node:fs');
|
|
912
|
+
const path = require('node:path');
|
|
913
|
+
const project = process.argv[2];
|
|
914
|
+
const state = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'packed-builder-entry', 'state.json'), 'utf8'));
|
|
915
|
+
const flow = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'packed-builder-entry', 'state.json'), 'utf8'));
|
|
916
|
+
const bundle = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'packed-builder-entry', 'trust.bundle'), 'utf8'));
|
|
917
|
+
if (flow.current_step !== 'design-probe' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
|
|
918
|
+
if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
|
|
919
|
+
if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) process.exit(1);
|
|
920
|
+
NODE
|
|
921
|
+
then
|
|
922
|
+
_pass "packed npm consumer ignores unrelated ambient Flow definitions and projects pickup-probe"
|
|
923
|
+
else
|
|
924
|
+
_fail "packed npm consumer did not execute canonical Builder entry"
|
|
925
|
+
fi
|
|
926
|
+
|
|
884
927
|
if [[ -d "$CODEX_FULL_DEST/.codex/skills/plan-work" && -d "$CODEX_FULL_DEST/.codex/skills/deliver" && -d "$CODEX_FULL_DEST/.codex/skills/agentic-engineering" ]]; then
|
|
885
928
|
_pass "Codex full install ships kit-skills and standalone skills together"
|
|
886
929
|
else
|
|
@@ -117,7 +117,7 @@ fi
|
|
|
117
117
|
# --- 2. actor A's own current resolution is unaffected by actor B's later call (AC7) ----------
|
|
118
118
|
echo "--- 2. actor A's current resolution survives actor B's later, unrelated ensure-session (AC7) ---"
|
|
119
119
|
|
|
120
|
-
AC7_ROOT="$TMPDIR_EVAL/ac7-
|
|
120
|
+
AC7_ROOT="$TMPDIR_EVAL/ac7-project/.kontourai/flow-agents"
|
|
121
121
|
|
|
122
122
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
123
123
|
--artifact-root "$AC7_ROOT" \
|
|
@@ -370,13 +370,14 @@ set -e
|
|
|
370
370
|
# --- 5. record-gate-claim resolves A's own per-actor flow/step, not B's legacy pointer (AC11) --
|
|
371
371
|
echo "--- 5. record-gate-claim resolves A's own per-actor flow/step, not B's legacy pointer (AC11) ---"
|
|
372
372
|
|
|
373
|
-
AC11_ROOT="$TMPDIR_EVAL/ac11-
|
|
373
|
+
AC11_ROOT="$TMPDIR_EVAL/ac11-project/.kontourai/flow-agents"
|
|
374
374
|
|
|
375
375
|
flow_agents_node "workflow-sidecar" ensure-session \
|
|
376
376
|
--artifact-root "$AC11_ROOT" \
|
|
377
377
|
--task-slug gate-actor-a \
|
|
378
378
|
--actor eval-actor-a-session \
|
|
379
379
|
--flow-id builder.build \
|
|
380
|
+
--skip-ownership-guard \
|
|
380
381
|
--source-request "Actor A is active on a FlowDefinition-driven session." \
|
|
381
382
|
--summary "Actor A, builder.build, pull-work step." \
|
|
382
383
|
--timestamp "2026-07-01T00:00:00Z" \
|
|
@@ -34,7 +34,7 @@ cleanup() { rm -rf "$TMP"; }
|
|
|
34
34
|
trap cleanup EXIT
|
|
35
35
|
|
|
36
36
|
WRITER="workflow-sidecar"
|
|
37
|
-
SESSION_ROOT="$TMP/.flow-agents"
|
|
37
|
+
SESSION_ROOT="$TMP/.kontourai/flow-agents"
|
|
38
38
|
|
|
39
39
|
echo "── P-a resolver unit checks ──"
|
|
40
40
|
|
|
@@ -258,6 +258,7 @@ if flow_agents_node "$WRITER" ensure-session \
|
|
|
258
258
|
--artifact-root "$SESSION_ROOT" \
|
|
259
259
|
--task-slug dual-emit-test \
|
|
260
260
|
--flow-id builder.build \
|
|
261
|
+
--skip-ownership-guard \
|
|
261
262
|
--title "Declared-Only Test" \
|
|
262
263
|
--summary "Test declared-only emit for ADR 0016 P-d." \
|
|
263
264
|
--criterion "Tests pass" \
|
|
@@ -269,11 +270,25 @@ fi
|
|
|
269
270
|
|
|
270
271
|
DUAL_DIR="$SESSION_ROOT/dual-emit-test"
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
advance_builder_to_verify() {
|
|
274
|
+
local session_dir="$1"
|
|
275
|
+
flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation selected-work \
|
|
276
|
+
--summary "Selected the fixture Work Item." --timestamp "2026-06-26T00:00:10Z" >/dev/null 2>&1 \
|
|
277
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation pickup-probe-readiness \
|
|
278
|
+
--summary "Pickup readiness is established." --timestamp "2026-06-26T00:00:20Z" >/dev/null 2>&1 \
|
|
279
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation probe-decisions-or-accepted-gaps \
|
|
280
|
+
--summary "Probe decisions are recorded." --timestamp "2026-06-26T00:00:30Z" >/dev/null 2>&1 \
|
|
281
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation implementation-plan \
|
|
282
|
+
--summary "Implementation plan is recorded." --timestamp "2026-06-26T00:00:40Z" >/dev/null 2>&1 \
|
|
283
|
+
&& flow_agents_node "$WRITER" record-gate-claim "$session_dir" --status pass --expectation implementation-scope \
|
|
284
|
+
--summary "Implementation scope is recorded." --timestamp "2026-06-26T00:00:50Z" >/dev/null 2>&1
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if advance_builder_to_verify "$DUAL_DIR"; then
|
|
288
|
+
_pass "canonical Builder Flow advances through required gates to verify"
|
|
289
|
+
else
|
|
290
|
+
_fail "canonical Builder Flow did not reach verify"
|
|
291
|
+
fi
|
|
277
292
|
|
|
278
293
|
# Verify current.json carries the flow keys
|
|
279
294
|
if node -e "
|
|
@@ -328,8 +343,27 @@ fi
|
|
|
328
343
|
echo ""
|
|
329
344
|
echo "── P-d declared-only: policy-kind check maps to builder.verify.policy-compliance ──"
|
|
330
345
|
|
|
331
|
-
#
|
|
332
|
-
|
|
346
|
+
# Use a separate canonical run because the failing tests evidence above legitimately
|
|
347
|
+
# routes its run back to execute before this independent producer assertion.
|
|
348
|
+
if flow_agents_node "$WRITER" ensure-session \
|
|
349
|
+
--artifact-root "$SESSION_ROOT" \
|
|
350
|
+
--task-slug dual-emit-policy \
|
|
351
|
+
--flow-id builder.build \
|
|
352
|
+
--skip-ownership-guard \
|
|
353
|
+
--title "Declared Policy Test" \
|
|
354
|
+
--summary "Test declared-only policy emit for ADR 0016 P-d." \
|
|
355
|
+
--criterion "Policy passes" \
|
|
356
|
+
--timestamp "2026-06-26T00:01:30Z" >/dev/null 2>&1 \
|
|
357
|
+
&& advance_builder_to_verify "$SESSION_ROOT/dual-emit-policy"; then
|
|
358
|
+
_pass "policy fixture canonical Builder Flow reaches verify"
|
|
359
|
+
else
|
|
360
|
+
_fail "policy fixture canonical Builder Flow did not reach verify"
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
POLICY_DIR="$SESSION_ROOT/dual-emit-policy"
|
|
364
|
+
POLICY_BUNDLE="$POLICY_DIR/trust.bundle"
|
|
365
|
+
|
|
366
|
+
if flow_agents_node "$WRITER" record-evidence "$POLICY_DIR" \
|
|
333
367
|
--verdict pass \
|
|
334
368
|
--check-json '{"id":"policy-check","kind":"policy","status":"pass","summary":"Policy compliance passed"}' \
|
|
335
369
|
--timestamp "2026-06-26T00:02:00Z" >"$TMP/policy-evidence.out" 2>"$TMP/policy-evidence.err"; then
|
|
@@ -340,7 +374,7 @@ fi
|
|
|
340
374
|
|
|
341
375
|
if node -e "
|
|
342
376
|
const fs = require('fs');
|
|
343
|
-
const bundle = JSON.parse(fs.readFileSync('${
|
|
377
|
+
const bundle = JSON.parse(fs.readFileSync('${POLICY_BUNDLE}', 'utf8'));
|
|
344
378
|
const claims = bundle.claims;
|
|
345
379
|
// Declared claim for policy kind should be builder.verify.policy-compliance
|
|
346
380
|
const policyDeclared = claims.find(c => c.claimType === 'builder.verify.policy-compliance');
|