@kontourai/flow-agents 3.4.3 → 3.6.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 (65) hide show
  1. package/.github/workflows/ci.yml +4 -0
  2. package/CHANGELOG.md +15 -0
  3. package/build/src/builder-flow-run-adapter.d.ts +10 -1
  4. package/build/src/builder-flow-run-adapter.js +29 -1
  5. package/build/src/builder-flow-runtime.d.ts +18 -0
  6. package/build/src/builder-flow-runtime.js +205 -13
  7. package/build/src/builder-lifecycle-authority.d.ts +35 -0
  8. package/build/src/builder-lifecycle-authority.js +219 -0
  9. package/build/src/cli/assignment-provider.d.ts +13 -0
  10. package/build/src/cli/assignment-provider.js +120 -62
  11. package/build/src/cli/builder-run.js +46 -5
  12. package/build/src/cli/workflow-artifact-cleanup-audit.js +3 -0
  13. package/build/src/cli/workflow-sidecar.d.ts +3 -0
  14. package/build/src/cli/workflow-sidecar.js +140 -30
  15. package/build/src/cli/workflow.d.ts +2 -0
  16. package/build/src/cli/workflow.js +521 -0
  17. package/build/src/cli.js +2 -0
  18. package/build/src/index.d.ts +4 -0
  19. package/build/src/index.js +2 -0
  20. package/build/src/lib/flow-resolver.js +7 -2
  21. package/build/src/lib/package-version.d.ts +2 -0
  22. package/build/src/lib/package-version.js +13 -0
  23. package/build/src/lib/pinned-cli-command.d.ts +6 -0
  24. package/build/src/lib/pinned-cli-command.js +21 -0
  25. package/context/contracts/artifact-contract.md +1 -1
  26. package/context/contracts/assignment-provider-contract.md +5 -2
  27. package/context/scripts/hooks/config-protection.js +8 -1
  28. package/context/scripts/hooks/lib/config-protection-remedies.js +3 -0
  29. package/context/scripts/hooks/stop-goal-fit.js +1 -1
  30. package/docs/context-map.md +2 -0
  31. package/docs/public-workflow-cli.md +63 -0
  32. package/docs/spec/builder-flow-runtime.md +49 -5
  33. package/docs/workflow-usage-guide.md +5 -0
  34. package/evals/ci/run-baseline.sh +2 -0
  35. package/evals/integration/test_assignment_provider_local_file.sh +54 -0
  36. package/evals/integration/test_builder_entry_enforcement.sh +241 -24
  37. package/evals/integration/test_bundle_install.sh +97 -0
  38. package/evals/integration/test_current_json_per_actor.sh +1 -0
  39. package/evals/integration/test_dual_emit_flow_step.sh +2 -0
  40. package/evals/integration/test_flowdef_session_activation.sh +6 -3
  41. package/evals/integration/test_goal_fit_escape_hatch.sh +3 -3
  42. package/evals/integration/test_phase_map_and_gate_claim.sh +4 -0
  43. package/evals/integration/test_public_workflow_cli.sh +259 -0
  44. package/evals/integration/test_workflow_sidecar_writer.sh +1 -0
  45. package/package.json +2 -2
  46. package/schemas/builder-lifecycle-authorization.schema.json +57 -0
  47. package/schemas/lifecycle-authority-keys.schema.json +25 -0
  48. package/schemas/workflow-state.schema.json +1 -1
  49. package/scripts/hooks/config-protection.js +8 -1
  50. package/scripts/hooks/lib/config-protection-remedies.js +3 -0
  51. package/scripts/hooks/stop-goal-fit.js +1 -1
  52. package/src/builder-flow-run-adapter.ts +47 -0
  53. package/src/builder-flow-runtime.ts +216 -4
  54. package/src/builder-lifecycle-authority.ts +218 -0
  55. package/src/cli/assignment-provider.ts +84 -20
  56. package/src/cli/builder-flow-runtime.test.mjs +404 -1
  57. package/src/cli/builder-run.ts +56 -5
  58. package/src/cli/workflow-artifact-cleanup-audit.ts +3 -0
  59. package/src/cli/workflow-sidecar.ts +138 -31
  60. package/src/cli/workflow.ts +471 -0
  61. package/src/cli.ts +2 -0
  62. package/src/index.ts +14 -0
  63. package/src/lib/flow-resolver.ts +6 -2
  64. package/src/lib/package-version.ts +15 -0
  65. package/src/lib/pinned-cli-command.ts +23 -0
@@ -50,6 +50,48 @@ else
50
50
  fail "symlinked Builder root wrote externally or returned the wrong diagnostic: $(cat "$TMP/symlink-root.out")"
51
51
  fi
52
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
+
53
95
  RACE_PROJECT="$TMP/race-project"
54
96
  RACE_ROOT="$RACE_PROJECT/.kontourai/flow-agents"
55
97
  RACE_MOVED="$RACE_PROJECT/.kontourai/flow-agents-acquired"
@@ -146,6 +188,7 @@ LOCAL_ROOT="$TMP/local/.kontourai/flow-agents"
146
188
  if flow_agents_node "$WRITER" ensure-session \
147
189
  --artifact-root "$LOCAL_ROOT" \
148
190
  --task-slug local-request \
191
+ --actor builder-entry-local \
149
192
  --title "Local request" \
150
193
  --summary "Providerless work still needs an anchor." \
151
194
  --flow-id builder.build \
@@ -158,19 +201,24 @@ const current = JSON.parse(fs.readFileSync(path.join(root, 'current.json'), 'utf
158
201
  const state = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'state.json'), 'utf8'));
159
202
  const workItem = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'work-item.json'), 'utf8'));
160
203
  const flowState = JSON.parse(fs.readFileSync(path.join(path.dirname(root), 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
161
- if (current.active_flow_id !== 'builder.build' || current.active_step_id !== 'pull-work') process.exit(1);
162
- if (state.status !== 'new' || state.phase !== 'pickup') process.exit(1);
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);
163
206
  if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['local:local-request'])) process.exit(1);
164
207
  if (workItem.id !== 'local-request' || workItem.title !== 'Local request') process.exit(1);
165
208
  if (workItem.source_provider?.kind !== 'local' || workItem.source_provider?.path !== 'work-item.json') process.exit(1);
166
- if (flowState.current_step !== 'pull-work' || flowState.subject !== 'local:local-request') process.exit(1);
167
- if (state.flow_run?.current_step !== 'pull-work') process.exit(1);
168
- if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
169
- if (!state.next_action?.command?.includes('builder-run sync')) 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("'workflow' 'status'")) process.exit(1);
170
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);
171
219
  NODE
172
220
  then
173
- pass "providerless request creates a local Work Item and canonical Flow run at pull-work"
221
+ pass "durably acquired local Work Item satisfies pull-work through the trust bundle and Flow advances"
174
222
  else
175
223
  fail "local Work Item or first-step state is invalid"
176
224
  fi
@@ -183,6 +231,7 @@ FLOW_DIGEST_BEFORE="$(find "$TMP/local/.kontourai/flow/runs/local-request" -type
183
231
  if flow_agents_node "$WRITER" ensure-session \
184
232
  --artifact-root "$LOCAL_ROOT" \
185
233
  --task-slug local-request \
234
+ --actor builder-entry-local \
186
235
  --title "Local request" \
187
236
  --summary "Providerless work still needs an anchor." \
188
237
  --flow-id builder.build \
@@ -195,10 +244,10 @@ const project = process.argv[2];
195
244
  const session = process.argv[3];
196
245
  const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
197
246
  const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
198
- if (flowState.current_step !== 'pull-work' || flowState.subject !== 'local:local-request') process.exit(1);
199
- if (sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
200
- if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
201
- if (!sidecar.next_action?.command?.includes('builder-run sync')) 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);
250
+ if (!sidecar.next_action?.command?.includes("'workflow' 'status'")) process.exit(1);
202
251
  NODE
203
252
  then
204
253
  pass "repeated ensure-session loads the canonical Flow run without resetting its history"
@@ -213,19 +262,20 @@ printf 'not a run-store directory\n' > "$BROKEN_PROJECT/.kontourai/flow"
213
262
  if flow_agents_node "$WRITER" ensure-session \
214
263
  --artifact-root "$BROKEN_ROOT" \
215
264
  --task-slug broken-start \
265
+ --actor builder-entry-broken \
216
266
  --title "Broken start" \
217
267
  --summary "Flow startup must fail visibly." \
218
268
  --flow-id builder.build >"$TMP/broken-start.out" 2>&1; then
219
269
  fail "invalid canonical Flow startup should fail ensure-session"
220
270
  elif [[ -f "$BROKEN_ROOT/broken-start/state.json" ]] \
221
271
  && [[ ! -e "$BROKEN_PROJECT/.kontourai/flow/runs/broken-start" ]] \
222
- && grep -q 'canonical Builder Flow run did not start' "$TMP/broken-start.out" \
223
- && grep -q 'flow-agents builder-run start --session-dir .kontourai/flow-agents/broken-start' "$TMP/broken-start.out" \
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" \
224
274
  && node - "$BROKEN_ROOT/broken-start/state.json" <<'NODE'
225
275
  const fs = require('node:fs');
226
276
  const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
227
277
  if (state.flow_run) process.exit(1);
228
- if (!state.next_action?.command?.includes('builder-run start')) process.exit(1);
278
+ if (!state.next_action?.command?.includes("'workflow' 'start'")) process.exit(1);
229
279
  NODE
230
280
  then
231
281
  pass "failed canonical startup is visible and leaves only retryable sidecar guidance"
@@ -233,13 +283,30 @@ else
233
283
  fail "failed canonical startup forged state or lost recovery guidance: $(cat "$TMP/broken-start.out")"
234
284
  fi
235
285
 
236
- if flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
237
- --expectation selected-work \
238
- --status pass \
239
- --summary "Selected local:local-request with scope and acceptance context." \
240
- --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Provider-neutral local Work Item."}' \
241
- >"$TMP/selected-work.out" 2>&1 \
242
- && node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
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'
243
310
  const fs = require('node:fs');
244
311
  const path = require('node:path');
245
312
  const project = process.argv[2];
@@ -251,9 +318,158 @@ if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
251
318
  if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
252
319
  NODE
253
320
  then
254
- 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"
255
471
  else
256
- 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")"
257
473
  fi
258
474
 
259
475
  if FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
@@ -266,12 +482,13 @@ else
266
482
  stop_status=$?
267
483
  if [[ "$stop_status" -eq 2 ]] \
268
484
  && grep -q 'required skills: pickup-probe' "$TMP/stop.err" \
269
- && grep -q 'builder-run sync' "$TMP/stop.err" \
485
+ && grep -q 'next command: sh -c' "$TMP/stop.err" \
270
486
  && grep -q 'release skipped for active Flow run' "$TMP/stop.err" \
271
487
  && node - "$LOCAL_SESSION/state.json" <<'NODE'
272
488
  const fs = require('node:fs');
273
489
  const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
274
490
  if (state.flow_run?.status !== 'active' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
491
+ if (!state.next_action?.command?.includes("'workflow' 'status'")) process.exit(1);
275
492
  NODE
276
493
  then
277
494
  pass "active Flow run blocks Stop, preserves liveness, and exposes executable guidance"
@@ -881,6 +881,103 @@ 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
+ PACKAGE_CLI="$PACKAGE_CONSUMER/node_modules/@kontourai/flow-agents/build/src/cli.js"
900
+ PACKAGE_SESSION="$PACKAGE_PROJECT/.kontourai/flow-agents/packed-builder-entry"
901
+ if (cd "$ROOT_DIR" && npm pack --silent --pack-destination "$TMPDIR_EVAL" >"$PACKAGE_PACK_LOG") \
902
+ && PACKAGE_TARBALL="$(find "$TMPDIR_EVAL" -maxdepth 1 -type f -name 'kontourai-flow-agents-*.tgz' -print -quit)" \
903
+ && [[ -n "$PACKAGE_TARBALL" ]] \
904
+ && npm install --silent --no-audit --no-fund --ignore-scripts --prefix "$PACKAGE_CONSUMER" "$PACKAGE_TARBALL" \
905
+ && (cd "$PACKAGE_AMBIENT" && CODEX_SESSION_ID=packed-package-consumer node "$PACKAGE_CONSUMER/node_modules/@kontourai/flow-agents/build/src/cli/workflow-sidecar.js" ensure-session \
906
+ --artifact-root "$PACKAGE_PROJECT/.kontourai/flow-agents" \
907
+ --task-slug packed-builder-entry \
908
+ --title "Packed Builder entry" \
909
+ --summary "Installed package should project pickup-probe." \
910
+ --flow-id builder.build >/dev/null 2>&1) \
911
+ && node - "$PACKAGE_PROJECT" <<'NODE' &&
912
+ const fs = require('node:fs');
913
+ const path = require('node:path');
914
+ const project = process.argv[2];
915
+ const state = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'packed-builder-entry', 'state.json'), 'utf8'));
916
+ const flow = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'packed-builder-entry', 'state.json'), 'utf8'));
917
+ const bundle = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'packed-builder-entry', 'trust.bundle'), 'utf8'));
918
+ if (flow.current_step !== 'design-probe' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
919
+ if (JSON.stringify(state.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
920
+ if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) process.exit(1);
921
+ NODE
922
+ node --input-type=module - "$PACKAGE_PROJECT" "$PACKAGE_CONSUMER" <<'NODE' &&
923
+ import fs from 'node:fs';
924
+ import path from 'node:path';
925
+ import { generateKeyPairSync, sign } from 'node:crypto';
926
+ import { pathToFileURL } from 'node:url';
927
+ const [project, consumer] = process.argv.slice(2);
928
+ const slug = 'packed-builder-entry';
929
+ const assignment = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'assignment', `${slug}.json`), 'utf8'));
930
+ const state = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', slug, 'state.json'), 'utf8'));
931
+ const packageEntry = path.join(consumer, 'node_modules', '@kontourai', 'flow-agents', 'build', 'src', 'index.js');
932
+ const { builderLifecycleAuthorizationPayload } = await import(pathToFileURL(packageEntry).href);
933
+ const keys = generateKeyPairSync('ed25519');
934
+ const keyId = 'packed-consumer';
935
+ fs.mkdirSync(path.join(project, '.flow-agents'), { recursive: true });
936
+ fs.writeFileSync(path.join(project, '.flow-agents', 'lifecycle-authority-keys.json'), JSON.stringify({
937
+ schema_version: '1.0',
938
+ keys: [{ id: keyId, algorithm: 'ed25519', public_key_pem: keys.publicKey.export({ type: 'spki', format: 'pem' }) }],
939
+ }, null, 2));
940
+ for (const operation of ['cancel', 'archive']) {
941
+ const requestedAt = new Date();
942
+ const unsigned = {
943
+ schema_version: '1.0',
944
+ operation,
945
+ run_id: slug,
946
+ subject: state.work_item_refs[0],
947
+ assignment_actor_key: assignment.actor_key,
948
+ assignment_actor: { ...assignment.actor, human: assignment.actor.human ?? null },
949
+ nonce: `packed-${operation}`,
950
+ expires_at: new Date(requestedAt.getTime() + 60 * 60_000).toISOString(),
951
+ request: {
952
+ reason: `packed consumer ${operation}`,
953
+ authority: { kind: 'user_request', actor: 'packed-fixture', request_ref: `fixture://packed/${operation}`, requested_at: requestedAt.toISOString() },
954
+ },
955
+ };
956
+ const authorization = { ...unsigned, signature: { algorithm: 'ed25519', key_id: keyId, value: sign(null, Buffer.from(builderLifecycleAuthorizationPayload(unsigned)), keys.privateKey).toString('base64') } };
957
+ fs.writeFileSync(path.join(project, `${operation}.authorization.json`), JSON.stringify(authorization, null, 2));
958
+ }
959
+ NODE
960
+ (cd "$PACKAGE_PROJECT" && CODEX_SESSION_ID=packed-package-consumer node "$PACKAGE_CLI" builder-run pause --session-dir "$PACKAGE_SESSION" --reason "packed pause" >/dev/null) \
961
+ && (cd "$PACKAGE_PROJECT" && CODEX_SESSION_ID=packed-package-consumer node "$PACKAGE_CLI" builder-run resume --session-dir "$PACKAGE_SESSION" --reason "packed resume" >/dev/null) \
962
+ && (cd "$PACKAGE_PROJECT" && node "$PACKAGE_CLI" builder-run cancel --session-dir "$PACKAGE_SESSION" --authorization-file "$PACKAGE_PROJECT/cancel.authorization.json" >/dev/null) \
963
+ && (cd "$PACKAGE_PROJECT" && node "$PACKAGE_CLI" builder-run archive --session-dir "$PACKAGE_SESSION" --authorization-file "$PACKAGE_PROJECT/archive.authorization.json" >/dev/null) \
964
+ && node - "$PACKAGE_PROJECT" <<'NODE'
965
+ const fs = require('node:fs');
966
+ const path = require('node:path');
967
+ const project = process.argv[2];
968
+ const slug = 'packed-builder-entry';
969
+ const archived = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow-agents', 'archive', slug, 'state.json'), 'utf8'));
970
+ const flow = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', slug, 'state.json'), 'utf8'));
971
+ const consumed = fs.readdirSync(path.join(project, '.kontourai', 'flow-agents', 'lifecycle-authority', 'consumed'));
972
+ if (archived.status !== 'archived' || flow.status !== 'canceled' || consumed.length !== 2) process.exit(1);
973
+ if (fs.existsSync(path.join(project, '.kontourai', 'flow-agents', slug))) process.exit(1);
974
+ NODE
975
+ then
976
+ _pass "packed npm consumer projects Builder entry and executes lifecycle commands"
977
+ else
978
+ _fail "packed npm consumer did not execute canonical Builder entry and lifecycle commands"
979
+ fi
980
+
884
981
  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
982
  _pass "Codex full install ships kit-skills and standalone skills together"
886
983
  else
@@ -377,6 +377,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
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" \
@@ -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" \
@@ -348,6 +349,7 @@ if flow_agents_node "$WRITER" ensure-session \
348
349
  --artifact-root "$SESSION_ROOT" \
349
350
  --task-slug dual-emit-policy \
350
351
  --flow-id builder.build \
352
+ --skip-ownership-guard \
351
353
  --title "Declared Policy Test" \
352
354
  --summary "Test declared-only policy emit for ADR 0016 P-d." \
353
355
  --criterion "Policy passes" \
@@ -51,6 +51,7 @@ mkdir -p "$MAIN_AROOT"
51
51
  if flow_agents_node "$WRITER" ensure-session \
52
52
  --artifact-root "$MAIN_AROOT" \
53
53
  --task-slug "$SLUG" \
54
+ --actor activation-test-actor \
54
55
  --title "Step 1 activation test" \
55
56
  --summary "Test that --flow-id builder.build activates the FlowDefinition-driven path." \
56
57
  --criterion "All gates produce declared claims" \
@@ -65,12 +66,14 @@ node -e "
65
66
  const fs = require('fs');
66
67
  const c = JSON.parse(fs.readFileSync('$MAIN_AROOT/current.json', 'utf8'));
67
68
  const flow = JSON.parse(fs.readFileSync('$TMP/main-project/.kontourai/flow/runs/$SLUG/state.json', 'utf8'));
69
+ const bundle = JSON.parse(fs.readFileSync('$SESSION_DIR/trust.bundle', 'utf8'));
68
70
  if (c.active_flow_id !== 'builder.build') throw new Error('expected active_flow_id=builder.build, got ' + c.active_flow_id);
69
- if (!c.active_step_id) throw new Error('expected active_step_id to be set (first step default), got ' + c.active_step_id);
70
- if (flow.status !== 'active' || flow.current_step !== 'pull-work') throw new Error('canonical Flow did not start at pull-work: ' + JSON.stringify(flow));
71
+ if (c.active_step_id !== 'design-probe') throw new Error('expected active_step_id=design-probe, got ' + c.active_step_id);
72
+ if (flow.status !== 'active' || flow.current_step !== 'design-probe') throw new Error('canonical Flow did not advance through trusted selection: ' + JSON.stringify(flow));
73
+ if (!(bundle.claims || []).some((claim) => claim.claimType === 'builder.pull-work.selected' && claim.status === 'verified')) throw new Error('missing verified selected-work claim');
71
74
  console.log('current.json: active_flow_id=' + c.active_flow_id + ' active_step_id=' + c.active_step_id);
72
75
  " 2>&1 \
73
- && _pass "ensure-session writes current projection and a canonical Flow run at pull-work" \
76
+ && _pass "ensure-session records trusted selection and projects the canonical Flow run at design-probe" \
74
77
  || _fail "ensure-session did not create and project the canonical Flow run"
75
78
 
76
79
  # ─── TEST 2: advance-state sets active_step_id via phase_map ─────────────────
@@ -15,10 +15,10 @@ _pass() { echo " ✓ $1"; }
15
15
  _fail() { echo " ✗ $1"; errors=$((errors + 1)); }
16
16
 
17
17
  REPO="$TMPDIR_EVAL/repo"
18
- mkdir -p "$REPO/.flow-agents/stuck"
18
+ mkdir -p "$REPO/.kontourai/flow-agents/stuck"
19
19
  printf '# Test Repo\n' > "$REPO/AGENTS.md"
20
20
  printf '# Stuck\n\nbranch: main\nstatus: executing\ntype: deliver\n\n## Plan\n\nTBD.\n' \
21
- > "$REPO/.flow-agents/stuck/stuck--deliver.md"
21
+ > "$REPO/.kontourai/flow-agents/stuck/stuck--deliver.md"
22
22
 
23
23
  PAYLOAD="{\"hook_event_name\":\"Stop\",\"cwd\":\"$REPO\"}"
24
24
 
@@ -54,7 +54,7 @@ c4=$(run_block "$TMPDIR_EVAL/b4.err")
54
54
  printf '%s' "$PAYLOAD" | FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=3 node "$ROOT/scripts/hooks/stop-goal-fit.js" >/dev/null 2>/dev/null
55
55
  # mutate the artifact so the warning set differs
56
56
  printf '# Stuck\n\nbranch: main\nstatus: verifying\ntype: deliver\n\n## Plan\n\nDifferent.\n' \
57
- > "$REPO/.flow-agents/stuck/stuck--deliver.md"
57
+ > "$REPO/.kontourai/flow-agents/stuck/stuck--deliver.md"
58
58
  cd=$(run_block "$TMPDIR_EVAL/bd.err")
59
59
  [[ "$cd" -eq 2 ]] && rg -q 'Stop blocked .* \(block 1; after 3 identical blocks' "$TMPDIR_EVAL/bd.err" \
60
60
  && _pass "changed goal-fit gap resets the streak to 1/3" \
@@ -129,6 +129,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
129
129
  --title "Ensure Default Step" \
130
130
  --summary "Test ensure-session default step." \
131
131
  --flow-id builder.build \
132
+ --skip-ownership-guard \
132
133
  --timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
133
134
 
134
135
  node -e "
@@ -154,6 +155,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
154
155
  --title "Gate Claim Test" \
155
156
  --summary "Test gate claim producer." \
156
157
  --flow-id builder.build \
158
+ --skip-ownership-guard \
157
159
  --timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
158
160
 
159
161
  flow_agents_node "workflow-sidecar" init-plan "$CLAIM_ROOT/gate-claim/gate-claim--deliver.md" \
@@ -261,6 +263,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
261
263
  --title "Tamper Test" \
262
264
  --summary "Testing tamper detection." \
263
265
  --flow-id builder.build \
266
+ --skip-ownership-guard \
264
267
  --timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
265
268
 
266
269
  flow_agents_node "workflow-sidecar" init-plan "$T_DIR/.kontourai/flow-agents/tamper/tamper--deliver.md" \
@@ -364,6 +367,7 @@ flow_agents_node "workflow-sidecar" ensure-session \
364
367
  --title "Clean Test" \
365
368
  --summary "Testing clean gate claim." \
366
369
  --flow-id builder.build \
370
+ --skip-ownership-guard \
367
371
  --timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
368
372
 
369
373
  flow_agents_node "workflow-sidecar" init-plan "$C_DIR/.kontourai/flow-agents/clean/clean--deliver.md" \