@kontourai/flow-agents 3.9.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 (106) hide show
  1. package/.github/workflows/ci.yml +8 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +1 -1
  4. package/build/src/cli/assignment-provider.js +4 -18
  5. package/build/src/cli/continuation-adapter.d.ts +10 -8
  6. package/build/src/cli/continuation-adapter.js +22 -4
  7. package/build/src/cli/validate-workflow-artifacts.js +21 -1
  8. package/build/src/cli/workflow-sidecar.js +15 -20
  9. package/build/src/cli/workflow.js +58 -11
  10. package/build/src/continuation-driver.d.ts +27 -3
  11. package/build/src/continuation-driver.js +97 -14
  12. package/build/src/index.d.ts +2 -2
  13. package/build/src/index.js +1 -1
  14. package/build/src/tools/build-universal-bundles.js +52 -25
  15. package/build/src/tools/validate-source-tree.js +5 -2
  16. package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  17. package/context/scripts/hooks/stop-goal-fit.js +244 -73
  18. package/context/scripts/hooks/workflow-steering.js +24 -15
  19. package/context/scripts/telemetry/lib/pricing.sh +18 -6
  20. package/context/scripts/telemetry/lib/transport.sh +139 -37
  21. package/context/scripts/telemetry/lib/usage.sh +282 -32
  22. package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  23. package/context/scripts/telemetry/telemetry.sh +76 -1
  24. package/docs/coordination-guide.md +21 -5
  25. package/docs/fixture-ownership.md +2 -1
  26. package/docs/public-workflow-cli.md +44 -0
  27. package/docs/spec/runtime-hook-surface.md +8 -4
  28. package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
  29. package/evals/acceptance/prove-capture-teeth.sh +13 -0
  30. package/evals/ci/run-baseline.sh +2 -0
  31. package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
  32. package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
  33. package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
  34. package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
  35. package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
  36. package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
  37. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
  38. package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
  39. package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
  40. package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
  41. package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
  42. package/evals/integration/test_actor_identity.sh +63 -0
  43. package/evals/integration/test_assignment_provider_local_file.sh +34 -0
  44. package/evals/integration/test_builder_entry_enforcement.sh +5 -1
  45. package/evals/integration/test_bundle_install.sh +106 -17
  46. package/evals/integration/test_bundle_lifecycle.sh +17 -3
  47. package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
  48. package/evals/integration/test_ci_actor_identity.sh +2 -2
  49. package/evals/integration/test_command_log_concurrency.sh +11 -0
  50. package/evals/integration/test_command_log_fork_classification.sh +11 -0
  51. package/evals/integration/test_command_log_integrity.sh +11 -0
  52. package/evals/integration/test_current_json_per_actor.sh +416 -21
  53. package/evals/integration/test_enforcer_expects_driven.sh +11 -0
  54. package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
  55. package/evals/integration/test_evidence_capture_hook.sh +25 -6
  56. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  57. package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
  58. package/evals/integration/test_gate_bypass_chain.sh +11 -0
  59. package/evals/integration/test_gate_lockdown.sh +10 -0
  60. package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
  61. package/evals/integration/test_goal_fit_hook.sh +257 -9
  62. package/evals/integration/test_goal_fit_rederive.sh +11 -0
  63. package/evals/integration/test_install_merge.sh +141 -3
  64. package/evals/integration/test_liveness_conflict_injection.sh +39 -3
  65. package/evals/integration/test_liveness_heartbeat.sh +46 -9
  66. package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
  67. package/evals/integration/test_public_workflow_cli.sh +6 -6
  68. package/evals/integration/test_session_resume_roundtrip.sh +30 -0
  69. package/evals/integration/test_stop_hook_release.sh +111 -11
  70. package/evals/integration/test_telemetry_tool_usage.sh +394 -0
  71. package/evals/integration/test_verify_hold.sh +64 -1
  72. package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
  73. package/evals/integration/test_workflow_steering_hook.sh +20 -4
  74. package/evals/run.sh +2 -0
  75. package/evals/static/test_universal_bundles.sh +123 -0
  76. package/package.json +1 -1
  77. package/packaging/README.md +21 -0
  78. package/packaging/codex-legacy-agents-fingerprints.json +33 -0
  79. package/scripts/README.md +2 -1
  80. package/scripts/audit-codex-legacy-agents.js +43 -0
  81. package/scripts/classify-codex-legacy-agents.js +71 -0
  82. package/scripts/hooks/evidence-capture.js +18 -6
  83. package/scripts/hooks/lib/actor-identity.js +62 -29
  84. package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
  85. package/scripts/hooks/lib/current-pointer.js +114 -12
  86. package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
  87. package/scripts/hooks/stop-goal-fit.js +244 -73
  88. package/scripts/hooks/workflow-steering.js +24 -15
  89. package/scripts/install-codex-home.sh +8 -7
  90. package/scripts/telemetry/lib/pricing.sh +18 -6
  91. package/scripts/telemetry/lib/transport.sh +71 -0
  92. package/scripts/telemetry/lib/usage.sh +282 -32
  93. package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
  94. package/scripts/telemetry/telemetry.sh +76 -1
  95. package/src/cli/assignment-provider.ts +5 -26
  96. package/src/cli/builder-flow-runtime.test.mjs +93 -6
  97. package/src/cli/continuation-adapter.ts +33 -7
  98. package/src/cli/continuation-driver.test.mjs +741 -2
  99. package/src/cli/public-api.test.mjs +1 -0
  100. package/src/cli/validate-workflow-artifacts.ts +19 -1
  101. package/src/cli/workflow-sidecar.ts +18 -23
  102. package/src/cli/workflow.ts +71 -11
  103. package/src/continuation-driver.ts +125 -19
  104. package/src/index.ts +4 -0
  105. package/src/tools/build-universal-bundles.ts +61 -25
  106. package/src/tools/validate-source-tree.ts +5 -2
@@ -46,6 +46,7 @@
46
46
  set -uo pipefail
47
47
 
48
48
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
49
+ CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
49
50
  source "$ROOT/evals/lib/node.sh"
50
51
 
51
52
  CLI="$ROOT/build/src/cli.js"
@@ -196,6 +197,31 @@ write_steering_fixture_repo() {
196
197
  mv "$repoRoot/.kontourai/flow-agents/$slug/state.dot.json.tmp" "$repoRoot/.kontourai/flow-agents/$slug/state.json"
197
198
  }
198
199
 
200
+ # seed_current_pointer <repo_root> <slug> <actor> — #440 FIXTURE-GAP: this suite's
201
+ # write_steering_fixture_repo fixtures were written before #440's per-actor ownership scoping and
202
+ # never establish a per-actor current pointer for the invoking actor -- under a RESOLVED
203
+ # FLOW_AGENTS_ACTOR override, workflow-steering.js's actorScopedWorkflowState now scopes to that
204
+ # actor's own (nonexistent) pointer and never reaches the fixture-under-test. Seeds BOTH the
205
+ # legacy current.json AND the per-actor current/<actor>.json pointer with the SAME payload,
206
+ # mirroring workflow-sidecar.ts's real writeCurrent() dual-write via current-pointer.js's own
207
+ # writePerActorCurrent.
208
+ seed_current_pointer() {
209
+ local repoRoot="$1" slug="$2" actor="$3"
210
+ local flowAgentsDir="$repoRoot/.kontourai/flow-agents"
211
+ CP_HELPER_ARG="$CURRENT_POINTER_HELPER" DIR_ARG="$flowAgentsDir" SLUG_ARG="$slug" ACTOR_ARG="$actor" node - <<'NODE'
212
+ const fs = require('fs');
213
+ const path = require('path');
214
+ const { writePerActorCurrent } = require(process.env.CP_HELPER_ARG);
215
+ const dir = process.env.DIR_ARG;
216
+ const slug = process.env.SLUG_ARG;
217
+ const actor = process.env.ACTOR_ARG;
218
+ const payload = { schema_version: '1.0', active_slug: slug, artifact_dir: slug };
219
+ fs.mkdirSync(dir, { recursive: true });
220
+ fs.writeFileSync(path.join(dir, 'current.json'), JSON.stringify(payload, null, 2) + '\n');
221
+ writePerActorCurrent(dir, actor, payload);
222
+ NODE
223
+ }
224
+
199
225
  ROOT_SCRATCH_STATE_WRITER="$TMPDIR_EVAL/write-state-json.js"
200
226
  cat > "$ROOT_SCRATCH_STATE_WRITER" << 'JSEOF'
201
227
  const fs = require("fs");
@@ -388,7 +414,7 @@ mkdir -p "$UNSTABLE_DIR"
388
414
  # (a) Unstable current actor: unset the explicit override, the native runtime session id, AND every
389
415
  # CI-provider marker (#398) so resolveActor() genuinely falls through to process-ancestry/unresolved
390
416
  # regardless of whether this suite is running inside a CI job.
391
- if (unset FLOW_AGENTS_ACTOR CLAUDE_CODE_SESSION_ID CODEX_SESSION_ID OPENCODE_SESSION_ID PI_SESSION_ID CLAUDECODE \
417
+ if (unset FLOW_AGENTS_ACTOR CLAUDE_CODE_SESSION_ID CODEX_THREAD_ID CODEX_SESSION_ID OPENCODE_SESSION_ID PI_SESSION_ID CLAUDECODE \
392
418
  GITHUB_ACTIONS GITLAB_CI CIRCLECI JENKINS_URL TF_BUILD BUILDKITE; \
393
419
  flow_agents_node "$WRITER" verify-hold "$UNSTABLE_DIR") >"$TMPDIR_EVAL/vh-unstable.out" 2>"$TMPDIR_EVAL/vh-unstable.err"; then
394
420
  pass "verify-hold with an unstable (ancestry/unresolved) current actor does NOT hard-block a subject held by a different actor (advisory-only, AC1 SECOND fix)"
@@ -713,6 +739,12 @@ grep -qF "hostile-holder-actor" "$TMPDIR_EVAL/vh-hostile.out" && pass "the sanit
713
739
  STEERING_REPO="$TMPDIR_EVAL/steering-hostile-repo"
714
740
  STEERING_HOSTILE_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
715
741
  write_steering_fixture_repo "$STEERING_REPO" "$HOSTILE_SLUG" "$STEERING_HOSTILE_NOW"
742
+ # #440 FIX 2 (de-vacuate, delta review): the invoking actor (eval-actor-hostile-reader) owns
743
+ # HOSTILE_SLUG -- seed its own per-actor pointer (real dual-write helper) so
744
+ # actorScopedWorkflowState finds it and supersessionSteering is REACHED at all; without this the
745
+ # absence assertion below passed only because no steering output existed in the first place, not
746
+ # because the sanitization itself was exercised.
747
+ seed_current_pointer "$STEERING_REPO" "$HOSTILE_SLUG" "eval-actor-hostile-reader"
716
748
  append_liveness_event "$STEERING_REPO/.kontourai/flow-agents" "$HOSTILE_SLUG" "$HOSTILE_HOLDER_ACTOR" "$STEERING_HOSTILE_NOW" 1800
717
749
 
718
750
  if FLOW_AGENTS_ACTOR="eval-actor-hostile-reader" node "$ROOT/scripts/hooks/workflow-steering.js" \
@@ -724,6 +756,14 @@ then
724
756
  else
725
757
  fail "steering hook should not fail for the hostile-holder fixture"
726
758
  fi
759
+ # #440 FIX 2 reachability control (delta review): prove the supersession notice is ACTUALLY
760
+ # present and names the sanitized holder -- the ANSI/control-byte absence checks below would
761
+ # otherwise pass vacuously if no notice were emitted at all (exactly the pre-fix defect).
762
+ if grep -qF "[SUPERSEDED:" "$TMPDIR_EVAL/steering-hostile.out" && grep -qF "hostile-holder-actor" "$TMPDIR_EVAL/steering-hostile.out"; then
763
+ pass "#440 reachability control: the steering supersession notice is present and names the sanitized (non-control-byte) hostile holder actor (AC7)"
764
+ else
765
+ fail "#440 reachability control: no supersession notice was emitted, or it did not name the sanitized hostile holder actor: $(cat "$TMPDIR_EVAL/steering-hostile.out")"
766
+ fi
727
767
  if grep -qF $'\x1b[31;1m' "$TMPDIR_EVAL/steering-hostile.out" || grep -qF $'\x07' "$TMPDIR_EVAL/steering-hostile.out"; then
728
768
  fail "hostile holder actor's raw ANSI/control bytes leaked into the steering supersession notice (AC7)"
729
769
  else
@@ -835,6 +875,7 @@ STEERING_SUP_REPO="$TMPDIR_EVAL/steering-supersede-repo"
835
875
  SUP_SLUG="steering-superseded-demo"
836
876
  SUP_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
837
877
  write_steering_fixture_repo "$STEERING_SUP_REPO" "$SUP_SLUG" "$SUP_NOW"
878
+ seed_current_pointer "$STEERING_SUP_REPO" "$SUP_SLUG" "eval-actor-steering-self-a"
838
879
  append_liveness_event "$STEERING_SUP_REPO/.kontourai/flow-agents" "$SUP_SLUG" "eval-actor-steering-holder-b" "$SUP_NOW" 1800
839
880
 
840
881
  # 7a. UserPromptSubmit on a SECOND, unrelated turn (not the takeover turn itself) — the
@@ -869,6 +910,13 @@ NO_SUP_REPO="$TMPDIR_EVAL/steering-no-supersede-repo"
869
910
  NO_SUP_SLUG="steering-not-superseded-demo"
870
911
  NO_SUP_NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
871
912
  write_steering_fixture_repo "$NO_SUP_REPO" "$NO_SUP_SLUG" "$NO_SUP_NOW"
913
+ # #440 FIX 2 (de-vacuate, delta review): the invoking actor (eval-actor-steering-self-a) owns
914
+ # NO_SUP_SLUG -- seed its own per-actor pointer (real dual-write helper) so
915
+ # actorScopedWorkflowState finds it. Without this, the "no notice" assertions below would pass
916
+ # only because NO scoped session existed at all (analyze()/actorScopedWorkflowState finds
917
+ # nothing), not because a genuinely scoped session correctly found no other-actor holder -- the
918
+ # exact "no scoped session" vs "scoped session, no other holder" distinction this case must prove.
919
+ seed_current_pointer "$NO_SUP_REPO" "$NO_SUP_SLUG" "eval-actor-steering-self-a"
872
920
  # No liveness stream at all for this repo — no other-actor holder can exist.
873
921
 
874
922
  if FLOW_AGENTS_ACTOR="eval-actor-steering-self-a" node "$ROOT/scripts/hooks/workflow-steering.js" \
@@ -880,6 +928,14 @@ then
880
928
  else
881
929
  fail "steering hook should not fail for UserPromptSubmit on the non-superseded fixture"
882
930
  fi
931
+ # #440 FIX 2 reachability control (delta review): prove the session WAS scoped/found (the STATE
932
+ # banner is present) so the "no SUPERSEDED notice" checks below prove "scoped session, no other
933
+ # holder" rather than vacuously "no scoped session at all".
934
+ if grep -qF "STATE: $NO_SUP_SLUG is status:in_progress phase:execution" "$TMPDIR_EVAL/steering-no-sup-prompt.out"; then
935
+ pass "#440 reachability control: actor A's own scoped session is found at UserPromptSubmit (STATE banner present) (AC5)"
936
+ else
937
+ fail "#440 reachability control: actor A's own scoped session was NOT found at UserPromptSubmit (no STATE banner): $(cat "$TMPDIR_EVAL/steering-no-sup-prompt.out")"
938
+ fi
883
939
  if grep -qF "[SUPERSEDED:" "$TMPDIR_EVAL/steering-no-sup-prompt.out"; then
884
940
  fail "supersession notice incorrectly appeared at UserPromptSubmit with no other-actor fresh holder (false positive) (AC5)"
885
941
  else
@@ -895,6 +951,13 @@ then
895
951
  else
896
952
  fail "steering hook should not fail for SessionStart on the non-superseded fixture"
897
953
  fi
954
+ # #440 FIX 2 reachability control (delta review): same rationale as the UserPromptSubmit check
955
+ # above, for SessionStart's RESUME block.
956
+ if grep -qF "RESUME: $NO_SUP_SLUG" "$TMPDIR_EVAL/steering-no-sup-start.out"; then
957
+ pass "#440 reachability control: actor A's own scoped session is found at SessionStart (RESUME banner present) (AC5)"
958
+ else
959
+ fail "#440 reachability control: actor A's own scoped session was NOT found at SessionStart (no RESUME banner): $(cat "$TMPDIR_EVAL/steering-no-sup-start.out")"
960
+ fi
898
961
  if grep -qF "[SUPERSEDED:" "$TMPDIR_EVAL/steering-no-sup-start.out"; then
899
962
  fail "supersession notice incorrectly appeared at SessionStart with no other-actor fresh holder (false positive) (AC5)"
900
963
  else
@@ -3,6 +3,7 @@
3
3
  set -uo pipefail
4
4
 
5
5
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
6
+ CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
6
7
  source "$ROOT/evals/lib/node.sh"
7
8
 
8
9
  TMPDIR_EVAL="$(mktemp -d)"
@@ -4467,9 +4468,17 @@ fi
4467
4468
 
4468
4469
  # Poison BOTH navigation pointers. Exact-session writers must continue to use the persisted
4469
4470
  # canonical flow_run from the session instead of treating ambient pointers as authority.
4471
+ # #440 fix-wave 2: the per-actor filename is now collision-resistant (sanitized prefix + hash of
4472
+ # the FULL actor key, see current-pointer.js's perActorCurrentFile) -- resolve the real per-actor
4473
+ # path via the shared helper instead of hand-deriving the old (now-stale) "<actor>.json" name.
4474
+ UNRESOLVABLE_PER_ACTOR_FILE="$(CP_HELPER_ARG="$CURRENT_POINTER_HELPER" ROOT_ARG="$UNRESOLVABLE_ROOT" ACTOR_ARG="unresolvable-actor" node - <<'NODE'
4475
+ const { perActorCurrentFile } = require(process.env.CP_HELPER_ARG);
4476
+ process.stdout.write(perActorCurrentFile(process.env.ROOT_ARG, process.env.ACTOR_ARG));
4477
+ NODE
4478
+ )"
4470
4479
  node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/unresolvable-mutate.err"
4471
4480
  import { readFileSync, writeFileSync } from 'node:fs';
4472
- for (const f of ['${UNRESOLVABLE_ROOT}/current.json', '${UNRESOLVABLE_ROOT}/current/unresolvable-actor.json']) {
4481
+ for (const f of ['${UNRESOLVABLE_ROOT}/current.json', '${UNRESOLVABLE_PER_ACTOR_FILE}']) {
4473
4482
  const d = JSON.parse(readFileSync(f, 'utf8'));
4474
4483
  d.active_flow_id = 'builder.bogus-nonexistent-flow-270';
4475
4484
  writeFileSync(f, JSON.stringify(d, null, 2) + '\n');
@@ -3,6 +3,7 @@
3
3
  set -uo pipefail
4
4
 
5
5
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
6
+ CURRENT_POINTER_HELPER="$ROOT/scripts/hooks/lib/current-pointer.js"
6
7
 
7
8
  TMPDIR_EVAL="$(mktemp -d)"
8
9
  errors=0
@@ -43,7 +44,22 @@ cat > "$REPO/.kontourai/flow-agents/steering-demo/trust.bundle" <<'JSON'
43
44
  {"schema_version":"1.0","claims":[]}
44
45
  JSON
45
46
 
46
- if node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/steering.out" 2>"$TMPDIR_EVAL/steering.err" <<JSON
47
+ # #440 FIXTURE-GAP: this file never set FLOW_AGENTS_ACTOR (every invocation ran under whichever
48
+ # ambient/ancestry actor happened to resolve, never asserted on). The 4 assertions below that
49
+ # expect steering-demo's STATE/RESUME banner need a RESOLVED actor that legitimately owns
50
+ # steering-demo -- a stable, explicit override plus its own per-actor current pointer (mirroring
51
+ # workflow-sidecar.ts's real writeCurrent() dual-write via current-pointer.js's own
52
+ # writePerActorCurrent). Only the specific invocations that need this pass FLOW_AGENTS_ACTOR below
53
+ # -- every other invocation in this file is untouched and keeps resolving via whatever ambient
54
+ # actor it always has (harmless: none of the untouched assertions depend on `current`).
55
+ STEERING_ACTOR="eval-workflow-steering-actor"
56
+ CP_HELPER_ARG="$CURRENT_POINTER_HELPER" FLOW_AGENTS_DIR_ARG="$REPO/.kontourai/flow-agents" \
57
+ SLUG_ARG="steering-demo" ACTOR_ARG="$STEERING_ACTOR" node - <<'NODE'
58
+ const { writePerActorCurrent } = require(process.env.CP_HELPER_ARG);
59
+ writePerActorCurrent(process.env.FLOW_AGENTS_DIR_ARG, process.env.ACTOR_ARG, { active_slug: process.env.SLUG_ARG });
60
+ NODE
61
+
62
+ if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/steering.out" 2>"$TMPDIR_EVAL/steering.err" <<JSON
47
63
  {"cwd":"$REPO","tool_input":{"command":"InvokeSubagents","content":{"subagents":[{"agent_name":"tool-verifier"}]}},"tool_response":"verification finished"}
48
64
  JSON
49
65
  then
@@ -137,7 +153,7 @@ else
137
153
  _fail "workflow steering hook should not fail for ordinary non-subagent tools"
138
154
  fi
139
155
 
140
- if node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/prompt.out" 2>"$TMPDIR_EVAL/prompt.err" <<JSON
156
+ if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/workflow-steering.js" >"$TMPDIR_EVAL/prompt.out" 2>"$TMPDIR_EVAL/prompt.err" <<JSON
141
157
  {"hook_event_name":"UserPromptSubmit","cwd":"$REPO","prompt":"continue"}
142
158
  JSON
143
159
  then
@@ -174,7 +190,7 @@ else
174
190
  _fail "Claude hook adapter should not fail for workflow steering"
175
191
  fi
176
192
 
177
- if node "$ROOT/scripts/hooks/claude-hook-adapter.js" UserPromptSubmit prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/claude-prompt-adapter.out" 2>"$TMPDIR_EVAL/claude-prompt-adapter.err" <<JSON
193
+ if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/claude-hook-adapter.js" UserPromptSubmit prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/claude-prompt-adapter.out" 2>"$TMPDIR_EVAL/claude-prompt-adapter.err" <<JSON
178
194
  {"hook_event_name":"UserPromptSubmit","cwd":"$REPO","prompt":"continue"}
179
195
  JSON
180
196
  then
@@ -551,7 +567,7 @@ else
551
567
  _fail "Codex hook adapter should not fail after tool-worker execution"
552
568
  fi
553
569
 
554
- if node "$ROOT/scripts/hooks/codex-hook-adapter.js" prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/codex-prompt-adapter.out" 2>"$TMPDIR_EVAL/codex-prompt-adapter.err" <<JSON
570
+ if FLOW_AGENTS_ACTOR="$STEERING_ACTOR" node "$ROOT/scripts/hooks/codex-hook-adapter.js" prompt:workflow-steering workflow-steering.js standard,strict >"$TMPDIR_EVAL/codex-prompt-adapter.out" 2>"$TMPDIR_EVAL/codex-prompt-adapter.err" <<JSON
555
571
  {"hook_event_name":"UserPromptSubmit","cwd":"$REPO","prompt":"continue"}
556
572
  JSON
557
573
  then
package/evals/run.sh CHANGED
@@ -175,6 +175,8 @@ run_integration() {
175
175
  echo ""
176
176
  bash "$EVAL_DIR/integration/test_telemetry_usage_pipeline.sh" || result=1
177
177
  echo ""
178
+ bash "$EVAL_DIR/integration/test_telemetry_tool_usage.sh" || result=1
179
+ echo ""
178
180
  bash "$EVAL_DIR/integration/test_telemetry_doctor.sh" || result=1
179
181
  echo ""
180
182
  bash "$EVAL_DIR/integration/test_usage_feedback_outcomes.sh" || result=1
@@ -65,12 +65,135 @@ for dir in "$DIST_DIR/kiro" "$DIST_DIR/claude-code" "$DIST_DIR/codex" "$DIST_DIR
65
65
  fi
66
66
  done
67
67
 
68
+ if node - "$ROOT_DIR/package.json" "$DIST_DIR" <<'NODE'
69
+ const fs = require("node:fs");
70
+ const path = require("node:path");
71
+ const [sourcePackageFile, dist] = process.argv.slice(2);
72
+ const source = JSON.parse(fs.readFileSync(sourcePackageFile, "utf8"));
73
+ for (const runtime of ["base", "kiro", "claude-code", "codex", "opencode", "pi"]) {
74
+ const manifest = JSON.parse(fs.readFileSync(path.join(dist, runtime, "build", "package.json"), "utf8"));
75
+ if (manifest.name !== "@kontourai/flow-agents" || manifest.version !== source.version || manifest.type !== "module") {
76
+ throw new Error(`${runtime} build package identity does not match Flow Agents ${source.version}`);
77
+ }
78
+ }
79
+ NODE
80
+ then
81
+ _pass "all bundles stamp their compiled runtime with Flow Agents package identity"
82
+ else
83
+ _fail "bundle compiled-runtime package identity is missing or stale"
84
+ fi
85
+
68
86
  if [[ -d "$DIST_DIR/codex/.agents/skills/plan-work" && ! -e "$DIST_DIR/codex/.codex/skills" ]]; then
69
87
  _pass "Codex exports portable skills only in the universal .agents catalog"
70
88
  else
71
89
  _fail "Codex portable skills are not rooted exclusively at .agents/skills"
72
90
  fi
73
91
 
92
+ if [[ ! -e "$DIST_DIR/codex/AGENTS.md" ]] \
93
+ && grep -q 'Generated from the canonical source' "$DIST_DIR/codex/README.md" \
94
+ && grep -q '## Exported agents' "$DIST_DIR/codex/README.md" \
95
+ && grep -q '`tool-worker`' "$DIST_DIR/codex/README.md" \
96
+ && ! grep -q 'Universal Agent Bundle (Codex)' "$DIST_DIR/codex/README.md"; then
97
+ _pass "Codex provenance and agent inventory are documentation, not global instructions"
98
+ else
99
+ _fail "Codex bundle still exposes generated global instructions or lost README discovery metadata"
100
+ fi
101
+
102
+ echo ""
103
+ echo "--- Documentation and Instruction Discovery Contract ---"
104
+ # Keep this table aligned with the supported-runtime capability policy. A dash
105
+ # means the harness has no bundle-owned repository instruction file.
106
+ while IFS='|' read -r runtime bundle_dir instruction_rel; do
107
+ readme="$DIST_DIR/$bundle_dir/README.md"
108
+ if [[ -f "$readme" ]] \
109
+ && grep -q 'Generated from the canonical source' "$readme" \
110
+ && grep -q '## Exported agents' "$readme" \
111
+ && grep -q '`tool-worker`' "$readme"; then
112
+ _pass "$runtime keeps provenance and the exported-agent inventory in README.md"
113
+ else
114
+ _fail "$runtime README.md is missing canonical provenance or exported-agent inventory"
115
+ fi
116
+
117
+ if [[ "$instruction_rel" == "-" ]]; then
118
+ if [[ ! -e "$DIST_DIR/$bundle_dir/AGENTS.md" && ! -e "$DIST_DIR/$bundle_dir/CLAUDE.md" ]]; then
119
+ _pass "$runtime does not publish a repository instruction surface"
120
+ else
121
+ _fail "$runtime publishes an instruction file despite declaring no discovery surface"
122
+ fi
123
+ continue
124
+ fi
125
+
126
+ instruction="$DIST_DIR/$bundle_dir/$instruction_rel"
127
+ if [[ -f "$instruction" ]]; then
128
+ _pass "$runtime publishes concise guidance on $instruction_rel"
129
+ else
130
+ _fail "$runtime is missing its $instruction_rel discovery surface"
131
+ continue
132
+ fi
133
+
134
+ if ! grep -Eq 'Treat the repo root as the source of truth|regenerate the bundle|were copied from the canonical source|Kiro-only hook wiring was stripped|## Exported Agents|## Exported agents' "$instruction" \
135
+ && [[ "$(grep -Ec '^- `tool-' "$instruction" || true)" -eq 0 ]]; then
136
+ _pass "$runtime instructions exclude maintainer prose and the exported-agent catalog"
137
+ else
138
+ _fail "$runtime instructions contain maintainer provenance, copied-directory notes, Kiro stripping notes, or a full agent catalog"
139
+ fi
140
+ done <<'MATRIX'
141
+ Base|base|AGENTS.md
142
+ Kiro|kiro|AGENTS.md
143
+ Claude Code|claude-code|CLAUDE.md
144
+ Codex|codex|-
145
+ OpenCode|opencode|AGENTS.md
146
+ pi|pi|AGENTS.md
147
+ MATRIX
148
+
149
+ if node - "$ROOT_DIR/src/tools/build-universal-bundles.ts" <<'NODE'
150
+ const fs = require("node:fs");
151
+ const source = fs.readFileSync(process.argv[2], "utf8");
152
+ for (const runtime of ["base", "kiro", "claude-code", "codex", "opencode", "pi"]) {
153
+ if (!source.includes(`\"${runtime}\"`)) throw new Error(`shared capability policy missing ${runtime}`);
154
+ }
155
+ for (const required of ["BUNDLE_CAPABILITIES", "renderBundleReadme", "renderOperationalInstructions", "installScript"]) {
156
+ if (!source.includes(required)) throw new Error(`shared bundle policy missing ${required}`);
157
+ }
158
+ if (source.includes("exportRootAgentsMd")) throw new Error("legacy combined provenance/instruction renderer remains");
159
+
160
+ const builders = source.match(/function build(?:Base|Kiro|ClaudeCode|Codex|Opencode|Pi)\([^]*?(?=\nfunction |\nexport |\nmain\()/g) || [];
161
+ if (builders.length !== 6) throw new Error(`expected six runtime builders, found ${builders.length}`);
162
+ for (const builder of builders) {
163
+ if (!builder.includes("BUNDLE_CAPABILITIES")) throw new Error("runtime builder does not select shared capability data");
164
+ if (!builder.includes("installScript(")) throw new Error("runtime builder bypasses the shared installer generator");
165
+ if (/AGENTS\.md|CLAUDE\.md/.test(builder)) throw new Error("runtime builder contains a local instruction-file branch");
166
+ }
167
+ NODE
168
+ then
169
+ _pass "all runtime builders select shared capabilities, renderers, and installer policy without local instruction branches"
170
+ else
171
+ _fail "runtime bundle policy is duplicated or bypasses shared capabilities/renderers/installer preservation"
172
+ fi
173
+
174
+ if (cd "$ROOT_DIR" && node scripts/audit-codex-legacy-agents.js packaging/codex-legacy-agents-fingerprints.json >/tmp/codex-legacy-agents-audit.txt 2>&1) \
175
+ && grep -q 'Audited 14 seed-capable releases; 1 unique payload(s); gaps=0' /tmp/codex-legacy-agents-audit.txt; then
176
+ _pass "Codex legacy fingerprint catalog independently covers all seed-capable release tags"
177
+ else
178
+ _fail "Codex legacy fingerprint catalog is incomplete, stale, or not byte-reproducible"
179
+ fi
180
+
181
+ if node - "$ROOT_DIR/scripts/classify-codex-legacy-agents.js" <<'NODE'
182
+ const fs = require("node:fs");
183
+ const source = fs.readFileSync(process.argv[2], "utf8");
184
+ for (const forbidden of ["renameSync", "unlinkSync", "writeFileSync", "copyFileSync", "mkdirSync", "chmodSync", "MIGRATION_TEST", "--apply", "quarantine", "mv --", "rm --", "cp --"]) {
185
+ if (source.includes(forbidden)) throw new Error(`production classifier contains destructive/test path: ${forbidden}`);
186
+ }
187
+ for (const required of ["lstatSync", "openSync", "O_RDONLY", "O_NOFOLLOW", "fstatSync", "readFileSync(fd)", "createHash", "manual remediation checklist", "shellQuote"]) {
188
+ if (!source.includes(required)) throw new Error(`production classifier missing read-only behavior: ${required}`);
189
+ }
190
+ NODE
191
+ then
192
+ _pass "Codex legacy classifier is read-only and exposes no production apply or test hooks"
193
+ else
194
+ _fail "Codex legacy classifier contains mutation/apply/test-hook behavior"
195
+ fi
196
+
74
197
  if node - "$DIST_DIR/codex/.agents/skills" <<'NODE'
75
198
  const fs = require("node:fs");
76
199
  const path = require("node:path");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
5
5
  "keywords": [
6
6
  "agents",
@@ -25,6 +25,27 @@ All targets also receive shared canonical directories where supported: `context/
25
25
 
26
26
  `docs/` and `evals/` are intentionally included in generated bundles today. `docs/` gives installed agents durable local reference material, and `evals/` provides install-time and runtime smoke tests for the exported bundle. If bundle size becomes a product constraint, prune these through `packaging/manifest.json` and update install tests rather than deleting generated output by hand.
27
27
 
28
+ ## Documentation And Instruction Discovery
29
+
30
+ Every runtime bundle puts generated-source provenance and the complete exported-agent inventory in `README.md`. That content is maintainer documentation, not agent instructions. A shared runtime capability policy separately decides whether a bundle publishes a concise repository instruction file:
31
+
32
+ | Runtime | Bundle documentation | Repository instruction surface |
33
+ | --- | --- | --- |
34
+ | Base | `README.md` | `AGENTS.md` |
35
+ | Kiro | `README.md` | `AGENTS.md` |
36
+ | Claude Code | `README.md` | `CLAUDE.md` |
37
+ | Codex | `README.md` | None |
38
+ | OpenCode | `README.md` | `AGENTS.md` |
39
+ | pi | `README.md` | `AGENTS.md` |
40
+
41
+ Instruction files contain only concise operational guidance discoverable by the target harness. They do not contain regeneration advice, packaging provenance, or the exported-agent catalog.
42
+
43
+ ## Repository Instruction Preservation
44
+
45
+ All generated installers use one preservation policy for repository instructions. An existing `AGENTS.md` or `CLAUDE.md` is repository-owned and is never overwritten or deleted. The installer excludes both paths from synchronization, including Kiro's `rsync --delete` path, and creates a runtime's declared instruction file only when that destination path is absent. This applies equally to direct bundle installation and `flow-agents init`.
46
+
47
+ The CLI does not own a second instruction policy: `flow-agents init` selects a generated runtime bundle and invokes that bundle's `install.sh`. Changes to discovery or preservation therefore belong in the shared bundle capability and installer generation code, with direct and CLI-mediated fixtures covering the same behavior.
48
+
28
49
  ## Generated And Runtime Boundaries
29
50
 
30
51
  `dist/` is a generated export surface, not the source of truth. Installed runtime directories such as `.codex/` and `.claude/` are also not source. They are created from the generated target bundle and installer scripts. If generated or installed hook config is wrong, fix the canonical source, rebuild `dist/`, and reinstall the runtime config.
@@ -0,0 +1,33 @@
1
+ {
2
+ "schema_version": 2,
3
+ "audit": {
4
+ "first_tag": "v2.4.0",
5
+ "last_tag": "v3.9.0",
6
+ "selection": "Every reachable release tag whose scripts/install-codex-home.sh contains the exact root seed loop 'for seed_file in AGENTS.md'.",
7
+ "reproduction_command": "node scripts/audit-codex-legacy-agents.js packaging/codex-legacy-agents-fingerprints.json",
8
+ "gaps": []
9
+ },
10
+ "files": [
11
+ {
12
+ "sha256": "5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228",
13
+ "bytes": 3315,
14
+ "fixture": "evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md",
15
+ "covered_releases": [
16
+ { "tag": "v2.4.0", "commit": "bee760272b8bb770df02400ccc5881bd3dbc8806" },
17
+ { "tag": "v3.0.0", "commit": "99abf1f6f44a333867b03702548927a12ad01275" },
18
+ { "tag": "v3.1.0", "commit": "7e6a9594212ef7813c428069b9f7fc6f39def8f1" },
19
+ { "tag": "v3.2.0", "commit": "15d5e9ae2bb9a80c3b3a89bf7887b5eff596f5d8" },
20
+ { "tag": "v3.3.0", "commit": "21c98807a259dfc4449bae2f359e975893dc4f4f" },
21
+ { "tag": "v3.4.0", "commit": "130a94696dac17cf1a65e6525e9168a835db79ff" },
22
+ { "tag": "v3.4.1", "commit": "0b41434f6422de62a8e06e48998a0699e3f623d2" },
23
+ { "tag": "v3.4.2", "commit": "c0d1724f0a5212942b0f457f4a7ae99c5f71da6b" },
24
+ { "tag": "v3.4.3", "commit": "07f98df4b7f737b543e3469ce3792fee2e5e6b81" },
25
+ { "tag": "v3.5.0", "commit": "4ffc9f9d7a5eeea006acddcdb259299fcc56354d" },
26
+ { "tag": "v3.6.0", "commit": "9f59f16143d7c6cc877778e4ad6620e2fc0cd829" },
27
+ { "tag": "v3.7.0", "commit": "4901adc14036c25d4ed98a7eac8c8e7b15b6a048" },
28
+ { "tag": "v3.8.0", "commit": "697795c87b0296d037ae76ee1984ce0eaa6684fb" },
29
+ { "tag": "v3.9.0", "commit": "a5c272bd19b563bd4e3da5db017e996eb0f854d0" }
30
+ ]
31
+ }
32
+ ]
33
+ }
package/scripts/README.md CHANGED
@@ -67,10 +67,11 @@ renamed, or changes category, update the table and the validator together.
67
67
  | `workflow-steering.js` | policy hook | `evals/integration/test_workflow_steering_hook.sh` | Provides workflow guidance from current artifact state. |
68
68
  | `pre-commit-quality.js` | repo guardrail hook | `evals/integration/test_hook_category_behaviors.sh` | Supports repository Git hook checks, not installed runtime hooks. |
69
69
  | `desktop-notify.sh` | local notification helper | `evals/integration/test_hook_category_behaviors.sh` | Optional local desktop notification helper. |
70
- | `lib/actor-identity.js` | shared hook library | `evals/integration/test_actor_identity.sh` | Shared runtime-agnostic actor identity resolver (`resolveActor`) consumed by `workflow-steering.js` and `workflow-sidecar.js` to retire the shared `"local"` liveness-actor default (issue #287). |
70
+ | `lib/actor-identity.js` | shared hook library | `evals/integration/test_actor_identity.sh` | Canonical runtime-agnostic actor resolver (`resolveActor`), including privacy-safe Codex thread identity and explicitly unstable process-ancestry fallback. TypeScript consumers load this file directly; there is no maintained mirror. |
71
71
  | `lib/audit-transport.sh` | shared hook library | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Shared audit event transport functions. |
72
72
  | `lib/codex-exit-code.js` | shared hook library | `evals/acceptance/prove-capture-teeth.sh` | Shared codex-only host-banner exit-code extraction (`extractExitCodeFromBanner`, `readExitCodeFromRollout`) — parses the `Process exited with code N` prose the codex CLI writes into a `function_call_output` payload or session rollout so `codex-hook-adapter.js` can inject a structured exit code before `evidence-capture.js` observes (issue #470). |
73
73
  | `lib/config-protection-remedies.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Sanctioned-remedy table for `config-protection.js` (WS8/ADR 0020 extraction). |
74
+ | `lib/continuation-turn-authority.js` | shared hook library | `src/cli/continuation-driver.test.mjs`, `evals/integration/test_goal_fit_hook.sh` | Signed transient authority for one public continuation adapter turn, independently anchored by the signer digest in driver mission state. Assignment-gated evidence mutation can fall back to its live signed assignment identity only after ordinary resolution mismatches; lifecycle controls never use it. Stop keeps a base-valid signed run selected across canonical dispositions, validates canonical state against its definition, and treats only ordinary active-gate warnings as advisory. Same-UID filesystem/process attacks remain outside the cooperative boundary. |
74
75
  | `lib/current-pointer.js` | shared hook library | `evals/integration/test_current_json_per_actor.sh` | Shared per-actor "current" pointer reader/writer (`readCurrentPointer`, `writePerActorCurrent`) — the compat-shim fallback (per-actor `current/<actor>.json` first, legacy global `current.json` fallback) every actor-aware consumer routes through (issue #291). |
75
76
  | `lib/hook-flags.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Shared profile/disable flag parsing. |
76
77
  | `lib/kit-catalog.js` | shared hook library | `evals/integration/test_workflow_steering_hook.sh` | Build-free kit catalog reader used by `workflow-steering.js` to load kit-declared workflow triggers while hooks remain fail-open and independent of compiled TypeScript output. |
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const cp = require("node:child_process");
5
+ const crypto = require("node:crypto");
6
+ const fs = require("node:fs");
7
+
8
+ function git(...args) { return cp.execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
9
+ function show(tag, file) { return git("show", `${tag}:${file}`); }
10
+ function payloadAt(tag) {
11
+ const manifest = JSON.parse(show(tag, "packaging/manifest.json"));
12
+ const excluded = new Set(manifest.codex?.excluded_agents ?? []);
13
+ const names = git("ls-tree", "--name-only", `${tag}:agents`).trim().split(/\n/).filter((name) => name.endsWith(".json"));
14
+ const agents = names.map((name) => JSON.parse(show(tag, `agents/${name}`)))
15
+ .filter((agent) => !excluded.has(agent.name)).sort((a, b) => a.name.localeCompare(b.name));
16
+ const summary = agents.map((agent) => `- \`${agent.name}\` — ${String(agent.description ?? "").trim()}`).join("\n");
17
+ return Buffer.from(`# Universal Agent Bundle (Codex)\n\nThis bundle was generated from the canonical source in this repo. Treat the repo root as the source of truth and regenerate the bundle instead of editing exported agent files by hand.\n\n## Shared Conventions\n\n- \`skills/\`, \`context/\`, \`powers/\`, \`prompts/\`, \`scripts/\`, and \`evals/\` were copied from the canonical source.\n- Cross-session task artifacts should live under \`${manifest.codex.task_dir}\`.\n- Kiro-only hook wiring was stripped from exported non-Kiro agents to keep the package portable.\n\n## Exported Agents\n\n${summary}\n`);
18
+ }
19
+
20
+ const catalogPath = process.argv[2];
21
+ if (!catalogPath) throw new Error("usage: audit-codex-legacy-agents.js CATALOG");
22
+ const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
23
+ const tags = git("tag", "--sort=version:refname").trim().split(/\n/).filter(Boolean);
24
+ const discovered = [];
25
+ for (const tag of tags) {
26
+ let installer;
27
+ try { installer = show(tag, "scripts/install-codex-home.sh"); } catch { continue; }
28
+ if (!/for seed_file in AGENTS\.md/.test(installer)) continue;
29
+ const bytes = payloadAt(tag);
30
+ discovered.push({ tag, commit: git("rev-list", "-n1", tag).trim(), bytes: bytes.length, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), payload: bytes });
31
+ }
32
+ const covered = new Map();
33
+ for (const file of catalog.files ?? []) for (const release of file.covered_releases ?? []) covered.set(release.tag, { ...release, file });
34
+ for (const release of discovered) {
35
+ const entry = covered.get(release.tag);
36
+ if (!entry) throw new Error(`catalog omits seed-capable release ${release.tag}`);
37
+ if (entry.commit !== release.commit || entry.file.sha256 !== release.sha256 || entry.file.bytes !== release.bytes) throw new Error(`catalog mismatch for ${release.tag}`);
38
+ if (!entry.file.fixture || !fs.existsSync(entry.file.fixture) || !release.payload.equals(fs.readFileSync(entry.file.fixture))) throw new Error(`fixture mismatch for ${release.tag}`);
39
+ }
40
+ for (const tag of covered.keys()) if (!discovered.some((release) => release.tag === tag)) throw new Error(`catalog includes non-seed-capable or unreachable release ${tag}`);
41
+ if (catalog.audit?.first_tag !== discovered[0]?.tag || catalog.audit?.last_tag !== discovered.at(-1)?.tag) throw new Error("catalog audit interval does not match independently discovered releases");
42
+ if (!Array.isArray(catalog.audit?.gaps)) throw new Error("catalog must explicitly record reproduction gaps");
43
+ console.log(`Audited ${discovered.length} seed-capable releases; ${new Set(discovered.map((item) => item.sha256)).size} unique payload(s); gaps=${catalog.audit.gaps.length}`);
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const crypto = require("node:crypto");
5
+ const fs = require("node:fs");
6
+ const path = require("node:path");
7
+
8
+ function shellQuote(value) { return `'${String(value).replace(/'/g, `'"'"'`)}'`; }
9
+ function advisory(message) { console.error(`classify-codex-legacy-agents: ${message}`); }
10
+
11
+ const [destinationArg, catalogArg] = process.argv.slice(2);
12
+ if (!destinationArg || !catalogArg) {
13
+ advisory("usage: classify-codex-legacy-agents.js DEST CATALOG");
14
+ process.exit(2);
15
+ }
16
+ const destination = path.resolve(destinationArg);
17
+ const source = path.join(destination, "AGENTS.md");
18
+ let stat;
19
+ try { stat = fs.lstatSync(source); }
20
+ catch (error) {
21
+ if (error?.code === "ENOENT") process.exit(0);
22
+ advisory(`cannot inspect ${source}; preserved without changes: ${error.message}`);
23
+ process.exit(2);
24
+ }
25
+ if (stat.isSymbolicLink() || !stat.isFile()) {
26
+ advisory(`preserved ambiguous non-regular or symlink object without changes: ${source}`);
27
+ process.exit(0);
28
+ }
29
+ if (fs.constants.O_NOFOLLOW === undefined) {
30
+ advisory(`platform lacks O_NOFOLLOW; preserved without classification: ${source}`);
31
+ process.exit(2);
32
+ }
33
+ let fd;
34
+ let bytes;
35
+ try {
36
+ fd = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
37
+ const opened = fs.fstatSync(fd);
38
+ if (!opened.isFile()) {
39
+ advisory(`preserved non-regular descriptor without changes: ${source}`);
40
+ process.exit(0);
41
+ }
42
+ bytes = fs.readFileSync(fd);
43
+ } catch (error) {
44
+ advisory(`cannot open/read ${source} without following links; preserved without changes: ${error.message}`);
45
+ process.exit(2);
46
+ } finally {
47
+ if (fd !== undefined) fs.closeSync(fd);
48
+ }
49
+ const digest = crypto.createHash("sha256").update(bytes).digest("hex");
50
+ const catalog = JSON.parse(fs.readFileSync(catalogArg, "utf8"));
51
+ const match = (catalog.files ?? []).find((entry) => entry.sha256 === digest && entry.bytes === bytes.length);
52
+ if (!match) {
53
+ advisory(`preserved unrecognized or user-owned ${source} (sha256=${digest}, bytes=${bytes.length})`);
54
+ process.exit(0);
55
+ }
56
+ const releases = (match.covered_releases ?? []).map((item) => `${item.tag}@${item.commit}`).join(", ");
57
+ advisory(`REFUSING INSTALL: exact historical Flow Agents-generated global instructions remain active`);
58
+ advisory(`path=${source}`);
59
+ advisory(`sha256=${digest} bytes=${bytes.length}`);
60
+ advisory(`matching_releases=${releases}`);
61
+ advisory("automatic mutation is disabled because portable Node cannot condition pathname removal on the inode that was classified");
62
+ advisory(`evidence command (hash): shasum -a 256 -- ${shellQuote(source)}`);
63
+ advisory(`evidence command (bytes): wc -c < ${shellQuote(source)}`);
64
+ advisory("manual remediation checklist:");
65
+ advisory("1. Stop all agent and writer processes that can modify the Codex home.");
66
+ advisory(`2. Revalidate the file is still exactly sha256=${digest} and bytes=${bytes.length} using the evidence commands above.`);
67
+ advisory("3. Choose a new, unused recovery path outside every agent instruction-discovery location.");
68
+ advisory("4. Move the file manually without overwrite using operator-trusted tooling; no mutation command is emitted here.");
69
+ advisory("5. Inspect the preserved recovery file and confirm its exact hash and byte count.");
70
+ advisory("6. Rerun the Flow Agents Codex installer.");
71
+ process.exit(3);
@@ -57,8 +57,8 @@
57
57
  const fs = require('fs');
58
58
  const path = require('path');
59
59
  const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
60
- const { resolveActor } = require('./lib/actor-identity.js');
61
- const { readCurrentPointer } = require('./lib/current-pointer.js');
60
+ const { resolveActor, isUnresolvedActor } = require('./lib/actor-identity.js');
61
+ const { readOwnCurrentPointer } = require('./lib/current-pointer.js');
62
62
  const { isAmbiguousAbsenceCommand } = require('./lib/runnable-command.js');
63
63
  const crypto = require('crypto');
64
64
 
@@ -204,14 +204,19 @@ function latestStateDir(flowAgentsDir) {
204
204
  }
205
205
 
206
206
  /**
207
- * Resolve the active artifact directory the same way the other hooks do:
208
- * prefer .kontourai/flow-agents/current.json (active_slug / artifact_dir), then fall back
209
- * to the newest-mtime state.json directory.
207
+ * Resolve the active artifact directory the same way the other #440-migrated hooks do: prefer
208
+ * the RESOLVED actor's own per-actor `current/<actor>.json` pointer (active_slug / artifact_dir)
209
+ * via `readOwnCurrentPointer` — never the shared legacy `current.json`, and never the repo-wide
210
+ * newest-mtime scan (D1: that would append this actor's OWN captured evidence into an unrelated
211
+ * actor's session directory). Only for an empty/unresolved actor does this fall back to the
212
+ * legacy global `current.json` and, failing that, the newest-mtime `state.json` directory scan —
213
+ * the pre-#440/#291 behavior, unchanged for that case (D3 compat). See the D1/D2/D3 comment
214
+ * inline below for the exact branching.
210
215
  */
211
216
  function resolveArtifactDir(root) {
212
217
  const actorKey = resolveActor(process.env).actor;
213
218
  for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
214
- const { payload: current } = readCurrentPointer(flowAgentsDir, actorKey);
219
+ const { payload: current } = readOwnCurrentPointer(flowAgentsDir, actorKey);
215
220
  if (current) {
216
221
  const slug = current.artifact_dir || current.active_slug;
217
222
  if (typeof slug === 'string' && slug.trim()) {
@@ -222,6 +227,13 @@ function resolveArtifactDir(root) {
222
227
  }
223
228
  }
224
229
  }
230
+ // #440 D1: a resolved actor with no own per-actor pointer never falls back to the repo-wide
231
+ // newest-mtime scan below — that would append this actor's OWN captured evidence into an
232
+ // unrelated actor's session directory (write-side ownership conflation). D2 accepted gap:
233
+ // captured evidence is simply dropped (existing `if (!artifactDir) return rawInput;` no-op in
234
+ // run()) until this actor's next sidecar command establishes its own per-actor pointer. D3: an
235
+ // unresolved actor keeps today's exact global-scan fallback, unchanged.
236
+ if (!isUnresolvedActor(actorKey)) return null;
225
237
  for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
226
238
  const latest = latestStateDir(flowAgentsDir);
227
239
  if (latest) return latest;