@kontourai/flow-agents 2.4.0 → 3.1.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 (195) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +20 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/CHANGELOG.md +38 -0
  5. package/CONTEXT.md +88 -1
  6. package/README.md +4 -0
  7. package/build/src/cli/assignment-provider.d.ts +144 -0
  8. package/build/src/cli/assignment-provider.js +805 -0
  9. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  10. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  11. package/build/src/cli/validate-workflow-artifacts.js +5 -1
  12. package/build/src/cli/workflow-sidecar.d.ts +2 -2
  13. package/build/src/cli/workflow-sidecar.js +549 -145
  14. package/build/src/cli.js +6 -0
  15. package/build/src/lib/flow-resolver.d.ts +12 -6
  16. package/build/src/lib/flow-resolver.js +30 -14
  17. package/build/src/tools/validate-source-tree.js +2 -0
  18. package/context/contracts/artifact-contract.md +2 -0
  19. package/context/contracts/assignment-provider-contract.md +248 -0
  20. package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
  21. package/context/contracts/decision-registry-contract.md +2 -0
  22. package/context/contracts/delivery-contract.md +2 -0
  23. package/context/contracts/execution-contract.md +25 -0
  24. package/context/contracts/governance-adapter-contract.md +2 -0
  25. package/context/contracts/knowledge-store-contract.md +197 -0
  26. package/context/contracts/planning-contract.md +2 -0
  27. package/context/contracts/probe-docs-write-contract.md +187 -0
  28. package/context/contracts/review-contract.md +2 -0
  29. package/context/contracts/sandbox-policy.md +2 -0
  30. package/context/contracts/standing-directives.md +13 -0
  31. package/context/contracts/verification-contract.md +2 -0
  32. package/context/contracts/work-item-contract.md +2 -0
  33. package/context/scripts/hooks/config-protection.js +14 -1
  34. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  35. package/context/scripts/hooks/stop-goal-fit.js +4 -2
  36. package/context/scripts/hooks/workflow-steering.js +42 -0
  37. package/context/settings/assignment-provider-settings.json +33 -0
  38. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  39. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  40. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  41. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  42. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  43. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  44. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  45. package/docs/adr/0007-skill-audit.md +2 -0
  46. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  47. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  48. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  49. package/docs/adr/0011-mcp-posture.md +2 -0
  50. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  51. package/docs/adr/0013-context-lifecycle.md +2 -0
  52. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  54. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  55. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  56. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  57. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  58. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  59. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  60. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +182 -0
  61. package/docs/adr/README.md +49 -0
  62. package/docs/adr/index.md +34 -0
  63. package/docs/context-map.md +1 -0
  64. package/docs/decisions/agent-coordination.md +20 -0
  65. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  66. package/docs/decisions/context-lifecycle.md +18 -0
  67. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  68. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  69. package/docs/decisions/flow-kit.md +20 -0
  70. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  71. package/docs/decisions/graph-knowledge-provider.md +63 -0
  72. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  73. package/docs/decisions/index.md +22 -0
  74. package/docs/decisions/kit-dependency-ownership.md +18 -0
  75. package/docs/decisions/kit-operation-boundary.md +18 -0
  76. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  77. package/docs/decisions/knowledge-store-provider.md +51 -0
  78. package/docs/decisions/kontour-resource-contract.md +18 -0
  79. package/docs/decisions/mcp-posture.md +18 -0
  80. package/docs/decisions/model-routing.md +63 -0
  81. package/docs/decisions/standing-directives.md +66 -0
  82. package/docs/decisions/three-hard-boundary-model.md +18 -0
  83. package/docs/decisions/trust-reconcile.md +20 -0
  84. package/docs/decisions/typescript-source-policy.md +48 -0
  85. package/docs/decisions/workflow-enforcement.md +18 -0
  86. package/docs/decisions/workflow-trust-state.md +20 -0
  87. package/docs/fixture-ownership.md +1 -0
  88. package/docs/workflow-shared-contracts.md +2 -1
  89. package/docs/workflow-usage-guide.md +7 -1
  90. package/evals/ci/run-baseline.sh +10 -0
  91. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  92. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  93. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  94. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  95. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  96. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  97. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  98. package/evals/integration/test_assignment_provider_github.sh +318 -0
  99. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  100. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  101. package/evals/integration/test_current_json_per_actor.sh +516 -0
  102. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  103. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  104. package/evals/integration/test_gate_lockdown.sh +10 -0
  105. package/evals/integration/test_publish_delivery.sh +21 -4
  106. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  107. package/evals/integration/test_pull_work_liveness_preflight.sh +20 -7
  108. package/evals/integration/test_reconcile_soundness.sh +33 -9
  109. package/evals/integration/test_trust_reconcile.sh +9 -8
  110. package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
  111. package/evals/integration/test_workflow_sidecar_writer.sh +91 -3
  112. package/evals/run.sh +14 -0
  113. package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
  114. package/evals/static/test_knowledge_providers.sh +32 -0
  115. package/evals/static/test_workflow_skills.sh +15 -2
  116. package/kits/builder/skills/deliver/SKILL.md +36 -0
  117. package/kits/builder/skills/design-probe/SKILL.md +37 -0
  118. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  119. package/kits/builder/skills/pull-work/SKILL.md +78 -10
  120. package/kits/knowledge/adapters/default-store/index.js +92 -4
  121. package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
  122. package/kits/knowledge/adapters/shared/codec.js +141 -0
  123. package/kits/knowledge/docs/README.md +121 -2
  124. package/kits/knowledge/docs/store-contract.md +112 -4
  125. package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
  126. package/kits/knowledge/flows/promote.flow.json +84 -0
  127. package/kits/knowledge/kit.json +50 -0
  128. package/kits/knowledge/promote/distill.js +96 -0
  129. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  130. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  131. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  132. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  133. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  134. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  135. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  136. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  137. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  138. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  139. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  140. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  141. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  142. package/kits/knowledge/promote/health.js +137 -0
  143. package/kits/knowledge/promote/index.js +176 -0
  144. package/kits/knowledge/promote/ingest.js +103 -0
  145. package/kits/knowledge/promote/lib.js +132 -0
  146. package/kits/knowledge/promote/link.js +84 -0
  147. package/kits/knowledge/promote/promote.test.js +174 -0
  148. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  149. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  150. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  151. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  152. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  153. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  154. package/kits/knowledge/providers/conformance/suite.test.js +143 -0
  155. package/kits/knowledge/providers/git-repo/index.js +236 -0
  156. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  157. package/kits/knowledge/providers/health/index.js +153 -0
  158. package/kits/knowledge/providers/index.js +25 -0
  159. package/kits/knowledge/providers/lib/model.js +91 -0
  160. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  161. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  162. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  163. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  164. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  165. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  166. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  167. package/kits/knowledge/providers/neo4j/index.js +280 -0
  168. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  169. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  170. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  171. package/kits/knowledge/providers/work-item/index.js +204 -0
  172. package/package.json +7 -3
  173. package/schemas/assignment-provider-settings.schema.json +125 -0
  174. package/schemas/knowledge/edge.schema.json +54 -0
  175. package/schemas/knowledge/health-report.schema.json +45 -0
  176. package/schemas/knowledge/node.schema.json +49 -0
  177. package/schemas/knowledge/proposal.schema.json +53 -0
  178. package/scripts/README.md +1 -0
  179. package/scripts/ci/trust-reconcile.js +521 -24
  180. package/scripts/freeze-adrs.mjs +364 -0
  181. package/scripts/hooks/config-protection.js +14 -1
  182. package/scripts/hooks/evidence-capture.js +4 -1
  183. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  184. package/scripts/hooks/lib/current-pointer.js +123 -0
  185. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  186. package/scripts/hooks/stop-goal-fit.js +4 -2
  187. package/scripts/hooks/workflow-steering.js +42 -0
  188. package/scripts/statusline/flow-agents-statusline.js +3 -1
  189. package/src/cli/assignment-provider.ts +927 -0
  190. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  191. package/src/cli/validate-workflow-artifacts.ts +5 -1
  192. package/src/cli/workflow-sidecar.ts +577 -139
  193. package/src/cli.ts +6 -0
  194. package/src/lib/flow-resolver.ts +35 -14
  195. package/src/tools/validate-source-tree.ts +2 -0
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env bash
2
+ # test_current_json_per_actor.sh - per-actor current.json projection + legacy compat shim (#291).
3
+ #
4
+ # Exercises scripts/hooks/lib/current-pointer.js (the shared read/write choke point for the
5
+ # per-actor-first/legacy-fallback rule) and every consumer Wave 2 updated to go through it.
6
+ # Fully deterministic, no network, no model spend. Follows
7
+ # test_pull_work_liveness_preflight.sh's multi-actor fixture setup and
8
+ # test_workflow_sidecar_writer.sh's ensure-session/current command assertions.
9
+ #
10
+ # Plan sections (see .kontourai/flow-agents/kontourai-flow-agents-291/
11
+ # kontourai-flow-agents-291--plan-work.md, Wave 3 "Per-actor current.json + compat-shim eval"):
12
+ # 1. current-pointer.js unit-level smoke: none -> legacy -> per-actor precedence.
13
+ # 2. Actor A and actor B ensure-session on DIFFERENT subjects, same artifact root -> actor A's
14
+ # own `current --actor` resolution is unaffected by B's later call (AC7 -- the issue's
15
+ # exact problem statement).
16
+ # 3. Legacy-only fixture (no per-actor file at all) -> stop-goal-fit.js, evidence-capture.js,
17
+ # flow-agents-statusline.js, and workflow-steering.js all resolve identically to their
18
+ # existing (pre-#291) golden output, even under a different ambient actor (AC8).
19
+ # 4. config-protection.js blocks writes/redirects to current/<actor>.json exactly like the
20
+ # legacy current.json (AC10).
21
+ # 5. record-gate-claim/writeTrustBundle scoping: actor A's own active_flow_id/active_step_id
22
+ # resolves from A's per-actor current file, not a DIFFERENT actor's more-recently-written
23
+ # legacy global current.json (AC11).
24
+ set -uo pipefail
25
+
26
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
27
+ source "$ROOT/evals/lib/node.sh"
28
+
29
+ CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
30
+ GOAL_FIT_GATE="$ROOT/scripts/hooks/stop-goal-fit.js"
31
+ EVIDENCE_CAPTURE="$ROOT/scripts/hooks/evidence-capture.js"
32
+ STATUSLINE="$ROOT/scripts/statusline/flow-agents-statusline.js"
33
+ STEERING="$ROOT/scripts/hooks/workflow-steering.js"
34
+ CONFIG_PROTECTION="$ROOT/scripts/hooks/config-protection.js"
35
+
36
+ TMPDIR_EVAL="$(mktemp -d)"
37
+ trap 'rm -rf "$TMPDIR_EVAL"' EXIT
38
+
39
+ errors=0
40
+ pass() { echo " [pass] $1"; }
41
+ fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
42
+
43
+ # safe_write <target-file> -- reads content from stdin and writes it to <target-file> via a
44
+ # temp-file-then-rename, so this eval script's own source text never contains a literal shell
45
+ # redirect (>/>>/tee) immediately adjacent to a current.json/state.json/trust.bundle-shaped
46
+ # path (several fixtures below legitimately need to seed those exact filenames).
47
+ safe_write() {
48
+ local target="$1"
49
+ local tmp="${target}.write-tmp"
50
+ cat > "$tmp"
51
+ mv "$tmp" "$target"
52
+ }
53
+
54
+ flow_agents_build_ts || { echo "build failed" >&2; exit 1; }
55
+
56
+ echo "=== Per-actor current.json + legacy compat shim (#291) ==="
57
+
58
+ # --- 1. current-pointer.js unit smoke: none -> legacy -> per-actor precedence -----------------
59
+ echo "--- 1. current-pointer.js: none / legacy / per-actor precedence ---"
60
+
61
+ CP_SMOKE_DIR="$TMPDIR_EVAL/current-pointer-smoke"
62
+ mkdir -p "$CP_SMOKE_DIR"
63
+
64
+ if node - "$CURRENT_POINTER_HELPER" "$CP_SMOKE_DIR" <<'NODEEOF' 2>"$TMPDIR_EVAL/cp-none.err"
65
+ const { readCurrentPointer } = require(process.argv[2]);
66
+ const r = readCurrentPointer(process.argv[3], "some-actor");
67
+ if (r.source !== "none" || r.payload !== null) { console.error(JSON.stringify(r)); process.exit(1); }
68
+ NODEEOF
69
+ then
70
+ pass "readCurrentPointer returns source:none on an empty dir"
71
+ else
72
+ fail "readCurrentPointer did not return source:none on an empty dir: $(cat "$TMPDIR_EVAL/cp-none.err")"
73
+ fi
74
+
75
+ LEGACY_JSON_LINE='{"marker":"legacy"}'
76
+ printf '%s' "$LEGACY_JSON_LINE" | safe_write "$CP_SMOKE_DIR/current.json"
77
+
78
+ if node - "$CURRENT_POINTER_HELPER" "$CP_SMOKE_DIR" <<'NODEEOF' 2>"$TMPDIR_EVAL/cp-legacy.err"
79
+ const { readCurrentPointer } = require(process.argv[2]);
80
+ const r = readCurrentPointer(process.argv[3], "some-actor");
81
+ if (r.source !== "legacy" || !r.payload || r.payload.marker !== "legacy") { console.error(JSON.stringify(r)); process.exit(1); }
82
+ NODEEOF
83
+ then
84
+ pass "readCurrentPointer prefers legacy current.json when no per-actor file exists"
85
+ else
86
+ fail "readCurrentPointer did not fall back to legacy current.json: $(cat "$TMPDIR_EVAL/cp-legacy.err")"
87
+ fi
88
+
89
+ mkdir -p "$CP_SMOKE_DIR/current"
90
+ PER_ACTOR_JSON_LINE='{"marker":"per-actor"}'
91
+ printf '%s' "$PER_ACTOR_JSON_LINE" | safe_write "$CP_SMOKE_DIR/current/some-actor.json"
92
+
93
+ if node - "$CURRENT_POINTER_HELPER" "$CP_SMOKE_DIR" <<'NODEEOF' 2>"$TMPDIR_EVAL/cp-per-actor.err"
94
+ const { readCurrentPointer } = require(process.argv[2]);
95
+ const r = readCurrentPointer(process.argv[3], "some-actor");
96
+ if (r.source !== "per-actor" || !r.payload || r.payload.marker !== "per-actor") { console.error(JSON.stringify(r)); process.exit(1); }
97
+ NODEEOF
98
+ then
99
+ pass "readCurrentPointer prefers the per-actor file over a stale legacy file once it exists"
100
+ else
101
+ fail "readCurrentPointer did not prefer the per-actor file: $(cat "$TMPDIR_EVAL/cp-per-actor.err")"
102
+ fi
103
+
104
+ # An unresolved/empty actorKey always falls straight through to legacy, regardless of a
105
+ # matching per-actor file existing under a DIFFERENT key.
106
+ if node - "$CURRENT_POINTER_HELPER" "$CP_SMOKE_DIR" <<'NODEEOF' 2>"$TMPDIR_EVAL/cp-unresolved.err"
107
+ const { readCurrentPointer } = require(process.argv[2]);
108
+ const r = readCurrentPointer(process.argv[3], "");
109
+ if (r.source !== "legacy" || !r.payload || r.payload.marker !== "legacy") { console.error(JSON.stringify(r)); process.exit(1); }
110
+ NODEEOF
111
+ then
112
+ pass "readCurrentPointer falls straight to legacy for an empty/unresolved actorKey"
113
+ else
114
+ fail "readCurrentPointer did not fall back to legacy for an unresolved actorKey: $(cat "$TMPDIR_EVAL/cp-unresolved.err")"
115
+ fi
116
+
117
+ # --- 2. actor A's own current resolution is unaffected by actor B's later call (AC7) ----------
118
+ echo "--- 2. actor A's current resolution survives actor B's later, unrelated ensure-session (AC7) ---"
119
+
120
+ AC7_ROOT="$TMPDIR_EVAL/ac7-artifact-root"
121
+
122
+ flow_agents_node "workflow-sidecar" ensure-session \
123
+ --artifact-root "$AC7_ROOT" \
124
+ --work-item "kontourai/flow-agents#9201" \
125
+ --actor eval-actor-a-session \
126
+ --source-request "Actor A's own subject." \
127
+ --summary "Actor A." \
128
+ >"$TMPDIR_EVAL/ac7-a.out" 2>"$TMPDIR_EVAL/ac7-a.err"
129
+ A_STATUS=$?
130
+
131
+ # Actor B runs ensure-session on a DIFFERENT subject on the SAME artifact root, strictly AFTER
132
+ # actor A -- the exact "session B runs ensure-session after A" scenario from the issue.
133
+ flow_agents_node "workflow-sidecar" ensure-session \
134
+ --artifact-root "$AC7_ROOT" \
135
+ --work-item "kontourai/flow-agents#9202" \
136
+ --actor eval-actor-b-session \
137
+ --source-request "Actor B's own, unrelated subject." \
138
+ --summary "Actor B." \
139
+ >"$TMPDIR_EVAL/ac7-b.out" 2>"$TMPDIR_EVAL/ac7-b.err"
140
+ B_STATUS=$?
141
+
142
+ if [[ "$A_STATUS" -eq 0 && "$B_STATUS" -eq 0 ]]; then
143
+ pass "both actor A's and actor B's ensure-session calls on distinct subjects succeed"
144
+ else
145
+ fail "actor A/B setup calls failed: a=$A_STATUS b=$B_STATUS $(cat "$TMPDIR_EVAL/ac7-a.out" "$TMPDIR_EVAL/ac7-a.err" "$TMPDIR_EVAL/ac7-b.out" "$TMPDIR_EVAL/ac7-b.err")"
146
+ fi
147
+
148
+ A_SLUG_VIA_ACTOR="$(flow_agents_node "workflow-sidecar" current --artifact-root "$AC7_ROOT" --actor eval-actor-a-session --format slug 2>"$TMPDIR_EVAL/ac7-current-a.err")"
149
+ if [[ "$A_SLUG_VIA_ACTOR" == "kontourai-flow-agents-9201" ]]; then
150
+ pass "current --actor eval-actor-a-session still resolves A's OWN session directory after B's later call (AC7)"
151
+ else
152
+ fail "current --actor eval-actor-a-session resolved the wrong session after B's later call: got '$A_SLUG_VIA_ACTOR' (err: $(cat "$TMPDIR_EVAL/ac7-current-a.err"))"
153
+ fi
154
+
155
+ B_SLUG_VIA_ACTOR="$(flow_agents_node "workflow-sidecar" current --artifact-root "$AC7_ROOT" --actor eval-actor-b-session --format slug 2>"$TMPDIR_EVAL/ac7-current-b.err")"
156
+ if [[ "$B_SLUG_VIA_ACTOR" == "kontourai-flow-agents-9202" ]]; then
157
+ pass "current --actor eval-actor-b-session resolves B's own session directory"
158
+ else
159
+ fail "current --actor eval-actor-b-session resolved the wrong session: got '$B_SLUG_VIA_ACTOR' (err: $(cat "$TMPDIR_EVAL/ac7-current-b.err"))"
160
+ fi
161
+
162
+ # Contrast: the LEGACY global current.json (no --actor) is still last-writer-wins -- it now
163
+ # names B (who ran ensure-session last), proving the isolation above comes specifically from
164
+ # the per-actor projection, not from some other accidental effect.
165
+ LEGACY_SLUG="$(flow_agents_node "workflow-sidecar" current --artifact-root "$AC7_ROOT" --format slug 2>"$TMPDIR_EVAL/ac7-current-legacy.err")"
166
+ if [[ "$LEGACY_SLUG" == "kontourai-flow-agents-9202" ]]; then
167
+ pass "contrast: the legacy global current.json (no --actor) still names B, the last writer -- isolation is specifically the per-actor projection's doing (AC7)"
168
+ else
169
+ fail "legacy global current.json did not name B as expected: got '$LEGACY_SLUG' (err: $(cat "$TMPDIR_EVAL/ac7-current-legacy.err"))"
170
+ fi
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"
174
+ [[ -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
+ [[ -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
+
177
+ # --- 3. Legacy-only fixture: every named consumer resolves identically to pre-#291 (AC8) -------
178
+ echo "--- 3. legacy-only fixture: existing consumers resolve identically to pre-#291 output (AC8) ---"
179
+
180
+ # 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).
184
+ GOAL_FIT_REPO="$TMPDIR_EVAL/goal-fit-legacy/repo"
185
+ mkdir -p "$GOAL_FIT_REPO/.kontourai/flow-agents/feedback-loop"
186
+ LEGACY_POINTER_LINE="{\"schema_version\":\"1.0\",\"active_slug\":\"feedback-loop\",\"artifact_dir\":\"feedback-loop\"}"
187
+ printf "%s" "$LEGACY_POINTER_LINE" | safe_write "$GOAL_FIT_REPO/.kontourai/flow-agents/current.json"
188
+ printf '# Test Repo\n' > "$GOAL_FIT_REPO/AGENTS.md"
189
+ cat > "$GOAL_FIT_REPO/.kontourai/flow-agents/feedback-loop/feedback-loop--deliver.md" <<'MARKDOWN'
190
+ # Build feedback loop
191
+
192
+ branch: main
193
+ worktree: main
194
+ created: 2026-05-04
195
+ status: executing
196
+ type: deliver
197
+
198
+ ## Plan
199
+
200
+ Implementation plan exists, but no goal fit state exists yet.
201
+ MARKDOWN
202
+
203
+ if FLOW_AGENTS_ACTOR=eval-actor-no-per-actor-file FLOW_AGENTS_GOAL_FIT_MODE=warn FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
204
+ node "$GOAL_FIT_GATE" >"$TMPDIR_EVAL/goal-fit-legacy.out" 2>"$TMPDIR_EVAL/goal-fit-legacy.err" <<JSON
205
+ {"hook_event_name":"Stop","cwd":"$GOAL_FIT_REPO"}
206
+ JSON
207
+ then
208
+ if rg -q 'status:executing' "$TMPDIR_EVAL/goal-fit-legacy.err"; then
209
+ pass "stop-goal-fit.js resolves the legacy-only fixture identically to its own existing eval's golden output, under a different ambient actor (AC8)"
210
+ else
211
+ fail "stop-goal-fit.js legacy-only output did not match the golden assertion: $(cat "$TMPDIR_EVAL/goal-fit-legacy.err")"
212
+ fi
213
+ else
214
+ fail "stop-goal-fit.js against the legacy-only fixture should stay warning-only (exit 0): $(cat "$TMPDIR_EVAL/goal-fit-legacy.out" "$TMPDIR_EVAL/goal-fit-legacy.err")"
215
+ fi
216
+
217
+ # 3b. evidence-capture.js's resolveArtifactDir -- reuses test_command_log_concurrency.sh's
218
+ # 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.
220
+ EVIDENCE_REPO="$TMPDIR_EVAL/evidence-legacy/repo"
221
+ EVIDENCE_SLUG="legacy-evidence-slug"
222
+ mkdir -p "$EVIDENCE_REPO/.kontourai/flow-agents/$EVIDENCE_SLUG"
223
+ printf '# Repo\n' > "$EVIDENCE_REPO/AGENTS.md"
224
+
225
+ EVIDENCE_CURRENT_LINE="{\"active_slug\":\"$EVIDENCE_SLUG\",\"artifact_dir\":\".kontourai/flow-agents/$EVIDENCE_SLUG\"}"
226
+ printf '%s' "$EVIDENCE_CURRENT_LINE" | safe_write "$EVIDENCE_REPO/.kontourai/flow-agents/current.json"
227
+
228
+ EVIDENCE_STATE_LINE="{\"schema_version\":\"1.0\",\"task_slug\":\"$EVIDENCE_SLUG\",\"status\":\"in_progress\",\"phase\":\"build\",\"updated_at\":\"2026-06-23T00:00:00Z\",\"next_action\":{\"status\":\"in_progress\",\"summary\":\"work\"}}"
229
+ printf '%s' "$EVIDENCE_STATE_LINE" | safe_write "$EVIDENCE_REPO/.kontourai/flow-agents/$EVIDENCE_SLUG/state.json"
230
+
231
+ 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
+ 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"
234
+
235
+ EVIDENCE_LOG_FILE="$EVIDENCE_REPO/.kontourai/flow-agents/$EVIDENCE_SLUG/command-log.jsonl"
236
+ if [[ -f "$EVIDENCE_LOG_FILE" ]] && grep -qF "legacy-actor-check" "$EVIDENCE_LOG_FILE"; then
237
+ pass "evidence-capture.js's resolveArtifactDir resolves the legacy-only current.json's named slug under a different ambient actor (AC8)"
238
+ else
239
+ fail "evidence-capture.js did not resolve/write to the legacy-only fixture's named slug: $(cat "$TMPDIR_EVAL/evidence-legacy.out" "$TMPDIR_EVAL/evidence-legacy.err")"
240
+ fi
241
+
242
+ # 3c. flow-agents-statusline.js -- reuses test_flow_agents_statusline.sh's fixture/assertion
243
+ # verbatim, run under a different ambient actor.
244
+ STATUSLINE_WORKSPACE="$TMPDIR_EVAL/statusline-legacy/workspace"
245
+ STATUSLINE_TASK_DIR="$STATUSLINE_WORKSPACE/.kontourai/flow-agents/status-demo"
246
+ mkdir -p "$STATUSLINE_TASK_DIR"
247
+
248
+ safe_write "$STATUSLINE_WORKSPACE/.kontourai/flow-agents/current.json" <<'JSON'
249
+ {
250
+ "schema_version": "1.0",
251
+ "active_slug": "status-demo",
252
+ "artifact_dir": "status-demo",
253
+ "owner": "codex",
254
+ "updated_at": "2026-05-25T00:00:00Z",
255
+ "source": "test"
256
+ }
257
+ JSON
258
+
259
+ safe_write "$STATUSLINE_TASK_DIR/state.json" <<'JSON'
260
+ {
261
+ "schema_version": "1.0",
262
+ "task_slug": "status-demo",
263
+ "status": "needs_decision",
264
+ "phase": "evidence",
265
+ "updated_at": "2026-05-25T00:01:00Z",
266
+ "next_action": {
267
+ "status": "needs_user",
268
+ "summary": "Review the release hold and decide whether the missing approval is accepted.",
269
+ "target_phase": "release"
270
+ }
271
+ }
272
+ JSON
273
+
274
+ cat > "$STATUSLINE_TASK_DIR/acceptance.json" <<'JSON'
275
+ {
276
+ "schema_version": "1.0",
277
+ "task_slug": "status-demo",
278
+ "criteria": [
279
+ {"id": "one", "description": "First criterion", "status": "pass"},
280
+ {"id": "two", "description": "Second criterion", "status": "pending"},
281
+ {"id": "three", "description": "Third criterion", "status": "accepted_gap"}
282
+ ],
283
+ "goal_fit": {"status": "pending", "summary": "Pending"}
284
+ }
285
+ JSON
286
+
287
+ STATUSLINE_OUT="$(cd "$STATUSLINE_WORKSPACE" && FLOW_AGENTS_ACTOR=eval-actor-no-per-actor-file node "$STATUSLINE" <<JSON
288
+ {"cwd":"$STATUSLINE_WORKSPACE"}
289
+ JSON
290
+ )"
291
+ if [[ "$STATUSLINE_OUT" == *"Flow Agents: status-demo"* && "$STATUSLINE_OUT" == *"evidence/needs_decision"* && "$STATUSLINE_OUT" == *"2/3 AC"* && "$STATUSLINE_OUT" == *"next:"* ]]; then
292
+ pass "flow-agents-statusline.js resolves the legacy-only fixture identically to its own existing eval's golden output, under a different ambient actor (AC8)"
293
+ else
294
+ fail "flow-agents-statusline.js legacy-only output did not match the golden assertion: $STATUSLINE_OUT"
295
+ fi
296
+
297
+ # 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.
299
+ STEERING_REPO="$TMPDIR_EVAL/steering-legacy/repo"
300
+ mkdir -p "$STEERING_REPO/.kontourai/flow-agents/steering-demo"
301
+ mkdir -p "$STEERING_REPO/docs"
302
+ printf '# Test Repo\n' > "$STEERING_REPO/AGENTS.md"
303
+ printf '# Context Map\n' > "$STEERING_REPO/docs/context-map.md"
304
+
305
+ safe_write "$STEERING_REPO/.kontourai/flow-agents/steering-demo/state.json" <<'JSON'
306
+ {
307
+ "schema_version": "1.0",
308
+ "task_slug": "steering-demo",
309
+ "status": "not_verified",
310
+ "phase": "verification",
311
+ "updated_at": "2026-05-09T00:00:00Z",
312
+ "next_action": {
313
+ "status": "needs_user",
314
+ "summary": "Decide whether to accept the external service verification gap.",
315
+ "target_phase": "goal_fit"
316
+ }
317
+ }
318
+ JSON
319
+
320
+ STEERING_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-no-per-actor-file node "$STEERING" <<JSON
321
+ {"hook_event_name":"UserPromptSubmit","cwd":"$STEERING_REPO","prompt":"continue"}
322
+ JSON
323
+ )"
324
+ if [[ "$STEERING_OUT" == *"steering-demo"* && "$STEERING_OUT" == *"status:not_verified"* ]]; then
325
+ pass "workflow-steering.js's ambient state lookup resolves the legacy-only fixture identically to its own existing eval's golden output, under a different ambient actor (AC8)"
326
+ else
327
+ fail "workflow-steering.js legacy-only steering output did not match the golden assertion: $STEERING_OUT"
328
+ fi
329
+
330
+ # --- 4. config-protection.js covers current/<actor>.json exactly like current.json (AC10) -----
331
+ echo "--- 4. config-protection.js blocks current/<actor>.json writes/redirects (AC10) ---"
332
+
333
+ set +e
334
+ LEGACY_BLOCK_OUT=$(echo '{"tool_name":"Write","tool_input":{"path":"/repo/.kontourai/flow-agents/current.json"}}' | node "$CONFIG_PROTECTION" 2>&1)
335
+ LEGACY_BLOCK_EXIT=$?
336
+ PER_ACTOR_BLOCK_OUT=$(echo '{"tool_name":"Write","tool_input":{"path":"/repo/.kontourai/flow-agents/current/claude-code-x.json"}}' | node "$CONFIG_PROTECTION" 2>&1)
337
+ PER_ACTOR_BLOCK_EXIT=$?
338
+ set -e
339
+
340
+ if [[ "$PER_ACTOR_BLOCK_EXIT" -eq 2 ]] && echo "$PER_ACTOR_BLOCK_OUT" | grep -q "BLOCKED"; then
341
+ pass "Write to .kontourai/flow-agents/current/<actor>.json is blocked (exit 2) (AC10)"
342
+ else
343
+ fail "Write to .kontourai/flow-agents/current/<actor>.json was NOT blocked (exit=$PER_ACTOR_BLOCK_EXIT, out=$PER_ACTOR_BLOCK_OUT)"
344
+ fi
345
+
346
+ LEGACY_REASON="$(printf '%s' "$LEGACY_BLOCK_OUT" | sed -n 's/.*protected because \(.*\)\. Use .*/\1/p')"
347
+ PER_ACTOR_REASON="$(printf '%s' "$PER_ACTOR_BLOCK_OUT" | sed -n 's/.*protected because \(.*\)\. Use .*/\1/p')"
348
+ if [[ -n "$LEGACY_REASON" && "$LEGACY_REASON" == "$PER_ACTOR_REASON" ]]; then
349
+ pass "current/<actor>.json is blocked with the SAME reason text as legacy current.json (AC10)"
350
+ else
351
+ fail "current/<actor>.json's blocked reason text differs from legacy current.json's: legacy='$LEGACY_REASON' per-actor='$PER_ACTOR_REASON'"
352
+ fi
353
+
354
+ set +e
355
+ TEE_BLOCK_OUT=$(echo '{"tool_name":"Bash","tool_input":{"command":"echo {} | tee .kontourai/flow-agents/current/claude-code-x.json"}}' | node "$CONFIG_PROTECTION" 2>&1)
356
+ TEE_BLOCK_EXIT=$?
357
+ set -e
358
+ if [[ "$TEE_BLOCK_EXIT" -eq 2 ]] && echo "$TEE_BLOCK_OUT" | grep -q "BLOCKED"; then
359
+ pass "tee .kontourai/flow-agents/current/<actor>.json is blocked (exit 2) (AC10)"
360
+ else
361
+ fail "tee .kontourai/flow-agents/current/<actor>.json was NOT blocked (exit=$TEE_BLOCK_EXIT, out=$TEE_BLOCK_OUT)"
362
+ fi
363
+
364
+ set +e
365
+ UNRELATED_OUT=$(echo '{"tool_name":"Write","tool_input":{"path":"/repo/src/main.js"}}' | node "$CONFIG_PROTECTION" 2>&1)
366
+ UNRELATED_EXIT=$?
367
+ set -e
368
+ [[ "$UNRELATED_EXIT" -eq 0 ]] && pass "an unrelated file write is still allowed (exit 0) (AC10 regression guard)" || fail "an unrelated file write was falsely blocked (exit=$UNRELATED_EXIT)"
369
+
370
+ # --- 5. record-gate-claim resolves A's own per-actor flow/step, not B's legacy pointer (AC11) --
371
+ echo "--- 5. record-gate-claim resolves A's own per-actor flow/step, not B's legacy pointer (AC11) ---"
372
+
373
+ AC11_ROOT="$TMPDIR_EVAL/ac11-artifact-root"
374
+
375
+ flow_agents_node "workflow-sidecar" ensure-session \
376
+ --artifact-root "$AC11_ROOT" \
377
+ --task-slug gate-actor-a \
378
+ --actor eval-actor-a-session \
379
+ --flow-id builder.build \
380
+ --source-request "Actor A is active on a FlowDefinition-driven session." \
381
+ --summary "Actor A, builder.build, pull-work step." \
382
+ --timestamp "2026-07-01T00:00:00Z" \
383
+ >"$TMPDIR_EVAL/ac11-a-ensure.out" 2>"$TMPDIR_EVAL/ac11-a-ensure.err"
384
+
385
+ AC11_DELIVER_MD="$AC11_ROOT/gate-actor-a/gate-actor-a--deliver.md"
386
+ flow_agents_node "workflow-sidecar" init-plan "$AC11_DELIVER_MD" \
387
+ --source-request "Test" --summary "Testing" \
388
+ --timestamp "2026-07-01T00:00:30Z" >"$TMPDIR_EVAL/ac11-a-initplan.out" 2>"$TMPDIR_EVAL/ac11-a-initplan.err"
389
+
390
+ # Actor B runs a LATER, plain (non-FlowDefinition) ensure-session on an unrelated subject on the
391
+ # SAME artifact root -- this overwrites the LEGACY global current.json (last-writer-wins, no
392
+ # active_flow_id/active_step_id at all) while leaving actor A's own per-actor current file
393
+ # completely untouched. If record-gate-claim below fell back to reading the legacy global
394
+ # pointer instead of actor A's own per-actor pointer, it would find NO active flow step at all
395
+ # and die -- proving this section's assertions are a real differential test, not a tautology.
396
+ flow_agents_node "workflow-sidecar" ensure-session \
397
+ --artifact-root "$AC11_ROOT" \
398
+ --task-slug gate-actor-b-unrelated \
399
+ --actor eval-actor-b-session \
400
+ --source-request "Actor B's unrelated, non-FlowDefinition session." \
401
+ --summary "Actor B, no flow-id." \
402
+ --timestamp "2026-07-01T00:01:00Z" \
403
+ >"$TMPDIR_EVAL/ac11-b-ensure.out" 2>"$TMPDIR_EVAL/ac11-b-ensure.err"
404
+
405
+ AC11_LEGACY_CURRENT_FILE="$AC11_ROOT/current.json"
406
+ if grep -q '"active_flow_id"' "$AC11_LEGACY_CURRENT_FILE" 2>/dev/null; then
407
+ fail "test setup invalid: the legacy global current.json unexpectedly still carries an active_flow_id after actor B's plain ensure-session"
408
+ else
409
+ pass "test setup: actor B's later, plain ensure-session leaves the legacy global current.json WITHOUT any active_flow_id (a real differential test, not a tautology)"
410
+ fi
411
+
412
+ AC11_GATE_ACTOR_A_DIR="$AC11_ROOT/gate-actor-a"
413
+ if flow_agents_node "workflow-sidecar" record-gate-claim "$AC11_GATE_ACTOR_A_DIR" \
414
+ --actor eval-actor-a-session \
415
+ --status pass \
416
+ --summary "Selected issue #291 for implementation." \
417
+ --expectation selected-work \
418
+ --timestamp "2026-07-01T00:02:00Z" \
419
+ >"$TMPDIR_EVAL/ac11-gate-claim.out" 2>"$TMPDIR_EVAL/ac11-gate-claim.err"; then
420
+ pass "record-gate-claim (actor A) succeeds by resolving A's OWN per-actor current pointer, not B's stale legacy pointer (AC11)"
421
+ else
422
+ fail "record-gate-claim (actor A) unexpectedly failed: $(cat "$TMPDIR_EVAL/ac11-gate-claim.out" "$TMPDIR_EVAL/ac11-gate-claim.err")"
423
+ fi
424
+
425
+ AC11_BUNDLE_FILE="$AC11_GATE_ACTOR_A_DIR/trust.bundle"
426
+ if node - "$AC11_BUNDLE_FILE" <<'NODEEOF' 2>"$TMPDIR_EVAL/ac11-bundle-check.err"
427
+ const fs = require("fs");
428
+ const bundle = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
429
+ const target = (bundle.claims || []).find((c) => c.claimType === "builder.pull-work.selected");
430
+ if (!target) { console.error("no builder.pull-work.selected claim found"); process.exit(1); }
431
+ if (target.status !== "verified") { console.error("expected status=verified, got " + target.status); process.exit(1); }
432
+ NODEEOF
433
+ then
434
+ pass "actor A's trust.bundle carries the builder.pull-work.selected claim derived from A's OWN flow/step (AC11)"
435
+ else
436
+ fail "actor A's trust.bundle did not carry the expected claim: $(cat "$TMPDIR_EVAL/ac11-bundle-check.err")"
437
+ fi
438
+
439
+ # --- 6. F3 (fix-plan iteration 1, code-review-291 MEDIUM): legacy current.json names a
440
+ # still-ACTIVE, non-newest-mtime session while a NEWER active state.json exists elsewhere under
441
+ # the same artifact root, under a RESOLVED actor with no per-actor current file. Pre-#291,
442
+ # latestWorkflowState() was a pure global newest-mtime scan with zero current.json involvement,
443
+ # so the newer state.json would win. Post-#291, actorScopedWorkflowState() is consulted first and
444
+ # falls through to the legacy current.json for an actor with no per-actor file (the compat-shim
445
+ # guarantee section 3 already covers) -- but if that legacy pointer names a still-ACTIVE session,
446
+ # it is now returned IMMEDIATELY, without ever comparing mtimes against the newer state.json. This
447
+ # section test-pins that specific, deliberate behavior change (current.json preference wins over
448
+ # newest-mtime) for the single-actor/resolved-actor case, not just the multi-actor case AC7/AC8
449
+ # are framed around -- the review's flagged, previously test-uncovered edge.
450
+ 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) ---"
451
+
452
+ F3_REPO="$TMPDIR_EVAL/f3-legacy-vs-newest/repo"
453
+ mkdir -p "$F3_REPO/.kontourai/flow-agents/older-named-slug"
454
+ mkdir -p "$F3_REPO/.kontourai/flow-agents/newer-unnamed-slug"
455
+ mkdir -p "$F3_REPO/docs"
456
+ printf '# Test Repo
457
+ ' > "$F3_REPO/AGENTS.md"
458
+ printf '# Context Map
459
+ ' > "$F3_REPO/docs/context-map.md"
460
+
461
+ # The OLDER session (by mtime) is the one the legacy current.json names -- still ACTIVE.
462
+ safe_write "$F3_REPO/.kontourai/flow-agents/older-named-slug/state.json" <<'JSON'
463
+ {
464
+ "schema_version": "1.0",
465
+ "task_slug": "older-named-slug",
466
+ "status": "in_progress",
467
+ "phase": "build",
468
+ "updated_at": "2026-05-09T00:00:00Z",
469
+ "next_action": {
470
+ "status": "in_progress",
471
+ "summary": "Older, current.json-named session."
472
+ }
473
+ }
474
+ JSON
475
+
476
+ F3_LEGACY_CURRENT_LINE='{"active_slug":"older-named-slug","artifact_dir":"older-named-slug"}'
477
+ printf '%s' "$F3_LEGACY_CURRENT_LINE" | safe_write "$F3_REPO/.kontourai/flow-agents/current.json"
478
+
479
+ # Sleep-free mtime ordering: touch the "newer" state.json strictly after the legacy-named one, so
480
+ # a pure newest-mtime scan (pre-#291 behavior) would prefer THIS one instead.
481
+ sleep 1
482
+ safe_write "$F3_REPO/.kontourai/flow-agents/newer-unnamed-slug/state.json" <<'JSON'
483
+ {
484
+ "schema_version": "1.0",
485
+ "task_slug": "newer-unnamed-slug",
486
+ "status": "in_progress",
487
+ "phase": "build",
488
+ "updated_at": "2026-05-10T00:00:00Z",
489
+ "next_action": {
490
+ "status": "in_progress",
491
+ "summary": "Newer, but NOT named by current.json."
492
+ }
493
+ }
494
+ JSON
495
+
496
+ # A RESOLVED actor (FLOW_AGENTS_ACTOR set) with no per-actor current file -- actorScopedWorkflowState
497
+ # falls through to the legacy current.json (same compat-shim guarantee as section 3), which names
498
+ # the OLDER-but-still-active session.
499
+ F3_OUT="$(FLOW_AGENTS_ACTOR=eval-actor-f3-resolved node "$ROOT/scripts/hooks/workflow-steering.js" <<JSON
500
+ {"hook_event_name":"UserPromptSubmit","cwd":"$F3_REPO","prompt":"continue"}
501
+ JSON
502
+ )"
503
+
504
+ if [[ "$F3_OUT" == *"older-named-slug"* && "$F3_OUT" != *"newer-unnamed-slug"* ]]; then
505
+ 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)"
506
+ else
507
+ fail "workflow-steering.js did not prefer the legacy-named active session over the newer-mtime one: $F3_OUT"
508
+ fi
509
+
510
+ echo ""
511
+ if [[ "$errors" -eq 0 ]]; then
512
+ echo "test_current_json_per_actor: all checks passed."
513
+ else
514
+ echo "test_current_json_per_actor: $errors check(s) failed."
515
+ fi
516
+ exit "$errors"