@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/agents/dev.json +0 -5
  3. package/build/src/builder-flow-runtime.js +18 -12
  4. package/build/src/cli/assignment-provider.d.ts +3 -0
  5. package/build/src/cli/assignment-provider.js +59 -10
  6. package/build/src/cli/workflow-sidecar.js +186 -26
  7. package/build/src/lib/flow-resolver.js +7 -2
  8. package/build/src/tools/build-universal-bundles.js +0 -2
  9. package/context/contracts/assignment-provider-contract.md +5 -2
  10. package/context/scripts/hooks/workflow-steering.js +2 -40
  11. package/docs/spec/builder-flow-runtime.md +19 -7
  12. package/docs/spec/runtime-hook-surface.md +4 -4
  13. package/evals/integration/test_assignment_provider_local_file.sh +54 -0
  14. package/evals/integration/test_builder_entry_enforcement.sh +369 -22
  15. package/evals/integration/test_builder_step_producers.sh +1 -1
  16. package/evals/integration/test_bundle_install.sh +46 -3
  17. package/evals/integration/test_current_json_per_actor.sh +3 -2
  18. package/evals/integration/test_dual_emit_flow_step.sh +43 -9
  19. package/evals/integration/test_flowdef_session_activation.sh +15 -6
  20. package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
  21. package/evals/integration/test_install_merge.sh +24 -0
  22. package/evals/integration/test_phase_map_and_gate_claim.sh +14 -10
  23. package/evals/integration/test_resolvefirststep_security.sh +1 -1
  24. package/evals/integration/test_sidecar_field_preservation.sh +4 -2
  25. package/evals/integration/test_takeover_protocol.sh +1 -1
  26. package/evals/integration/test_workflow_sidecar_writer.sh +18 -6
  27. package/evals/integration/test_workflow_steering_hook.sh +0 -72
  28. package/package.json +1 -1
  29. package/schemas/workflow-state.schema.json +1 -0
  30. package/scripts/hooks/workflow-steering.js +2 -40
  31. package/scripts/install-merge.js +2 -0
  32. package/src/builder-flow-runtime.ts +23 -12
  33. package/src/cli/assignment-provider.ts +55 -11
  34. package/src/cli/builder-flow-runtime.test.mjs +32 -0
  35. package/src/cli/workflow-sidecar.ts +183 -28
  36. package/src/lib/flow-resolver.ts +6 -2
  37. 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
- if (current.active_flow_id !== 'builder.build' || current.active_step_id !== 'pull-work') process.exit(1);
67
- if (state.status !== 'new' || state.phase !== 'pickup') process.exit(1);
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 (state.next_action?.command !== 'flow-agents builder-run start --session-dir .kontourai/flow-agents/local-request') process.exit(1);
72
- if (state.next_action?.enforcement !== 'before_tool_use') process.exit(1);
73
- if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
74
- if (!state.next_action?.summary?.includes('`pull-work`')) process.exit(1);
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 "providerless request creates a local Work Item and starts at pull-work"
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
- if flow_agents_node builder-run start --session-dir "$LOCAL_SESSION" >"$TMP/builder-start.out" 2>&1 \
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 !== 'pull-work' || flowState.subject !== 'local:local-request') process.exit(1);
95
- if (sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
96
- if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
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 "small-model entry command creates canonical Flow run and projects pull-work action"
253
+ pass "repeated ensure-session loads the canonical Flow run without resetting its history"
101
254
  else
102
- fail "canonical Builder run did not start or project correctly: $(cat "$TMP/builder-start.out")"
255
+ fail "canonical Builder ensure was not idempotent: $(cat "$TMP/builder-ensure-again.out")"
103
256
  fi
104
257
 
105
- if flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
106
- --expectation selected-work \
107
- --status pass \
108
- --summary "Selected local:local-request with scope and acceptance context." \
109
- --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Provider-neutral local Work Item."}' \
110
- >"$TMP/selected-work.out" 2>&1 \
111
- && node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
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 "gate-claim write synchronizes Flow and projects the next skill"
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 "selected-work claim did not advance the canonical run: $(cat "$TMP/selected-work.out")"
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 (!hasWorkflowSteering(file, "PreToolUse", "preToolUse")) throw new Error(`missing pre-tool workflow entry enforcement: ${file}`);
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 wire prompt-submit steering and pre-tool entry enforcement across Claude Code, Codex, and Kiro"
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-artifact-root"
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-artifact-root"
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
- flow_agents_node "$WRITER" advance-state "$DUAL_DIR" \
273
- --status in_progress --phase verification \
274
- --summary "Testing declared-only verify claims." --next-action "Record evidence." \
275
- --flow-definition builder.build \
276
- --timestamp "2026-06-26T00:00:30Z" >/dev/null 2>&1
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
- # Record a policy check with the same flow context
332
- if flow_agents_node "$WRITER" record-evidence "$DUAL_DIR" \
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('${BUNDLE}', 'utf8'));
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');