@kontourai/flow-agents 3.8.0 → 3.10.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 (109) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +1 -1
  4. package/build/src/builder-flow-runtime.js +16 -10
  5. package/build/src/cli/assignment-provider.js +4 -18
  6. package/build/src/cli/continuation-adapter.d.ts +26 -0
  7. package/build/src/cli/continuation-adapter.js +243 -0
  8. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  9. package/build/src/cli/workflow-sidecar.js +15 -20
  10. package/build/src/cli/workflow.js +99 -6
  11. package/build/src/continuation-driver.d.ts +116 -0
  12. package/build/src/continuation-driver.js +527 -0
  13. package/build/src/index.d.ts +2 -0
  14. package/build/src/index.js +1 -0
  15. package/build/src/tools/build-universal-bundles.js +52 -25
  16. package/build/src/tools/validate-source-tree.js +5 -2
  17. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  18. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  19. package/context/scripts/hooks/workflow-steering.js +24 -15
  20. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  21. package/context/scripts/telemetry/lib/transport.sh +139 -37
  22. package/context/scripts/telemetry/lib/usage.sh +282 -32
  23. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  24. package/context/scripts/telemetry/telemetry.sh +76 -1
  25. package/docs/coordination-guide.md +21 -5
  26. package/docs/fixture-ownership.md +2 -1
  27. package/docs/public-workflow-cli.md +98 -0
  28. package/docs/spec/runtime-hook-surface.md +8 -4
  29. package/docs/workflow-usage-guide.md +7 -0
  30. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  31. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  32. package/evals/ci/run-baseline.sh +2 -0
  33. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  34. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  35. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  36. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  38. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  42. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  43. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  44. package/evals/integration/test_actor_identity.sh +63 -0
  45. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  46. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  47. package/evals/integration/test_bundle_install.sh +106 -17
  48. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  49. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  50. package/evals/integration/test_ci_actor_identity.sh +2 -2
  51. package/evals/integration/test_command_log_concurrency.sh +11 -0
  52. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  53. package/evals/integration/test_command_log_integrity.sh +11 -0
  54. package/evals/integration/test_current_json_per_actor.sh +416 -21
  55. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  56. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  57. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  58. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  59. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  60. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  61. package/evals/integration/test_gate_lockdown.sh +10 -0
  62. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  63. package/evals/integration/test_goal_fit_hook.sh +257 -9
  64. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  65. package/evals/integration/test_install_merge.sh +141 -3
  66. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  67. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  68. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  69. package/evals/integration/test_public_workflow_cli.sh +6 -6
  70. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  71. package/evals/integration/test_stop_hook_release.sh +111 -11
  72. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  73. package/evals/integration/test_verify_hold.sh +64 -1
  74. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  75. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  76. package/evals/run.sh +2 -0
  77. package/evals/static/test_universal_bundles.sh +123 -0
  78. package/package.json +4 -4
  79. package/packaging/README.md +21 -0
  80. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  81. package/scripts/README.md +2 -1
  82. package/scripts/audit-codex-legacy-agents.js +43 -0
  83. package/scripts/classify-codex-legacy-agents.js +71 -0
  84. package/scripts/hooks/evidence-capture.js +18 -6
  85. package/scripts/hooks/lib/actor-identity.js +62 -29
  86. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  87. package/scripts/hooks/lib/current-pointer.js +114 -12
  88. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  89. package/scripts/hooks/stop-goal-fit.js +244 -73
  90. package/scripts/hooks/workflow-steering.js +24 -15
  91. package/scripts/install-codex-home.sh +8 -7
  92. package/scripts/telemetry/lib/pricing.sh +18 -6
  93. package/scripts/telemetry/lib/transport.sh +71 -0
  94. package/scripts/telemetry/lib/usage.sh +282 -32
  95. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  96. package/scripts/telemetry/telemetry.sh +76 -1
  97. package/src/builder-flow-runtime.ts +9 -3
  98. package/src/cli/assignment-provider.ts +5 -26
  99. package/src/cli/builder-flow-runtime.test.mjs +215 -2
  100. package/src/cli/continuation-adapter.ts +265 -0
  101. package/src/cli/continuation-driver.test.mjs +1303 -0
  102. package/src/cli/public-api.test.mjs +1 -0
  103. package/src/cli/validate-workflow-artifacts.ts +19 -1
  104. package/src/cli/workflow-sidecar.ts +18 -23
  105. package/src/cli/workflow.ts +113 -6
  106. package/src/continuation-driver.ts +638 -0
  107. package/src/index.ts +24 -0
  108. package/src/tools/build-universal-bundles.ts +61 -25
  109. package/src/tools/validate-source-tree.ts +5 -2
@@ -169,8 +169,19 @@ else
169
169
  fail "legacy global current.json did not name B as expected: got '$LEGACY_SLUG' (err: $(cat "$TMPDIR_EVAL/ac7-current-legacy.err"))"
170
170
  fi
171
171
 
172
- PER_ACTOR_FILE_A="$AC7_ROOT/current/eval-actor-a-session.json"
173
- PER_ACTOR_FILE_B="$AC7_ROOT/current/eval-actor-b-session.json"
172
+ # #440 fix-wave 2: the per-actor filename is now collision-resistant (sanitized prefix + hash of
173
+ # the FULL actor key, see current-pointer.js's perActorCurrentFile) -- compute the expected path
174
+ # via the real function rather than re-deriving the naming rule by hand.
175
+ PER_ACTOR_FILE_A="$(CP_HELPER_ARG="$CURRENT_POINTER_HELPER" ROOT_ARG="$AC7_ROOT" ACTOR_ARG="eval-actor-a-session" node - <<'NODE'
176
+ const { perActorCurrentFile } = require(process.env.CP_HELPER_ARG);
177
+ process.stdout.write(perActorCurrentFile(process.env.ROOT_ARG, process.env.ACTOR_ARG));
178
+ NODE
179
+ )"
180
+ PER_ACTOR_FILE_B="$(CP_HELPER_ARG="$CURRENT_POINTER_HELPER" ROOT_ARG="$AC7_ROOT" ACTOR_ARG="eval-actor-b-session" node - <<'NODE'
181
+ const { perActorCurrentFile } = require(process.env.CP_HELPER_ARG);
182
+ process.stdout.write(perActorCurrentFile(process.env.ROOT_ARG, process.env.ACTOR_ARG));
183
+ NODE
184
+ )"
174
185
  [[ -f "$PER_ACTOR_FILE_A" ]] && pass "actor A's per-actor current file exists on disk" || fail "actor A's per-actor current file was not written"
175
186
  [[ -f "$PER_ACTOR_FILE_B" ]] && pass "actor B's per-actor current file exists on disk" || fail "actor B's per-actor current file was not written"
176
187
 
@@ -178,9 +189,13 @@ PER_ACTOR_FILE_B="$AC7_ROOT/current/eval-actor-b-session.json"
178
189
  echo "--- 3. legacy-only fixture: existing consumers resolve identically to pre-#291 output (AC8) ---"
179
190
 
180
191
  # 3a. stop-goal-fit.js -- reuses test_goal_fit_hook.sh's own fixture/assertion verbatim, run
181
- # under a DIFFERENT ambient actor that has no per-actor current file -- proves the compat shim
182
- # falls straight through to the legacy file (same output regardless of actor) exactly as
183
- # pre-#291 (when this file had no actor-awareness at all).
192
+ # under a genuinely UNRESOLVED actor (FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED escape hatch,
193
+ # matching test_stop_hook_release.sh's F(iii) convention) -- proves the compat shim falls
194
+ # straight through to the legacy file exactly as pre-#291 (when this file had no actor-awareness
195
+ # at all). #440: a RESOLVED actor with no own per-actor pointer no longer falls back to this
196
+ # legacy file for BLOCKING purposes (see new section 7) -- that distinct, resolved-actor case is
197
+ # the bug #440 fixes, so this section must force a genuinely unresolved actor to keep testing the
198
+ # unchanged compat-shim path AC4 is about, not the now-intentionally-changed resolved-actor path.
184
199
  GOAL_FIT_REPO="$TMPDIR_EVAL/goal-fit-legacy/repo"
185
200
  mkdir -p "$GOAL_FIT_REPO/.kontourai/flow-agents/feedback-loop"
186
201
  LEGACY_POINTER_LINE="{\"schema_version\":\"1.0\",\"active_slug\":\"feedback-loop\",\"artifact_dir\":\"feedback-loop\"}"
@@ -200,7 +215,7 @@ type: deliver
200
215
  Implementation plan exists, but no goal fit state exists yet.
201
216
  MARKDOWN
202
217
 
203
- if FLOW_AGENTS_ACTOR=eval-actor-no-per-actor-file FLOW_AGENTS_GOAL_FIT_MODE=warn FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
218
+ if FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1 NODE_ENV=test FLOW_AGENTS_GOAL_FIT_MODE=warn FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
204
219
  node "$GOAL_FIT_GATE" >"$TMPDIR_EVAL/goal-fit-legacy.out" 2>"$TMPDIR_EVAL/goal-fit-legacy.err" <<JSON
205
220
  {"hook_event_name":"Stop","cwd":"$GOAL_FIT_REPO"}
206
221
  JSON
@@ -216,7 +231,8 @@ fi
216
231
 
217
232
  # 3b. evidence-capture.js's resolveArtifactDir -- reuses test_command_log_concurrency.sh's
218
233
  # fixture: current.json (legacy-only) names a slug; a captured PostToolUse event must land in
219
- # THAT slug's command-log.jsonl, under a different ambient actor with no per-actor file.
234
+ # THAT slug's command-log.jsonl, under a genuinely UNRESOLVED actor (#440: see 3a's comment --
235
+ # a RESOLVED actor with no own pointer now intentionally returns null here instead, see section 7).
220
236
  EVIDENCE_REPO="$TMPDIR_EVAL/evidence-legacy/repo"
221
237
  EVIDENCE_SLUG="legacy-evidence-slug"
222
238
  mkdir -p "$EVIDENCE_REPO/.kontourai/flow-agents/$EVIDENCE_SLUG"
@@ -230,7 +246,7 @@ printf '%s' "$EVIDENCE_STATE_LINE" | safe_write "$EVIDENCE_REPO/.kontourai/flow-
230
246
 
231
247
  EVIDENCE_EVENT_LINE="{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"cwd\":\"$EVIDENCE_REPO\",\"tool_input\":{\"command\":\"echo legacy-actor-check\"},\"tool_response\":{\"exitCode\":0,\"stdout\":\"ok\"}}"
232
248
  printf '%s' "$EVIDENCE_EVENT_LINE" \
233
- | FLOW_AGENTS_ACTOR=eval-actor-no-per-actor-file node "$EVIDENCE_CAPTURE" >"$TMPDIR_EVAL/evidence-legacy.out" 2>"$TMPDIR_EVAL/evidence-legacy.err"
249
+ | FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1 NODE_ENV=test node "$EVIDENCE_CAPTURE" >"$TMPDIR_EVAL/evidence-legacy.out" 2>"$TMPDIR_EVAL/evidence-legacy.err"
234
250
 
235
251
  EVIDENCE_LOG_FILE="$EVIDENCE_REPO/.kontourai/flow-agents/$EVIDENCE_SLUG/command-log.jsonl"
236
252
  if [[ -f "$EVIDENCE_LOG_FILE" ]] && grep -qF "legacy-actor-check" "$EVIDENCE_LOG_FILE"; then
@@ -295,7 +311,8 @@ else
295
311
  fi
296
312
 
297
313
  # 3d. workflow-steering.js's ambient state lookup -- reuses test_workflow_steering_hook.sh's
298
- # steering-demo fixture/assertion verbatim, under a different ambient actor.
314
+ # steering-demo fixture/assertion verbatim, under a genuinely UNRESOLVED actor (#440: see 3a's
315
+ # comment -- a RESOLVED actor with no own pointer now intentionally sees no banner, see section 7).
299
316
  STEERING_REPO="$TMPDIR_EVAL/steering-legacy/repo"
300
317
  mkdir -p "$STEERING_REPO/.kontourai/flow-agents/steering-demo"
301
318
  mkdir -p "$STEERING_REPO/docs"
@@ -317,7 +334,7 @@ safe_write "$STEERING_REPO/.kontourai/flow-agents/steering-demo/state.json" <<'J
317
334
  }
318
335
  JSON
319
336
 
320
- STEERING_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-no-per-actor-file node "$STEERING" <<JSON
337
+ STEERING_OUT="$(FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1 NODE_ENV=test node "$STEERING" <<JSON
321
338
  {"hook_event_name":"UserPromptSubmit","cwd":"$STEERING_REPO","prompt":"continue"}
322
339
  JSON
323
340
  )"
@@ -451,16 +468,19 @@ fi
451
468
 
452
469
  # --- 6. F3 (fix-plan iteration 1, code-review-291 MEDIUM): legacy current.json names a
453
470
  # still-ACTIVE, non-newest-mtime session while a NEWER active state.json exists elsewhere under
454
- # the same artifact root, under a RESOLVED actor with no per-actor current file. Pre-#291,
471
+ # the same artifact root, under a genuinely UNRESOLVED actor (#440: this section's fixture uses
472
+ # the FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED escape hatch -- a RESOLVED actor with no per-actor
473
+ # current file no longer falls through to the legacy pointer at all, see section 7; this section
474
+ # is specifically about the unchanged D3/unresolved-actor compat path). Pre-#291,
455
475
  # latestWorkflowState() was a pure global newest-mtime scan with zero current.json involvement,
456
476
  # so the newer state.json would win. Post-#291, actorScopedWorkflowState() is consulted first and
457
- # falls through to the legacy current.json for an actor with no per-actor file (the compat-shim
458
- # guarantee section 3 already covers) -- but if that legacy pointer names a still-ACTIVE session,
459
- # it is now returned IMMEDIATELY, without ever comparing mtimes against the newer state.json. This
460
- # section test-pins that specific, deliberate behavior change (current.json preference wins over
461
- # newest-mtime) for the single-actor/resolved-actor case, not just the multi-actor case AC7/AC8
477
+ # falls through to the legacy current.json for an unresolved actor (the compat-shim guarantee
478
+ # section 3 already covers) -- but if that legacy pointer names a still-ACTIVE session, it is now
479
+ # returned IMMEDIATELY, without ever comparing mtimes against the newer state.json. This section
480
+ # test-pins that specific, deliberate behavior change (current.json preference wins over
481
+ # newest-mtime) for the single-actor/unresolved-actor case, not just the multi-actor case AC7/AC8
462
482
  # are framed around -- the review's flagged, previously test-uncovered edge.
463
- echo "--- 6. legacy current.json (non-newest, still-active) wins over a newer state.json elsewhere, under a resolved actor (F3, fix-plan iteration 1) ---"
483
+ echo "--- 6. legacy current.json (non-newest, still-active) wins over a newer state.json elsewhere, under an unresolved actor (F3, fix-plan iteration 1) ---"
464
484
 
465
485
  F3_REPO="$TMPDIR_EVAL/f3-legacy-vs-newest/repo"
466
486
  mkdir -p "$F3_REPO/.kontourai/flow-agents/older-named-slug"
@@ -506,20 +526,395 @@ safe_write "$F3_REPO/.kontourai/flow-agents/newer-unnamed-slug/state.json" <<'JS
506
526
  }
507
527
  JSON
508
528
 
509
- # A RESOLVED actor (FLOW_AGENTS_ACTOR set) with no per-actor current file -- actorScopedWorkflowState
510
- # falls through to the legacy current.json (same compat-shim guarantee as section 3), which names
529
+ # A genuinely UNRESOLVED actor (#440: see 3a's comment -- a RESOLVED actor with no per-actor
530
+ # current file no longer falls through here, see section 7) -- actorScopedWorkflowState falls
531
+ # through to the legacy current.json (same compat-shim guarantee as section 3), which names
511
532
  # the OLDER-but-still-active session.
512
- F3_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-f3-resolved node "$ROOT/scripts/hooks/workflow-steering.js" <<JSON
533
+ F3_OUT="$(FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1 NODE_ENV=test node "$ROOT/scripts/hooks/workflow-steering.js" <<JSON
513
534
  {"hook_event_name":"UserPromptSubmit","cwd":"$F3_REPO","prompt":"continue"}
514
535
  JSON
515
536
  )"
516
537
 
517
538
  if [[ "$F3_OUT" == *"older-named-slug"* && "$F3_OUT" != *"newer-unnamed-slug"* ]]; then
518
- pass "workflow-steering.js prefers the legacy current.json's named ACTIVE session over a newer-mtime state.json elsewhere, under a resolved actor with no per-actor file (F3 -- pins the post-#291 behavior change explicitly, not just AC7/AC8's multi-actor framing)"
539
+ pass "workflow-steering.js prefers the legacy current.json's named ACTIVE session over a newer-mtime state.json elsewhere, under an unresolved actor (F3 -- pins the post-#291 behavior change explicitly, not just AC7/AC8's multi-actor framing; #440: a resolved actor with no per-actor file no longer takes this path at all, see section 7)"
519
540
  else
520
541
  fail "workflow-steering.js did not prefer the legacy-named active session over the newer-mtime one: $F3_OUT"
521
542
  fi
522
543
 
544
+ # --- 7. #440: two-session ownership isolation (readOwnCurrentPointer) --------------------------
545
+ # Distinct from section 2 (AC7, both A and B have run ensure-session): here actor A does NOT run
546
+ # ensure-session at all before the "not own pointer" assertions below -- the "resolved actor, no
547
+ # own per-actor pointer at all" case readOwnCurrentPointer's D1 rule (never fall back to the
548
+ # shared legacy current.json / a global scan) is specifically about. Positive controls (actor A's
549
+ # own gate/banner/capture keep working once A DOES have its own pointer) run last, in their own
550
+ # subsection, so they never contaminate the "no own pointer" preconditions the earlier assertions
551
+ # depend on.
552
+ echo "--- 7. #440: two-session ownership isolation (readOwnCurrentPointer) ---"
553
+
554
+ # This file's own header only sets `set -uo pipefail` (no errexit) -- but section 4's
555
+ # config-protection.js checks toggle `set -e` back ON after their own `set +e`/`set -e`
556
+ # bracketing and never restore the header's original (errexit-off) mode, so errexit is (latently,
557
+ # unintentionally) still active by the time execution reaches here. This section deliberately
558
+ # invokes commands expected to exit nonzero (e.g. the AC2 anti-gaming positive control, which
559
+ # must itself observe a block/exit!=0) via plain `VAR="$(...)"` assignments -- under errexit a
560
+ # nonzero-returning command substitution assignment terminates the whole script immediately, with
561
+ # no error message, silently truncating every assertion after it. `set +e` restores this section to
562
+ # the file's own declared header mode for the remainder of the file (safe: sections 1-6 above are
563
+ # unaffected since they already ran; nothing below this section depends on errexit being on).
564
+ set +e
565
+
566
+ AC440_REPO="$TMPDIR_EVAL/ac440-project"
567
+ AC440_ROOT="$AC440_REPO/.kontourai/flow-agents"
568
+ mkdir -p "$AC440_REPO/docs"
569
+ printf '# Test Repo\n' > "$AC440_REPO/AGENTS.md"
570
+ printf '# Context Map\n' > "$AC440_REPO/docs/context-map.md"
571
+
572
+ # 7.0 Setup: actor B runs ensure-session on a real Work Item (establishing B's own per-actor
573
+ # current/eval-actor-440-b.json AND the shared legacy current.json, exactly like section 2's
574
+ # AC7_ROOT setup), then B's session is mutated into a genuine, blocking evidence gap (execution
575
+ # phase, no trust.bundle) so the "not blocked by B" assertions below are a real differential test,
576
+ # not a tautology -- mirrors this file's own section 3a fixture shape (status:executing markdown +
577
+ # no trust.bundle) and test_stop_hook_release.sh's seed_session/write_session_state convention.
578
+ flow_agents_node "workflow-sidecar" ensure-session \
579
+ --artifact-root "$AC440_ROOT" \
580
+ --work-item "kontourai/flow-agents#94401" \
581
+ --actor eval-actor-440-b \
582
+ --source-request "Actor B's own subject, with a real evidence gap." \
583
+ --summary "Actor B session with a deliberate evidence gap." \
584
+ >"$TMPDIR_EVAL/ac440-b-ensure.out" 2>"$TMPDIR_EVAL/ac440-b-ensure.err"
585
+ AC440_B_STATUS=$?
586
+
587
+ AC440_B_SLUG="kontourai-flow-agents-94401"
588
+ AC440_B_DIR="$AC440_ROOT/$AC440_B_SLUG"
589
+
590
+ if [[ "$AC440_B_STATUS" -eq 0 && -d "$AC440_B_DIR" ]]; then
591
+ pass "setup: actor B's ensure-session establishes B's own per-actor current file and a real session dir"
592
+ else
593
+ fail "setup: actor B's ensure-session failed: $(cat "$TMPDIR_EVAL/ac440-b-ensure.out" "$TMPDIR_EVAL/ac440-b-ensure.err")"
594
+ fi
595
+
596
+ AC440_B_STATE_LINE="{\"schema_version\":\"1.0\",\"task_slug\":\"$AC440_B_SLUG\",\"status\":\"in_progress\",\"phase\":\"execution\",\"updated_at\":\"2026-07-13T00:00:00Z\",\"next_action\":{\"status\":\"continue\",\"summary\":\"Actor B's own unresolved evidence gap.\"}}"
597
+ printf '%s' "$AC440_B_STATE_LINE" | safe_write "$AC440_B_DIR/state.json"
598
+
599
+ cat > "$AC440_B_DIR/${AC440_B_SLUG}--deliver.md" <<MARKDOWN
600
+ # ${AC440_B_SLUG}
601
+
602
+ branch: main
603
+ worktree: main
604
+ created: 2026-07-13
605
+ status: executing
606
+ type: deliver
607
+
608
+ ## Plan
609
+
610
+ Actor B's own session, deliberately left with a genuine evidence gap (no trust.bundle) so this
611
+ section's "actor A is not blocked by B's gap" assertions are a real differential test.
612
+ MARKDOWN
613
+
614
+ # 7.1 AC1 (liveness): a `claim` liveness event for B's own subject/actor already exists in the
615
+ # shared liveness stream (ensure-session emits one as part of its own claim). Invoking the Stop
616
+ # hook as actor A (who has NOT run ensure-session -- no own per-actor pointer at all) must never
617
+ # append a `release`/other event attributed to A's own actor string for B's subject -- readActiveSlug
618
+ # resolving B's slug for A would be exactly the liveness half of the bug #440 fixes.
619
+ AC440_LIVENESS_FILE="$AC440_ROOT/liveness/events.jsonl"
620
+ AC440_LIVENESS_LINES_BEFORE=0
621
+ [[ -f "$AC440_LIVENESS_FILE" ]] && AC440_LIVENESS_LINES_BEFORE="$(wc -l < "$AC440_LIVENESS_FILE" | tr -d ' ')"
622
+
623
+ FLOW_AGENTS_ACTOR=eval-actor-440-a FLOW_AGENTS_GOAL_FIT_MODE=warn node "$GOAL_FIT_GATE" \
624
+ >"$TMPDIR_EVAL/ac440-a-liveness.out" 2>"$TMPDIR_EVAL/ac440-a-liveness.err" <<JSON
625
+ {"hook_event_name":"Stop","cwd":"$AC440_REPO"}
626
+ JSON
627
+
628
+ if grep -q "\"subjectId\":\"$AC440_B_SLUG\",\"actor\":\"eval-actor-440-a\"" "$AC440_LIVENESS_FILE" 2>/dev/null; then
629
+ fail "AC1: actor A's Stop hook emitted a liveness event naming B's slug under A's own actor identity"
630
+ else
631
+ pass "AC1: actor A's Stop hook (no own per-actor pointer) emits no liveness event naming B's subject under A's actor identity"
632
+ fi
633
+
634
+ # 7.2 AC2 (not blocked by B): actor A's Stop hook, in block mode, must exit 0 and never mention
635
+ # B's slug -- B's own real evidence gap is informational-only from A's perspective, never blocking.
636
+ AC440_A_NOTBLOCKED_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-440-a FLOW_AGENTS_GOAL_FIT_MODE=block node "$GOAL_FIT_GATE" 2>"$TMPDIR_EVAL/ac440-a-notblocked.err" <<JSON
637
+ {"hook_event_name":"Stop","cwd":"$AC440_REPO"}
638
+ JSON
639
+ )"
640
+ AC440_A_NOTBLOCKED_STATUS=$?
641
+
642
+ if [[ "$AC440_A_NOTBLOCKED_STATUS" -eq 0 ]]; then
643
+ pass "AC2: actor A's Stop hook (no own pointer, B has a real evidence gap) exits 0 -- not blocked by B's gap"
644
+ else
645
+ fail "AC2: actor A's Stop hook unexpectedly blocked (exit=$AC440_A_NOTBLOCKED_STATUS): $AC440_A_NOTBLOCKED_OUT $(cat "$TMPDIR_EVAL/ac440-a-notblocked.err")"
646
+ fi
647
+
648
+ if grep -qF "$AC440_B_SLUG" "$TMPDIR_EVAL/ac440-a-notblocked.err"; then
649
+ fail "AC2: actor A's Stop hook stderr unexpectedly named B's slug: $(cat "$TMPDIR_EVAL/ac440-a-notblocked.err")"
650
+ else
651
+ pass "AC2: actor A's Stop hook stderr never names B's slug"
652
+ fi
653
+
654
+ # 7.3 AC1 (banner): workflow-steering.js's SessionStart RESUME banner must never re-ground onto
655
+ # B's active session for actor A (no own per-actor pointer).
656
+ AC440_STEERING_A_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-440-a node "$STEERING" <<JSON
657
+ {"hook_event_name":"SessionStart","cwd":"$AC440_REPO"}
658
+ JSON
659
+ )"
660
+
661
+ if [[ "$AC440_STEERING_A_OUT" != *"RESUME:"* && "$AC440_STEERING_A_OUT" != *"$AC440_B_SLUG"* ]]; then
662
+ pass "AC1: workflow-steering.js's SessionStart banner (actor A, no own pointer) shows no RESUME and never names B's slug"
663
+ else
664
+ fail "AC1: workflow-steering.js's SessionStart banner unexpectedly showed RESUME or B's slug for actor A: $AC440_STEERING_A_OUT"
665
+ fi
666
+
667
+ # 7.4 Evidence-capture (Task 2.4 coverage): a PostToolUse event captured while actor A has no own
668
+ # per-actor pointer must never land in B's command-log.jsonl.
669
+ AC440_B_LOG="$AC440_B_DIR/command-log.jsonl"
670
+ AC440_EVIDENCE_EVENT="{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"cwd\":\"$AC440_REPO\",\"tool_input\":{\"command\":\"echo actor-a-must-not-land-in-b-log\"},\"tool_response\":{\"exitCode\":0,\"stdout\":\"ok\"}}"
671
+ printf '%s' "$AC440_EVIDENCE_EVENT" \
672
+ | FLOW_AGENTS_ACTOR=eval-actor-440-a node "$EVIDENCE_CAPTURE" >"$TMPDIR_EVAL/ac440-evidence-a.out" 2>"$TMPDIR_EVAL/ac440-evidence-a.err"
673
+
674
+ if [[ -f "$AC440_B_LOG" ]] && grep -qF "actor-a-must-not-land-in-b-log" "$AC440_B_LOG"; then
675
+ fail "evidence-capture.js (actor A, no own pointer) wrote A's tool activity into B's command-log.jsonl"
676
+ else
677
+ pass "evidence-capture.js (actor A, no own pointer) never writes A's tool activity into B's command-log.jsonl"
678
+ fi
679
+
680
+ # 7.5 Positive controls: actor A's OWN gate/banner/capture keep working exactly as before, once A
681
+ # has its own per-actor pointer -- the critical anti-gaming assertion (own gaps still gate; own
682
+ # work still steers/captures). Run last so the earlier "no own pointer" assertions above are never
683
+ # contaminated by A's own session existing.
684
+ flow_agents_node "workflow-sidecar" ensure-session \
685
+ --artifact-root "$AC440_ROOT" \
686
+ --work-item "kontourai/flow-agents#94402" \
687
+ --actor eval-actor-440-a \
688
+ --source-request "Actor A's own subject, with its own deliberate evidence gap." \
689
+ --summary "Actor A session with its own deliberate evidence gap." \
690
+ >"$TMPDIR_EVAL/ac440-a-ensure.out" 2>"$TMPDIR_EVAL/ac440-a-ensure.err"
691
+ AC440_A_STATUS=$?
692
+
693
+ AC440_A_SLUG="kontourai-flow-agents-94402"
694
+ AC440_A_DIR="$AC440_ROOT/$AC440_A_SLUG"
695
+
696
+ if [[ "$AC440_A_STATUS" -eq 0 && -d "$AC440_A_DIR" ]]; then
697
+ pass "positive control setup: actor A's own ensure-session establishes A's own per-actor current file"
698
+ else
699
+ fail "positive control setup: actor A's own ensure-session failed: $(cat "$TMPDIR_EVAL/ac440-a-ensure.out" "$TMPDIR_EVAL/ac440-a-ensure.err")"
700
+ fi
701
+
702
+ AC440_A_STATE_LINE="{\"schema_version\":\"1.0\",\"task_slug\":\"$AC440_A_SLUG\",\"status\":\"in_progress\",\"phase\":\"execution\",\"updated_at\":\"2026-07-13T00:00:00Z\",\"next_action\":{\"status\":\"continue\",\"summary\":\"Actor A's own unresolved evidence gap.\"}}"
703
+ printf '%s' "$AC440_A_STATE_LINE" | safe_write "$AC440_A_DIR/state.json"
704
+
705
+ cat > "$AC440_A_DIR/${AC440_A_SLUG}--deliver.md" <<MARKDOWN
706
+ # ${AC440_A_SLUG}
707
+
708
+ branch: main
709
+ worktree: main
710
+ created: 2026-07-13
711
+ status: executing
712
+ type: deliver
713
+
714
+ ## Plan
715
+
716
+ Actor A's own session, deliberately left with its own genuine evidence gap (no trust.bundle) --
717
+ own-work gating must remain unaffected by the #440 fix (anti-gaming guarantee).
718
+ MARKDOWN
719
+
720
+ AC440_A_BLOCKED_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-440-a FLOW_AGENTS_GOAL_FIT_MODE=block node "$GOAL_FIT_GATE" 2>"$TMPDIR_EVAL/ac440-a-blocked.err" <<JSON
721
+ {"hook_event_name":"Stop","cwd":"$AC440_REPO"}
722
+ JSON
723
+ )"
724
+ AC440_A_BLOCKED_STATUS=$?
725
+
726
+ if [[ "$AC440_A_BLOCKED_STATUS" -ne 0 ]]; then
727
+ pass "AC2 (anti-gaming): actor A's Stop hook STILL blocks on A's own evidence gap once A has its own per-actor pointer"
728
+ else
729
+ fail "AC2 (anti-gaming): actor A's Stop hook unexpectedly did NOT block on A's own evidence gap: $AC440_A_BLOCKED_OUT $(cat "$TMPDIR_EVAL/ac440-a-blocked.err")"
730
+ fi
731
+
732
+ AC440_STEERING_A_OWN_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-440-a node "$STEERING" <<JSON
733
+ {"hook_event_name":"SessionStart","cwd":"$AC440_REPO"}
734
+ JSON
735
+ )"
736
+
737
+ if [[ "$AC440_STEERING_A_OWN_OUT" == *"RESUME: $AC440_A_SLUG"* ]]; then
738
+ pass "AC1 (positive control): workflow-steering.js's SessionStart banner shows RESUME: <A's own slug> once A has its own per-actor pointer"
739
+ else
740
+ fail "AC1 (positive control): workflow-steering.js's SessionStart banner did not show RESUME: $AC440_A_SLUG: $AC440_STEERING_A_OWN_OUT"
741
+ fi
742
+
743
+ AC440_A_LOG="$AC440_A_DIR/command-log.jsonl"
744
+ AC440_EVIDENCE_EVENT_OWN="{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"cwd\":\"$AC440_REPO\",\"tool_input\":{\"command\":\"echo actor-a-own-capture-check\"},\"tool_response\":{\"exitCode\":0,\"stdout\":\"ok\"}}"
745
+ printf '%s' "$AC440_EVIDENCE_EVENT_OWN" \
746
+ | FLOW_AGENTS_ACTOR=eval-actor-440-a node "$EVIDENCE_CAPTURE" >"$TMPDIR_EVAL/ac440-evidence-a-own.out" 2>"$TMPDIR_EVAL/ac440-evidence-a-own.err"
747
+
748
+ if [[ -f "$AC440_A_LOG" ]] && grep -qF "actor-a-own-capture-check" "$AC440_A_LOG"; then
749
+ pass "evidence-capture.js (positive control): actor A's own capture still works once A has its own per-actor pointer"
750
+ else
751
+ fail "evidence-capture.js (positive control): actor A's own capture did not work: $(cat "$TMPDIR_EVAL/ac440-evidence-a-own.out" "$TMPDIR_EVAL/ac440-evidence-a-own.err")"
752
+ fi
753
+
754
+ # 7.6 FIX 1 (HIGH, collision -- independent review): two distinct resolved actor keys sharing a
755
+ # >=64-char common sanitized prefix (differing only in a tail the pre-#440-fix-wave-2 64-char
756
+ # truncation would have discarded) must map to DISTINCT per-actor pointer files, and each actor's
757
+ # OWN read must resolve its OWN pointer -- never the other's -- proving neither session grounds
758
+ # onto the other. Reproduces the exact collision the review found:
759
+ # perActorCurrentFile(dir, A) === perActorCurrentFile(dir, B) under the pre-fix mapping.
760
+ AC440_COLLIDE_DIR="$TMPDIR_EVAL/collide-root/.kontourai/flow-agents"
761
+ if CP_HELPER_ARG="$CURRENT_POINTER_HELPER" DIR_ARG="$AC440_COLLIDE_DIR" node - <<'NODE' 2>"$TMPDIR_EVAL/ac440-collide.err"
762
+ const { perActorCurrentFile, writePerActorCurrent, readOwnCurrentPointer } = require(process.env.CP_HELPER_ARG);
763
+ const dir = process.env.DIR_ARG;
764
+ const prefix = 'claude-code:' + 's'.repeat(52);
765
+ const keyA = prefix + ':host-a';
766
+ const keyB = prefix + ':host-b';
767
+
768
+ const fileA = perActorCurrentFile(dir, keyA);
769
+ const fileB = perActorCurrentFile(dir, keyB);
770
+ if (fileA === fileB) {
771
+ console.error(`FILES_COLLIDE: ${fileA}`);
772
+ process.exit(1);
773
+ }
774
+
775
+ writePerActorCurrent(dir, keyA, { active_slug: 'collide-a-slug' });
776
+ writePerActorCurrent(dir, keyB, { active_slug: 'collide-b-slug' });
777
+
778
+ const resultA = readOwnCurrentPointer(dir, keyA);
779
+ const resultB = readOwnCurrentPointer(dir, keyB);
780
+ if (!resultA.payload || resultA.payload.active_slug !== 'collide-a-slug') {
781
+ console.error(`ACTOR_A_WRONG_SLUG: ${JSON.stringify(resultA)}`);
782
+ process.exit(1);
783
+ }
784
+ if (!resultB.payload || resultB.payload.active_slug !== 'collide-b-slug') {
785
+ console.error(`ACTOR_B_WRONG_SLUG: ${JSON.stringify(resultB)}`);
786
+ process.exit(1);
787
+ }
788
+ NODE
789
+ then
790
+ pass "FIX 1: two actor keys sharing a >=64-char common sanitized prefix map to DISTINCT per-actor pointer files, and each actor's own read resolves its own session, never the other's (collision-resistant naming)"
791
+ else
792
+ fail "FIX 1: colliding actor keys did not map to distinct files / grounded onto each other's session: $(cat "$TMPDIR_EVAL/ac440-collide.err")"
793
+ fi
794
+
795
+ # 7.7 FIX 1 (legacy-name fallback -- independent review): a pointer written under the PRE-fix-wave-2
796
+ # filename scheme (legacyPerActorCurrentFile -- sanitizeSegment(actorKey) alone, no hash) with NO
797
+ # new-scheme file present must still resolve for its owning actor -- the transition-window compat
798
+ # guarantee (a still-running published-3.9.0-era session's pointer keeps resolving).
799
+ if CP_HELPER_ARG="$CURRENT_POINTER_HELPER" DIR_ARG="$AC440_COLLIDE_DIR" node - <<'NODE' 2>"$TMPDIR_EVAL/ac440-legacy-fallback.err"
800
+ const fs = require('fs');
801
+ const path = require('path');
802
+ const { legacyPerActorCurrentFile, perActorCurrentFile, readOwnCurrentPointer } = require(process.env.CP_HELPER_ARG);
803
+ const dir = process.env.DIR_ARG;
804
+ const actorKey = 'eval-actor-440-legacy-fallback';
805
+
806
+ const newFile = perActorCurrentFile(dir, actorKey);
807
+ if (fs.existsSync(newFile)) { console.error(`NEW_FILE_UNEXPECTEDLY_EXISTS: ${newFile}`); process.exit(1); }
808
+
809
+ const legacyFile = legacyPerActorCurrentFile(dir, actorKey);
810
+ fs.mkdirSync(path.dirname(legacyFile), { recursive: true });
811
+ fs.writeFileSync(legacyFile, JSON.stringify({ active_slug: 'legacy-fallback-slug' }));
812
+
813
+ const result = readOwnCurrentPointer(dir, actorKey);
814
+ if (!result.payload || result.payload.active_slug !== 'legacy-fallback-slug') {
815
+ console.error(`FALLBACK_DID_NOT_RESOLVE: ${JSON.stringify(result)}`);
816
+ process.exit(1);
817
+ }
818
+ if (result.file !== legacyFile) {
819
+ console.error(`FALLBACK_FILE_MISMATCH: got ${result.file} expected ${legacyFile}`);
820
+ process.exit(1);
821
+ }
822
+ NODE
823
+ then
824
+ pass "FIX 1: a pointer written under the pre-fix-wave-2 legacy filename (no new-scheme file present) still resolves for its owning actor (transition-window fallback)"
825
+ else
826
+ fail "FIX 1: legacy-filename fallback did not resolve as expected: $(cat "$TMPDIR_EVAL/ac440-legacy-fallback.err")"
827
+ fi
828
+
829
+ # 7.8 FINDING 1 (MED, independent review delta): record-agent-event's updateCurrentAgent must
830
+ # migrate a LEGACY-only per-actor pointer (as a still-running published-3.9.0-era sidecar would
831
+ # have written, pre-fix-wave-2) to the new collision-resistant filename on first touch, applying
832
+ # the active_agents/updated_at projection -- not silently skip that projection because it only
833
+ # read the new filename directly (fs.existsSync/readFileSync on perActorCurrentFile() alone,
834
+ # never falling back to legacyPerActorCurrentFile()).
835
+ AC440_MIGRATE_ROOT="$TMPDIR_EVAL/ac440-migrate-project/.kontourai/flow-agents"
836
+ AC440_MIGRATE_ACTOR="eval-actor-440-legacy-migrate"
837
+ AC440_MIGRATE_AGENT_ID="tool-worker"
838
+
839
+ flow_agents_node "workflow-sidecar" ensure-session \
840
+ --artifact-root "$AC440_MIGRATE_ROOT" \
841
+ --work-item "kontourai/flow-agents#94405" \
842
+ --actor "$AC440_MIGRATE_ACTOR" \
843
+ --source-request "Actor establishing a session whose pointer will be downgraded to legacy-only." \
844
+ --summary "Legacy-pointer migration regression session." \
845
+ >"$TMPDIR_EVAL/ac440-migrate-ensure.out" 2>"$TMPDIR_EVAL/ac440-migrate-ensure.err"
846
+ AC440_MIGRATE_SLUG="kontourai-flow-agents-94405"
847
+ AC440_MIGRATE_DIR="$AC440_MIGRATE_ROOT/$AC440_MIGRATE_SLUG"
848
+
849
+ # Downgrade the per-actor pointer to LEGACY-ONLY: capture the payload ensure-session wrote under
850
+ # the NEW filename, delete the new file, and re-write the SAME payload under the pre-fix-wave-2
851
+ # legacy filename -- reproducing exactly what a pointer a still-running published-3.9.0 sidecar
852
+ # (pre-fix-wave-2) would have on disk.
853
+ if CP_HELPER_ARG="$CURRENT_POINTER_HELPER" DIR_ARG="$AC440_MIGRATE_ROOT" ACTOR_ARG="$AC440_MIGRATE_ACTOR" node - <<'NODE' 2>"$TMPDIR_EVAL/ac440-migrate-downgrade.err"
854
+ const fs = require('fs');
855
+ const { perActorCurrentFile, legacyPerActorCurrentFile } = require(process.env.CP_HELPER_ARG);
856
+ const dir = process.env.DIR_ARG;
857
+ const actorKey = process.env.ACTOR_ARG;
858
+ const newFile = perActorCurrentFile(dir, actorKey);
859
+ if (!fs.existsSync(newFile)) { console.error(`NEW_FILE_MISSING: ${newFile}`); process.exit(1); }
860
+ const payload = fs.readFileSync(newFile, 'utf8');
861
+ fs.unlinkSync(newFile);
862
+ const legacyFile = legacyPerActorCurrentFile(dir, actorKey);
863
+ fs.writeFileSync(legacyFile, payload);
864
+ NODE
865
+ then
866
+ pass "setup: actor's per-actor pointer downgraded to legacy-only (new-name file removed, same payload re-written under the pre-fix-wave-2 legacy filename)"
867
+ else
868
+ fail "setup: failed to downgrade the per-actor pointer to legacy-only: $(cat "$TMPDIR_EVAL/ac440-migrate-downgrade.err")"
869
+ fi
870
+
871
+ flow_agents_node "workflow-sidecar" record-agent-event \
872
+ --artifact-root "$AC440_MIGRATE_ROOT" \
873
+ --actor "$AC440_MIGRATE_ACTOR" \
874
+ --agent-id "$AC440_MIGRATE_AGENT_ID" \
875
+ --kind note \
876
+ --status active \
877
+ --summary "legacy-pointer migration regression probe" \
878
+ >"$TMPDIR_EVAL/ac440-migrate-record.out" 2>"$TMPDIR_EVAL/ac440-migrate-record.err"
879
+ AC440_MIGRATE_RECORD_STATUS=$?
880
+
881
+ # (source-tree legacy-ref scan note: the agent-events relative path below is built through a
882
+ # variable, not a literal contiguous "agents/" + agent-id token, matching
883
+ # test_model_routing_escalation.sh's own path.join(sdir, "agents", agent, "events.jsonl")
884
+ # convention -- a literal contiguous "agents/<agent-id>/..."-shaped string in an eval file is
885
+ # flagged by validate-source-tree.ts's legacy-ref scanner as a possible stale repo-path
886
+ # reference.)
887
+ if [[ "$AC440_MIGRATE_RECORD_STATUS" -eq 0 ]] && [[ -f "$AC440_MIGRATE_DIR/agents/$AC440_MIGRATE_AGENT_ID/events.jsonl" ]]; then
888
+ pass "FINDING 1: record-agent-event succeeds and records the agent event against a legacy-only per-actor pointer"
889
+ else
890
+ fail "FINDING 1: record-agent-event did not succeed/record against a legacy-only pointer: status=$AC440_MIGRATE_RECORD_STATUS $(cat "$TMPDIR_EVAL/ac440-migrate-record.out" "$TMPDIR_EVAL/ac440-migrate-record.err")"
891
+ fi
892
+
893
+ # The CORE regression: updateCurrentAgent must have migrated the pointer to the NEW filename with
894
+ # the active_agents projection applied -- not silently skipped it because it only checked the new
895
+ # (at that point, absent) filename directly.
896
+ if CP_HELPER_ARG="$CURRENT_POINTER_HELPER" DIR_ARG="$AC440_MIGRATE_ROOT" ACTOR_ARG="$AC440_MIGRATE_ACTOR" node - <<'NODE' 2>"$TMPDIR_EVAL/ac440-migrate-verify.err"
897
+ const fs = require('fs');
898
+ const { perActorCurrentFile } = require(process.env.CP_HELPER_ARG);
899
+ const dir = process.env.DIR_ARG;
900
+ const actorKey = process.env.ACTOR_ARG;
901
+ const newFile = perActorCurrentFile(dir, actorKey);
902
+ if (!fs.existsSync(newFile)) { console.error(`NEW_FILE_STILL_MISSING_AFTER_RECORD_AGENT_EVENT: ${newFile}`); process.exit(1); }
903
+ const payload = JSON.parse(fs.readFileSync(newFile, 'utf8'));
904
+ const active = Array.isArray(payload.active_agents) ? payload.active_agents : [];
905
+ const entry = active.find((a) => a && a.agent_id === 'tool-worker');
906
+ if (!entry || entry.status !== 'active') {
907
+ console.error(`ACTIVE_AGENTS_PROJECTION_MISSING: ${JSON.stringify(payload)}`);
908
+ process.exit(1);
909
+ }
910
+ NODE
911
+ then
912
+ pass "FINDING 1: record-agent-event migrates a legacy-only per-actor pointer to the new collision-resistant filename on first touch, with the active_agents projection correctly applied (not silently skipped)"
913
+ else
914
+ fail "FINDING 1: record-agent-event did not migrate/update the per-actor pointer as expected: $(cat "$TMPDIR_EVAL/ac440-migrate-verify.err")"
915
+ fi
916
+
917
+
523
918
  echo ""
524
919
  if [[ "$errors" -eq 0 ]]; then
525
920
  echo "test_current_json_per_actor: all checks passed."
@@ -19,6 +19,17 @@
19
19
  set -uo pipefail
20
20
 
21
21
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22
+
23
+ # #440 FIXTURE-GAP: this suite's fixtures were written before #440's per-actor ownership scoping
24
+ # and never establish a per-actor current pointer for the invoking actor -- under a RESOLVED
25
+ # ambient actor (ancestry-derived locally, GITHUB_RUN_ID-derived CI-runtime in CI), stop-goal-fit.js's
26
+ # analyze() now scopes to that actor's own (nonexistent) pointer and never reaches the
27
+ # fixture-under-test at all. This suite is about anti-gaming/gate mechanics, not #440's ownership
28
+ # scoping, so forcing the documented test-only unresolved-actor escape hatch restores EXACTLY this
29
+ # suite's pre-#440 behavior (D3 compat: an unresolved actor keeps the unchanged legacy-fallback/
30
+ # global-scan discovery every assertion below was written against).
31
+ export FLOW_AGENTS_ACTOR_TEST_FORCE_UNRESOLVED=1
32
+ export NODE_ENV=test
22
33
  GATE="$ROOT/scripts/hooks/stop-goal-fit.js"
23
34
 
24
35
  export FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000
@@ -427,6 +427,45 @@ fi
427
427
  [[ "$(json_query "$TMPDIR_EVAL/status-race.json" "assignment.assignee")" == *"$EXPECTED_RACE_HOLDER"* ]] && pass "on-disk record holder matches the process that actually exited 0 (no silent overwrite by the loser) (AC6)" || fail "on-disk record holder does not match the winning process: $(cat "$TMPDIR_EVAL/status-race.json")"
428
428
 
429
429
  echo ""
430
+
431
+ # 7. #554: ensure-session consumes the helper-owned safe struct and re-enters as self.
432
+ CODEX_PRIVATE_ROOT="$TMPDIR_EVAL/codex-private-root"
433
+ CODEX_PRIVATE_RAW='ENSURE-PRIVATE-SENTINEL:thread/value'
434
+ CODEX_PRIVATE_WORK="kontourai/flow-agents#9554"
435
+ CODEX_PRIVATE_SLUG="kontourai-flow-agents-9554"
436
+ if CODEX_THREAD_ID="$CODEX_PRIVATE_RAW" CODEX_SESSION_ID= flow_agents_node "workflow-sidecar" ensure-session \
437
+ --artifact-root "$CODEX_PRIVATE_ROOT" --work-item "$CODEX_PRIVATE_WORK" \
438
+ --source-request "Canonical Codex identity fixture." --summary "Privacy-safe actor fixture." \
439
+ >"$TMPDIR_EVAL/codex-private-first.out" 2>"$TMPDIR_EVAL/codex-private-first.err"; then
440
+ pass "ensure-session succeeds with CODEX_THREAD_ID"
441
+ else
442
+ fail "ensure-session failed with CODEX_THREAD_ID: $(cat "$TMPDIR_EVAL/codex-private-first.out" "$TMPDIR_EVAL/codex-private-first.err")"
443
+ fi
444
+ CODEX_PRIVATE_RECORD="$CODEX_PRIVATE_ROOT/assignment/$CODEX_PRIVATE_SLUG.json"
445
+ if node - "$CODEX_PRIVATE_RECORD" "$ROOT/scripts/hooks/lib/actor-identity.js" <<'NODE'
446
+ const fs = require('fs');
447
+ const record = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
448
+ const { serializeActor } = require(process.argv[3]);
449
+ if (record.actor.runtime !== 'codex' || !/^thread-[a-f0-9]{24}$/.test(record.actor.session_id)) throw new Error('unsafe actor struct');
450
+ if (record.actor_key !== serializeActor(record.actor)) throw new Error('key/struct mismatch');
451
+ NODE
452
+ then pass "ensure-session persists a canonical Codex actor struct whose serialization equals actor_key"
453
+ else fail "ensure-session persisted a divergent or unsafe Codex actor struct/key"
454
+ fi
455
+ if rg -qF 'ENSURE-PRIVATE-SENTINEL' "$CODEX_PRIVATE_ROOT" "$TMPDIR_EVAL/codex-private-first.out" "$TMPDIR_EVAL/codex-private-first.err"; then
456
+ fail "raw CODEX_THREAD_ID sentinel leaked into ensure-session artifacts or output"
457
+ else
458
+ pass "raw CODEX_THREAD_ID sentinel is absent from the complete ensure-session artifact/output set"
459
+ fi
460
+ if CODEX_THREAD_ID="$CODEX_PRIVATE_RAW" CODEX_SESSION_ID= flow_agents_node "workflow-sidecar" ensure-session \
461
+ --artifact-root "$CODEX_PRIVATE_ROOT" --work-item "$CODEX_PRIVATE_WORK" \
462
+ --source-request "Canonical Codex identity fixture reentry." --summary "Privacy-safe actor fixture reentry." \
463
+ >"$TMPDIR_EVAL/codex-private-second.out" 2>"$TMPDIR_EVAL/codex-private-second.err"; then
464
+ pass "second ensure-session recognizes the first canonical Codex claim as self"
465
+ else
466
+ fail "second ensure-session did not recognize the canonical Codex claim as self: $(cat "$TMPDIR_EVAL/codex-private-second.out" "$TMPDIR_EVAL/codex-private-second.err")"
467
+ fi
468
+
430
469
  if [[ "$errors" -eq 0 ]]; then
431
470
  echo "test_ensure_session_ownership_guard: all checks passed."
432
471
  else