@kontourai/flow-agents 3.2.0 → 3.4.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 (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -18,8 +18,9 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
20
20
  const { resolveActor } = require('./lib/actor-identity');
21
- const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
21
+ const { flowAgentsArtifactRootsForRead, resolveSharedRepoRoot, warnIfFailingOpenInsideGitTree } = require('./lib/local-artifact-paths');
22
22
  const { readCurrentPointer } = require('./lib/current-pointer');
23
+ const { workflowTriggersFor } = require('./lib/kit-catalog');
23
24
 
24
25
  const STEERING = {
25
26
  'tool-planner': [
@@ -71,14 +72,36 @@ const ACTIVE_STATE_STATUSES = new Set([
71
72
  ]);
72
73
 
73
74
  function findRepoRoot(startDir) {
74
- let dir = path.resolve(startDir || process.cwd());
75
+ const resolvedStartDir = path.resolve(startDir || process.cwd());
76
+ // #357: prefer git's SHARED common-dir root (the primary checkout's repo root, resolved
77
+ // identically from any linked worktree) before falling back to the plain `.git`/AGENTS.md
78
+ // ancestor walk below. Without this, a worktree's OWN `.git` FILE (not the shared `.git`
79
+ // DIRECTORY) satisfies `fs.existsSync(path.join(dir, '.git'))` at the very first directory
80
+ // checked, so the walk always stopped at the worktree root instead of resolving to the
81
+ // shared primary checkout — see src/lib/local-artifact-root.ts's resolveSharedRepoRoot doc
82
+ // comment for the full rationale. Fails open to the walk below (unchanged) when git is
83
+ // unavailable, `resolvedStartDir` is not inside a git working tree, or the command fails —
84
+ // this is also exactly today's behavior in the single-checkout case, where
85
+ // `--git-common-dir` resolves to `.git` under `resolvedStartDir` itself.
86
+ const sharedRoot = resolveSharedRepoRoot(resolvedStartDir);
87
+ if (sharedRoot) return sharedRoot;
88
+ let dir = resolvedStartDir;
75
89
  const root = path.parse(dir).root;
76
90
  for (let depth = 0; dir && depth < 40; depth++) {
77
- if (fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, 'AGENTS.md'))) return dir;
91
+ if (fs.existsSync(path.join(dir, '.git'))) {
92
+ // #413 iteration-2 Fix 1: resolveSharedRepoRoot failed above even though a `.git` entry
93
+ // exists right here — git resolution was ATTEMPTED and FAILED (corrupted gitlink, bad
94
+ // GIT_DIR, git unavailable, etc.), not merely "no git repo at all". Loud, not silent —
95
+ // see warnIfFailingOpenInsideGitTree's own doc comment for the full rationale. This walk
96
+ // still returns `dir` (the ancestor-walk fallback is preserved unchanged) after warning.
97
+ warnIfFailingOpenInsideGitTree(resolvedStartDir, dir);
98
+ return dir;
99
+ }
100
+ if (fs.existsSync(path.join(dir, 'AGENTS.md'))) return dir;
78
101
  if (dir === root) break;
79
102
  dir = path.dirname(dir);
80
103
  }
81
- return path.resolve(startDir || process.cwd());
104
+ return resolvedStartDir;
82
105
  }
83
106
 
84
107
  function walkStateFiles(dir, out = []) {
@@ -159,8 +182,25 @@ function latestWorkflowState(root) {
159
182
  return states[0] || null;
160
183
  }
161
184
 
185
+ // #293 AC7 (injection discipline): strips C0 (0x00-0x1F), DEL (0x7F), and C1 (0x80-0x9F,
186
+ // which includes ANSI-CSI-adjacent bytes) BEFORE whitespace-collapse/truncation — mirroring
187
+ // src/cli/workflow-sidecar.ts's stripControlCharsForDisplay / assignment-provider.ts's
188
+ // sanitizeDisplayField exactly (same charset, same rationale: holder/actor/assignee strings
189
+ // sourced from the shared multi-writer liveness stream or an attacker-postable GitHub comment
190
+ // are untrusted display input). Previously this function only collapsed whitespace, so a raw
191
+ // ESC/BEL byte embedded in a hostile actor string passed through unstripped into every steering
192
+ // notice that calls safeStateText (resumeSteering's LIVENESS WARNING block, supersessionSteering's
193
+ // SUPERSEDED notice) — fixed here, in the one shared helper, rather than in each call site.
162
194
  function safeStateText(value, maxLength = 240) {
163
- const text = String(value || '').replace(/\s+/g, ' ').trim();
195
+ // Whitespace-collapse FIRST (so an embedded newline/tab becomes a joining space, preserving
196
+ // the existing multiline-summary neutralization behavior), THEN strip control/ANSI bytes
197
+ // (#293 AC7) — reversing this order would strip \n before the collapse ever sees it, losing
198
+ // the join-space between sentences. Only C0/DEL/C1 bytes THAT SURVIVE the collapse (ESC, BEL,
199
+ // and other non-whitespace control bytes — real \s already became a plain space above) are
200
+ // stripped, mirroring src/cli/workflow-sidecar.ts's stripControlCharsForDisplay /
201
+ // assignment-provider.ts's sanitizeDisplayField charset exactly.
202
+ const collapsed = String(value || '').replace(/\s+/g, ' ').trim();
203
+ const text = collapsed.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
164
204
  if (text.length <= maxLength) return text;
165
205
  return `${text.slice(0, maxLength - 3)}...`;
166
206
  }
@@ -227,6 +267,9 @@ function stateSteering(root) {
227
267
  `STATE: ${state.task_slug || path.basename(path.dirname(current.file))} is status:${state.status} phase:${state.phase}.`,
228
268
  ];
229
269
  if (next.summary) parts.push(`Recorded next_action.summary: "${safeStateText(next.summary)}"`);
270
+ if (Array.isArray(next.skills) && next.skills.length) parts.push(`Required skills: ${next.skills.map(skill => safeStateText(skill, 80)).join(' -> ')}.`);
271
+ if (Array.isArray(next.operations) && next.operations.length) parts.push(`Required operations: ${next.operations.map(operation => safeStateText(operation, 80)).join(' -> ')}.`);
272
+ if (next.command) parts.push(`Run: ${safeStateText(next.command, 240)}`);
230
273
  if (next.target_phase) parts.push(`Target phase: ${safeStateText(next.target_phase, 80)}.`);
231
274
  if (next.status === 'needs_user' || state.status === 'needs_decision' || state.status === 'not_verified') {
232
275
  parts.push('Do not deliver as complete until the user decision or accepted gap is recorded.');
@@ -248,6 +291,52 @@ function contextMapSteering(root) {
248
291
  ].join(' ');
249
292
  }
250
293
 
294
+ /**
295
+ * Compose the SessionStart advisory line for installed-skill drift (#439). Fires only inside a
296
+ * kit-bearing checkout (repo has `kits/` AND a built `dist/claude-code/.claude/skills` bundle) —
297
+ * never triggers a build itself, staying read-only and cheap on every SessionStart. Returns ''
298
+ * (never throws past this function) in every other case: not kit-bearing, no manifest yet, no
299
+ * drift found, or any internal error.
300
+ *
301
+ * Reuses the SINGLE shared classification choke point, `scripts/hooks/lib/skill-drift.js`'s
302
+ * `compareSkillDrift()` — this function must never hand-roll its own drift comparison (same
303
+ * convention the CLI check and the manifest writer follow, see #439 plan).
304
+ *
305
+ * @param {string} root Repository root
306
+ * @returns {string}
307
+ */
308
+ function skillDriftSteering(root) {
309
+ try {
310
+ if (!fs.existsSync(path.join(root, 'kits'))) return '';
311
+ if (!fs.existsSync(path.join(root, 'dist', 'claude-code', '.claude', 'skills'))) return '';
312
+
313
+ const { loadManifest, compareSkillDrift, resolveClaudeGlobalSkillsDir } = require('./lib/skill-drift');
314
+
315
+ const destDir = resolveClaudeGlobalSkillsDir(process.env);
316
+ // Sibling layout: <destDir>/.flow-agents/skills-manifest.json — mirrors
317
+ // src/lib/local-artifact-root.ts's durableFlowAgentsRoot(destDir)/skillsManifestPath exactly
318
+ // (durableFlowAgentsRoot(cwd) resolves to `${cwd}/.flow-agents`, no parent traversal).
319
+ const manifestPath = path.join(destDir, '.flow-agents', 'skills-manifest.json');
320
+ const manifest = loadManifest(manifestPath);
321
+
322
+ const installedDir = path.join(destDir, 'skills');
323
+ const kitSourceDir = path.join(root, 'dist', 'claude-code', '.claude', 'skills');
324
+ const report = compareSkillDrift({ installedDir, kitSourceDir, manifest });
325
+ if (!report.hasDrift) return '';
326
+
327
+ const { kitUpdated, userModified, unbaselined, missingInstall, kitRemoved } = report.summary;
328
+ // kit_removed (#439 review fix) folds into staleCount alongside the other non-in_sync
329
+ // states — this advisory line only ever surfaces kit-updated/user-modified counts by name
330
+ // (see the template string below), so kit_removed does not get its own named count here; it
331
+ // is still reflected in the total. Full per-state detail is always available via
332
+ // `flow-agents skill-drift-check`.
333
+ const staleCount = kitUpdated + userModified + unbaselined + missingInstall + kitRemoved;
334
+ return `[SKILL DRIFT] ${staleCount} installed Claude Code skill file(s) are stale vs this kit (${kitUpdated} kit-updated, ${userModified} user-modified). Run \`flow-agents init --runtime claude-code --global\` to refresh (user-modified files are reported, not overwritten). See \`flow-agents skill-drift-check\` for details.`;
335
+ } catch {
336
+ return '';
337
+ }
338
+ }
339
+
251
340
  function promptText(input) {
252
341
  const candidates = [
253
342
  input && input.prompt,
@@ -262,7 +351,7 @@ function promptText(input) {
262
351
  return '';
263
352
  }
264
353
 
265
- function looksLikeBuilderWork(text) {
354
+ function looksLikeImplementationWork(text) {
266
355
  const normalized = safeStateText(text, 2000).toLowerCase();
267
356
  if (!normalized) return false;
268
357
  const readOnlyIntent = /\b(read[- ]only|review[- ]only|no (source |code |file )?(changes?|edits?|modifications?)|do not (modify|edit|change|write|fix|implement|touch)|don't (modify|edit|change|write|fix|implement|touch)|dont (modify|edit|change|write|fix|implement|touch))\b/.test(normalized);
@@ -281,17 +370,29 @@ function looksLikeBuilderWork(text) {
281
370
  return hasWorkVerb || (hasWorkObject && hasRequestLanguage);
282
371
  }
283
372
 
284
- function builderWorkflowSteering(input) {
285
- if (!looksLikeBuilderWork(promptText(input))) return '';
286
- return [
287
- 'BUILDER WORKFLOW ROUTE: this user prompt looks like coding/build work.',
288
- 'Builder lifecycle: shape raw ideas -> build selected work -> publish verified branch/PR -> learn correction feedback.',
289
- 'Before source edits or implementation commands, activate Builder Kit delivery workflow.',
290
- 'If the user explicitly requested TDD, activate `tdd-workflow`; otherwise activate `deliver` and keep the session on `builder.build`.',
291
- 'Use `npm run workflow:sidecar -- ensure-session --flow-id builder.build ...` when the repo provides the sidecar writer.',
292
- 'After local verification, continue to publish/release-readiness and learning-review; do not treat local verification as terminal delivery.',
293
- 'Do not bypass plan-work -> execute-plan -> review-work -> verify-work for coding tasks; direct implementation is only acceptable when the user explicitly asks for no workflow or explanation-only help.',
294
- ].join(' ');
373
+ function looksLikeKnowledgeWork(text) {
374
+ const normalized = safeStateText(text, 2000).toLowerCase();
375
+ if (!normalized) return false;
376
+ return /\b(remember|capture|save|file|bookmark)\s+(this|that|the|my|our)\b/.test(normalized) ||
377
+ /\bcapture\s+(this\s+)?(decision|learning|lesson|note|context)\b/.test(normalized) ||
378
+ /\bwhat did we learn about\b/.test(normalized);
379
+ }
380
+
381
+ function kitWorkflowSteering(input, root) {
382
+ const prompt = promptText(input);
383
+ const categories = [];
384
+ if (looksLikeImplementationWork(prompt)) categories.push('implementation-work-detected');
385
+ if (looksLikeKnowledgeWork(prompt)) categories.push('knowledge-capture-detected');
386
+ if (!categories.length) return '';
387
+ try {
388
+ return categories
389
+ .flatMap(category => workflowTriggersFor(root, category))
390
+ .map(trigger => trigger.steering)
391
+ .filter(Boolean)
392
+ .join('\n');
393
+ } catch {
394
+ return '';
395
+ }
295
396
  }
296
397
 
297
398
  /**
@@ -324,6 +425,13 @@ function resumeSteering(root, current) {
324
425
  // Full next action (240-char display path, not the 80-char normalization)
325
426
  const nextSummary = next.summary ? safeStateText(next.summary, 240) : 'none';
326
427
  lines.push(`Next action: ${nextSummary}`);
428
+ if (Array.isArray(next.skills) && next.skills.length) {
429
+ lines.push(`Required skills: ${next.skills.map(skill => safeStateText(skill, 80)).join(' -> ')}`);
430
+ }
431
+ if (Array.isArray(next.operations) && next.operations.length) {
432
+ lines.push(`Required operations: ${next.operations.map(operation => safeStateText(operation, 80)).join(' -> ')}`);
433
+ }
434
+ if (next.command) lines.push(`Run: ${safeStateText(next.command, 240)}`);
327
435
 
328
436
  // Plan artifact path
329
437
  let planPath = 'not found';
@@ -411,6 +519,51 @@ function resumeSteering(root, current) {
411
519
  }
412
520
  }
413
521
 
522
+ /**
523
+ * Compose the every-turn SUPERSEDED steering notice (issue #293).
524
+ *
525
+ * Unlike resumeSteering()'s RESUME block (SessionStart only), this notice must
526
+ * surface on EVERY turn (UserPromptSubmit as well as SessionStart) once another
527
+ * actor holds a fresh liveness claim on the caller's own subject — the
528
+ * "don't publish, you may be stale" signal the verify-hold gate backstops at
529
+ * publish time. Reuses the SAME liveness-only join resumeSteering()'s liveness
530
+ * advisory block already performs (freshHolders(events, slug, selfActor, now)),
531
+ * not a second computation.
532
+ *
533
+ * freshHolders() already excludes selfActor internally (see
534
+ * scripts/hooks/lib/liveness-read.js), so any holder returned here is by
535
+ * definition another actor — no additional self-filter is required.
536
+ *
537
+ * Fully fail-open: any error anywhere in this function returns '' rather than
538
+ * throwing, matching every other steering helper in this file.
539
+ *
540
+ * @param {string} root Repository root
541
+ * @param {{ file: string, payload: object }} current Latest active state entry (this actor's own)
542
+ * @returns {string}
543
+ */
544
+ function supersessionSteering(root, current) {
545
+ try {
546
+ const state = current.payload;
547
+ const workflowDir = path.dirname(current.file);
548
+ const slug = state.task_slug || path.basename(workflowDir);
549
+
550
+ const resolved = resolveActor(process.env);
551
+ const selfActor = resolved.actor || 'local';
552
+
553
+ const events = flowAgentsArtifactRootsForRead(root)
554
+ .flatMap(artifactRoot => readLivenessEvents(path.join(artifactRoot, 'liveness', 'events.jsonl')));
555
+ if (events.length === 0) return '';
556
+
557
+ const holders = freshHolders(events, slug, selfActor, Date.now());
558
+ if (holders.length === 0) return '';
559
+
560
+ const holder = holders[0];
561
+ return `[SUPERSEDED: another agent appears live on this work: actor ${safeStateText(holder.actor)}, last seen ${holder.lastAt}. Your claim on this subject may be stale — do not publish until you re-verify hold (see verify-hold gate) or hand off.]`;
562
+ } catch {
563
+ return '';
564
+ }
565
+ }
566
+
414
567
  function run(rawInput) {
415
568
  try {
416
569
  const input = JSON.parse(rawInput);
@@ -443,18 +596,35 @@ function run(rawInput) {
443
596
  }
444
597
 
445
598
  if (event === 'UserPromptSubmit') {
446
- const builderHint = builderWorkflowSteering(input);
447
- if (builderHint) {
448
- hints.push(builderHint);
599
+ const kitHint = kitWorkflowSteering(input, root);
600
+ if (kitHint) {
601
+ hints.push(kitHint);
449
602
  const contextHint = contextMapSteering(root);
450
603
  if (contextHint) hints.push(contextHint);
451
604
  }
452
605
  }
453
606
 
607
+ // Every-turn supersession notice (#293): fires on UserPromptSubmit (every turn),
608
+ // not just SessionStart, so a takeover mid-session keeps surfacing the warning.
609
+ if (event === 'UserPromptSubmit' && current) {
610
+ const supersessionHint = supersessionSteering(root, current);
611
+ if (supersessionHint) hints.push(supersessionHint);
612
+ }
613
+
454
614
  // SessionStart only: append the RESUME block for richer situational awareness
455
615
  if (event === 'SessionStart' && current) {
456
616
  const resumeBlock = resumeSteering(root, current);
457
617
  if (resumeBlock) hints.push(resumeBlock);
618
+ const supersessionHint = supersessionSteering(root, current);
619
+ if (supersessionHint) hints.push(supersessionHint);
620
+ }
621
+
622
+ // SessionStart only, unconditional of `current` (#439): the installed-skill drift advisory
623
+ // fires on every SessionStart inside a kit-bearing checkout, independent of whether an active
624
+ // workflow session exists — do NOT fold this into the `current`-gated block above.
625
+ if (event === 'SessionStart') {
626
+ const driftHint = skillDriftSteering(root);
627
+ if (driftHint) hints.push(driftHint);
458
628
  }
459
629
 
460
630
  if (shouldAppendWorkflowContext) {
@@ -487,12 +657,14 @@ module.exports = {
487
657
  stateSteering,
488
658
  critiqueSteering,
489
659
  contextMapSteering,
660
+ skillDriftSteering,
490
661
  latestWorkflowState,
491
662
  findRepoRoot,
492
663
  safeStateText,
493
664
  stateNeedsAmbientSteering,
494
665
  resumeSteering,
666
+ supersessionSteering,
495
667
  promptText,
496
- looksLikeBuilderWork,
497
- builderWorkflowSteering,
668
+ looksLikeImplementationWork,
669
+ kitWorkflowSteering,
498
670
  };
@@ -51,6 +51,41 @@ while [[ $# -gt 0 ]]; do
51
51
  esac
52
52
  done
53
53
  REAL_CODEX_HOME="${CODEX_REAL_HOME:-$HOME/.codex}"
54
+
55
+ canonicalize_path() {
56
+ node - "$1" <<'NODE'
57
+ const fs = require("node:fs");
58
+ const path = require("node:path");
59
+ let current = path.resolve(process.argv[2]);
60
+ const missing = [];
61
+ while (!fs.existsSync(current)) {
62
+ missing.unshift(path.basename(current));
63
+ const parent = path.dirname(current);
64
+ if (parent === current) break;
65
+ current = parent;
66
+ }
67
+ process.stdout.write(path.resolve(fs.realpathSync(current), ...missing));
68
+ NODE
69
+ }
70
+
71
+ [[ ! -L "$DEST" ]] || { echo "install-codex-home.sh: refusing symlink destination root: $DEST" >&2; exit 1; }
72
+ ROOT_REAL="$(canonicalize_path "$ROOT_DIR")"
73
+ BUNDLE_SOURCE_REAL="$(canonicalize_path "$ROOT_DIR/dist/codex")"
74
+ DEST_REAL="$(canonicalize_path "$DEST")"
75
+ REAL_CODEX_HOME_REAL="$(canonicalize_path "$REAL_CODEX_HOME")"
76
+ case "$DEST_REAL/" in
77
+ "$ROOT_REAL/"*|"$BUNDLE_SOURCE_REAL/"*)
78
+ echo "install-codex-home.sh: destination overlaps Flow Agents source: $DEST_REAL" >&2
79
+ exit 1
80
+ ;;
81
+ esac
82
+ case "$ROOT_REAL/" in
83
+ "$DEST_REAL/"*)
84
+ echo "install-codex-home.sh: Flow Agents source overlaps destination: $DEST_REAL" >&2
85
+ exit 1
86
+ ;;
87
+ esac
88
+
54
89
  if command -v npm >/dev/null 2>&1; then
55
90
  (cd "$ROOT_DIR" && FLOW_AGENTS_EXPORT_DIAGNOSTICS=0 npm run build:bundles --silent >/dev/null)
56
91
  else
@@ -59,7 +94,8 @@ else
59
94
  fi
60
95
 
61
96
  mkdir -p "$DEST"
62
- DEST_REAL="$(cd "$DEST" && pwd -P)"
97
+ DEST="$(cd "$DEST" && pwd -P)"
98
+ DEST_REAL="$DEST"
63
99
 
64
100
  assert_safe_dest_path() {
65
101
  local rel="$1"
@@ -95,10 +131,17 @@ if [[ -f "$DEST/hooks.json" ]]; then
95
131
  cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
96
132
  fi
97
133
 
98
- # A real Codex home can contain user-owned config, profiles, auth, hooks, and
99
- # locally installed kits. Update Flow Agents-owned bundle trees in place without
100
- # deleting that state. `kits/local` is user/runtime state; everything else under
101
- # the generated bundle paths is managed by this installer.
134
+ # A real Codex home can contain user-owned files in every shared directory.
135
+ # Prepare an exact Flow Agents overlay, then synchronize only files recorded in
136
+ # the ownership manifest. The synchronizer never uses directory-wide --delete:
137
+ # it removes only unchanged files from its prior manifest and refuses collisions.
138
+ FA_OWNED_OVERLAY="$(mktemp -d /tmp/fa-codex-overlay.XXXXXX)"
139
+ cleanup_install_temps() {
140
+ rm -rf "$FA_OWNED_OVERLAY"
141
+ [[ -z "${FA_USER_HOOKS_STASH:-}" ]] || rm -f "$FA_USER_HOOKS_STASH"
142
+ }
143
+ trap cleanup_install_temps EXIT
144
+
102
145
  for managed_dir in \
103
146
  .flow-agents \
104
147
  agent-cards \
@@ -115,52 +158,65 @@ for managed_dir in \
115
158
  do
116
159
  if [[ -d "$ROOT_DIR/dist/codex/$managed_dir" ]]; then
117
160
  assert_safe_dest_path "$managed_dir"
118
- mkdir -p "$DEST/$managed_dir"
119
- rsync -a --delete "$ROOT_DIR/dist/codex/$managed_dir/" "$DEST/$managed_dir/"
161
+ mkdir -p "$FA_OWNED_OVERLAY/$managed_dir"
162
+ rsync -a "$ROOT_DIR/dist/codex/$managed_dir/" "$FA_OWNED_OVERLAY/$managed_dir/"
120
163
  fi
121
164
  done
122
165
 
123
166
  # Skills are user-extensible in a real Codex home. Merge both bundle skill
124
167
  # layers into the flattened destination without deleting user-owned skills.
125
168
  if [[ -d "$ROOT_DIR/dist/codex/skills" ]]; then
126
- assert_safe_dest_path "skills"
127
- mkdir -p "$DEST/skills"
128
- rsync -a "$ROOT_DIR/dist/codex/skills/" "$DEST/skills/"
169
+ mkdir -p "$FA_OWNED_OVERLAY/skills"
170
+ rsync -a "$ROOT_DIR/dist/codex/skills/" "$FA_OWNED_OVERLAY/skills/"
129
171
  fi
130
172
 
131
173
  if [[ -d "$ROOT_DIR/dist/codex/.codex/skills" ]]; then
132
- assert_safe_dest_path "skills"
133
- mkdir -p "$DEST/skills"
134
- rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$DEST/skills/"
174
+ mkdir -p "$FA_OWNED_OVERLAY/skills"
175
+ rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$FA_OWNED_OVERLAY/skills/"
135
176
  fi
136
177
 
137
178
  if [[ -d "$ROOT_DIR/dist/codex/kits" ]]; then
138
- assert_safe_dest_path "kits"
139
- mkdir -p "$DEST/kits"
140
- rsync -a --delete --exclude 'local/' "$ROOT_DIR/dist/codex/kits/" "$DEST/kits/"
179
+ mkdir -p "$FA_OWNED_OVERLAY/kits"
180
+ rsync -a --exclude 'local/' "$ROOT_DIR/dist/codex/kits/" "$FA_OWNED_OVERLAY/kits/"
141
181
  fi
142
182
 
143
183
  # Agents are user-extensible in a real Codex home. Merge generated agents
144
184
  # without deleting user-owned agents.
145
185
  if [[ -d "$ROOT_DIR/dist/codex/.codex/agents" ]]; then
146
- assert_safe_dest_path "agents"
147
- mkdir -p "$DEST/agents"
148
- rsync -a "$ROOT_DIR/dist/codex/.codex/agents/" "$DEST/agents/"
186
+ mkdir -p "$FA_OWNED_OVERLAY/agents"
187
+ rsync -a "$ROOT_DIR/dist/codex/.codex/agents/" "$FA_OWNED_OVERLAY/agents/"
149
188
  fi
150
189
 
151
190
  for bundle_file in README.md console.telemetry.json install.sh; do
152
191
  if [[ -f "$ROOT_DIR/dist/codex/$bundle_file" ]]; then
153
- assert_safe_dest_path "$bundle_file"
154
- cp "$ROOT_DIR/dist/codex/$bundle_file" "$DEST/$bundle_file"
192
+ cp "$ROOT_DIR/dist/codex/$bundle_file" "$FA_OWNED_OVERLAY/$bundle_file"
155
193
  fi
156
194
  done
157
195
 
196
+ assert_safe_dest_path ".flow-agents/codex-install-manifest.json"
197
+ node "$ROOT_DIR/scripts/install-owned-files.js" \
198
+ "$FA_OWNED_OVERLAY" "$DEST" ".flow-agents/codex-install-manifest.json"
199
+
200
+ atomic_copy() {
201
+ local source="$1"
202
+ local rel="$2"
203
+ assert_safe_dest_path "$rel"
204
+ local target="$DEST/$rel"
205
+ if [[ -e "$target" && ! -f "$target" ]]; then
206
+ echo "install-codex-home.sh: refusing to replace non-file: $target" >&2
207
+ exit 1
208
+ fi
209
+ local temp
210
+ temp="$(mktemp "$(dirname "$target")/.flow-agents-install.XXXXXX")"
211
+ cp "$source" "$temp"
212
+ mv -f "$temp" "$target"
213
+ }
214
+
158
215
  # Root Codex config/profiles and AGENTS.md may be user-owned in ~/.codex.
159
216
  # Seed Flow Agents defaults only when absent.
160
217
  for seed_file in AGENTS.md; do
161
218
  if [[ -f "$ROOT_DIR/dist/codex/$seed_file" && ! -e "$DEST/$seed_file" ]]; then
162
- assert_safe_dest_path "$seed_file"
163
- cp "$ROOT_DIR/dist/codex/$seed_file" "$DEST/$seed_file"
219
+ atomic_copy "$ROOT_DIR/dist/codex/$seed_file" "$seed_file"
164
220
  fi
165
221
  done
166
222
  generated_profile_files=()
@@ -173,8 +229,7 @@ for seed_source in "$ROOT_DIR/dist/codex/.codex/config.toml" "$ROOT_DIR"/dist/co
173
229
  profile_names+=("${seed_file%.config.toml}")
174
230
  fi
175
231
  if [[ ! -e "$DEST/$seed_file" ]] || grep -q 'Generated from packaging/manifest.json' "$DEST/$seed_file"; then
176
- assert_safe_dest_path "$seed_file"
177
- cp "$seed_source" "$DEST/$seed_file"
232
+ atomic_copy "$seed_source" "$seed_file"
178
233
  fi
179
234
  done
180
235
  for profile_path in "$DEST"/*.config.toml; do
@@ -195,9 +250,8 @@ done
195
250
  rmdir "$DEST/.codex" 2>/dev/null || true
196
251
 
197
252
  for auth_file in auth.json version.json installation_id models_cache.json; do
198
- if [[ "$REAL_CODEX_HOME" != "$DEST" && -f "$REAL_CODEX_HOME/$auth_file" ]]; then
199
- assert_safe_dest_path "$auth_file"
200
- cp "$REAL_CODEX_HOME/$auth_file" "$DEST/$auth_file"
253
+ if [[ "$REAL_CODEX_HOME_REAL" != "$DEST_REAL" && -f "$REAL_CODEX_HOME_REAL/$auth_file" ]]; then
254
+ atomic_copy "$REAL_CODEX_HOME_REAL/$auth_file" "$auth_file"
201
255
  fi
202
256
  done
203
257
 
@@ -212,26 +266,22 @@ chmod 700 "$DEST" 2>/dev/null || true
212
266
  FA_VERSION="$(node -p "require('$ROOT_DIR/package.json').version" 2>/dev/null || echo unknown)"
213
267
  FA_MANAGED_HOOKS="$ROOT_DIR/dist/codex/.codex/hooks.json"
214
268
  if command -v node >/dev/null 2>&1 && [[ -f "$FA_MANAGED_HOOKS" ]]; then
215
- if [[ -n "$FA_USER_HOOKS_STASH" && -f "$FA_USER_HOOKS_STASH" ]]; then
216
- # Merge user's prior hooks (stash) with the current FA managed hooks.
217
- node "$ROOT_DIR/scripts/install-merge.js" \
218
- --config "$FA_USER_HOOKS_STASH" \
219
- --managed-hooks "$FA_MANAGED_HOOKS" \
220
- --version "$FA_VERSION" \
221
- --install-record "$DEST/.flow-agents/install.json" \
222
- --runtime "codex"
223
- # Move the merged result into the destination.
224
- cp "$FA_USER_HOOKS_STASH" "$DEST/hooks.json"
225
- rm -f "$FA_USER_HOOKS_STASH"
226
- else
227
- # No prior user hooks: just write the version stamp (FA hooks are already correct from rsync).
228
- node "$ROOT_DIR/scripts/install-merge.js" \
229
- --config "$DEST/hooks.json" \
230
- --managed-hooks "$FA_MANAGED_HOOKS" \
231
- --version "$FA_VERSION" \
232
- --install-record "$DEST/.flow-agents/install.json" \
233
- --runtime "codex"
269
+ FA_HOOKS_WORK="${FA_USER_HOOKS_STASH:-}"
270
+ if [[ -z "$FA_HOOKS_WORK" ]]; then
271
+ FA_HOOKS_WORK="$(mktemp /tmp/fa-hooks-work.XXXXXX.json)"
272
+ printf '{"hooks":{}}\n' > "$FA_HOOKS_WORK"
234
273
  fi
274
+ FA_INSTALL_RECORD_WORK="$(mktemp /tmp/fa-install-record.XXXXXX.json)"
275
+ node "$ROOT_DIR/scripts/install-merge.js" \
276
+ --config "$FA_HOOKS_WORK" \
277
+ --managed-hooks "$FA_MANAGED_HOOKS" \
278
+ --version "$FA_VERSION" \
279
+ --install-record "$FA_INSTALL_RECORD_WORK" \
280
+ --runtime "codex"
281
+ atomic_copy "$FA_HOOKS_WORK" "hooks.json"
282
+ atomic_copy "$FA_INSTALL_RECORD_WORK" ".flow-agents/install.json"
283
+ rm -f "$FA_HOOKS_WORK" "$FA_INSTALL_RECORD_WORK"
284
+ FA_USER_HOOKS_STASH=""
235
285
  fi
236
286
 
237
287
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then