@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
@@ -23,10 +23,13 @@
23
23
  * flow-agents marker (the COLLISION_MARKER strings from init.ts).
24
24
  * (c) APPEND the current managed hook groups from the bundle.
25
25
  * (d) Preserve ALL other keys (permissions, statusLine, user hooks, auth).
26
+ * Managed non-hook values are added only when absent or when replacing
27
+ * an existing Flow Agents-owned value. Conflicting user-owned values win
28
+ * and are reported to stderr.
26
29
  * (e) Atomic write (write tmp + rename).
27
30
  * (f) Write/update .flow-agents/install.json version stamp.
28
31
  *
29
- * Export: mergeSettings(existing, managed) — pure, testable.
32
+ * Export: mergeSettings(existing, managed, options) — pure, testable.
30
33
  * The managed ownership region is identified purely by statusMessage markers
31
34
  * (cross-runtime, no top-level key needed in settings.json).
32
35
  */
@@ -47,6 +50,33 @@ const FA_MARKERS = [
47
50
  "Capturing Flow Agents command evidence",
48
51
  ];
49
52
 
53
+ const FA_OWNERSHIP_MARKERS = [
54
+ ...FA_MARKERS,
55
+ "flow-agents",
56
+ "Flow Agents",
57
+ ];
58
+
59
+ function stableJson(value) {
60
+ if (value === undefined) return "undefined";
61
+ return JSON.stringify(value);
62
+ }
63
+
64
+ function valuesEqual(a, b) {
65
+ return stableJson(a) === stableJson(b);
66
+ }
67
+
68
+ function valueContainsManagedMarker(value) {
69
+ return FA_OWNERSHIP_MARKERS.some((marker) => stableJson(value).includes(marker));
70
+ }
71
+
72
+ function conflict(path, existingValue, managedValue) {
73
+ return { path, existingValue, managedValue };
74
+ }
75
+
76
+ function emitConflict(onConflict, item) {
77
+ if (typeof onConflict === "function") onConflict(item);
78
+ }
79
+
50
80
  /**
51
81
  * Returns true if a hook-group entry is owned by flow-agents.
52
82
  * A hook-group in Claude Code settings looks like:
@@ -95,16 +125,28 @@ function mergeArrayUnion(a, b) {
95
125
  * @param {unknown} existingPerms @param {unknown} managedPerms
96
126
  * @returns {Record<string, unknown>}
97
127
  */
98
- function mergePermissions(existingPerms, managedPerms) {
128
+ function mergePermissions(existingPerms, managedPerms, options = {}) {
99
129
  const e = existingPerms && typeof existingPerms === "object" ? existingPerms : {};
100
130
  const m = managedPerms && typeof managedPerms === "object" ? managedPerms : {};
101
- const out = Object.assign({}, e, m);
131
+ const out = Object.assign({}, e);
102
132
  for (const listKey of ["allow", "deny", "ask"]) {
103
133
  if (Array.isArray(e[listKey]) || Array.isArray(m[listKey])) {
104
134
  out[listKey] = mergeArrayUnion(e[listKey], m[listKey]);
105
135
  }
106
136
  }
107
- if (e.defaultMode !== undefined) out.defaultMode = e.defaultMode;
137
+ for (const [key, value] of Object.entries(m)) {
138
+ if (["allow", "deny", "ask"].includes(key)) continue;
139
+ if (!(key in e)) {
140
+ out[key] = value;
141
+ continue;
142
+ }
143
+ if (valuesEqual(e[key], value) || valueContainsManagedMarker(e[key])) {
144
+ out[key] = value;
145
+ continue;
146
+ }
147
+ out[key] = e[key];
148
+ emitConflict(options.onConflict, conflict(`permissions.${key}`, e[key], value));
149
+ }
108
150
  return out;
109
151
  }
110
152
 
@@ -117,14 +159,17 @@ function mergePermissions(existingPerms, managedPerms) {
117
159
  *
118
160
  * Returns a new object with:
119
161
  * - All keys from `existing` preserved (permissions, statusLine, auth, etc.)
120
- * - All keys from `managed` merged in (flow-agents owned keys like statusLine, hooks)
162
+ * - Managed keys added when absent, or updated only when the existing value
163
+ * is already Flow Agents-owned.
164
+ * - User-owned managed-key conflicts preserved from `existing` and surfaced
165
+ * through `options.onConflict`.
121
166
  * - For the `hooks` key: user-owned hook groups (non-FA) survive; FA groups are
122
167
  * replaced with the current managed set from `managed`.
123
168
  *
124
169
  * Strategy:
125
170
  * 1. Start with a shallow copy of `existing` (preserves all user keys).
126
- * 2. Overlay all scalar/non-hooks keys from `managed` (statusLine, permissions
127
- * from the bundle, skipDangerousModePermissionPrompt, etc.).
171
+ * 2. Add or update Flow Agents-owned non-hooks keys from `managed`; preserve
172
+ * user-owned conflicting values and report conflicts.
128
173
  * 3. For `hooks`: iterate each event key from both existing and managed;
129
174
  * keep user (non-FA) groups from existing, append the current FA groups
130
175
  * from managed.
@@ -133,19 +178,26 @@ function mergePermissions(existingPerms, managedPerms) {
133
178
  * @param {Record<string, unknown>} managed
134
179
  * @returns {Record<string, unknown>}
135
180
  */
136
- function mergeSettings(existing, managed) {
181
+ function mergeSettings(existing, managed, options = {}) {
137
182
  // 1. Start with all existing keys (preserves user-owned data).
138
183
  const result = Object.assign({}, existing);
139
184
 
140
- // 2. Overlay non-hooks keys from managed. `permissions` is DEEP-merged so the
141
- // user's allow/deny/ask lists + defaultMode survive flow-agents only adds
142
- // its required entries (#117: never clobber user customizations).
185
+ // 2. Add non-hooks keys from managed without overwriting user-owned values.
186
+ // If a key is absent, add the managed default. If the existing value is
187
+ // already Flow Agents-owned (for example the generated statusLine command),
188
+ // replace it with the current managed value. Otherwise preserve the user's
189
+ // value and surface a conflict so the user can decide.
143
190
  for (const [key, value] of Object.entries(managed)) {
144
191
  if (key === "hooks") continue;
145
192
  if (key === "permissions") {
146
- result.permissions = mergePermissions(existing.permissions, value);
147
- } else {
193
+ result.permissions = mergePermissions(existing.permissions, value, options);
194
+ } else if (!(key in existing)) {
148
195
  result[key] = value;
196
+ } else if (valuesEqual(existing[key], value) || valueContainsManagedMarker(existing[key])) {
197
+ result[key] = value;
198
+ } else {
199
+ result[key] = existing[key];
200
+ emitConflict(options.onConflict, conflict(key, existing[key], value));
149
201
  }
150
202
  }
151
203
 
@@ -267,7 +319,13 @@ function runMerge({ configPath, managedHooksPath, version, installRecordPath, ru
267
319
  }
268
320
 
269
321
  // (b) + (c) + (d) Merge.
270
- const merged = mergeSettings(existing, managed);
322
+ const conflicts = [];
323
+ const merged = mergeSettings(existing, managed, { onConflict: (item) => conflicts.push(item) });
324
+ for (const item of conflicts) {
325
+ process.stderr.write(
326
+ `install-merge: conflict: preserving existing setting '${item.path}' and not applying Flow Agents managed value\n`
327
+ );
328
+ }
271
329
 
272
330
  // (e) Atomic write.
273
331
  atomicWriteJson(configPath, merged);
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ // Install a prepared Flow Agents overlay without claiming or deleting unrelated files.
3
+ const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const path = require("node:path");
6
+
7
+ function fail(message) {
8
+ process.stderr.write(`install-owned-files: ${message}\n`);
9
+ process.exit(1);
10
+ }
11
+
12
+ const [sourceArg, destArg, manifestArg] = process.argv.slice(2);
13
+ if (!sourceArg || !destArg || !manifestArg) fail("usage: install-owned-files.js <overlay> <destination> <manifest-relative-path>");
14
+ const source = fs.realpathSync(sourceArg);
15
+ const dest = fs.realpathSync(destArg);
16
+ const manifestPath = path.join(dest, manifestArg);
17
+
18
+ function hashFile(file) {
19
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
20
+ }
21
+
22
+ function relativeFiles(root, current = root) {
23
+ const out = [];
24
+ for (const name of fs.readdirSync(current).sort()) {
25
+ const file = path.join(current, name);
26
+ const stat = fs.lstatSync(file);
27
+ if (stat.isSymbolicLink()) fail(`refusing symlink in install overlay: ${file}`);
28
+ if (stat.isDirectory()) out.push(...relativeFiles(root, file));
29
+ else if (stat.isFile()) out.push(path.relative(root, file).split(path.sep).join("/"));
30
+ else fail(`unsupported install overlay entry: ${file}`);
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function targetFor(rel) {
36
+ if (!rel || rel.split("/").some((part) => !part || part === "." || part === "..")) fail(`unsafe relative path: ${rel}`);
37
+ const target = path.resolve(dest, ...rel.split("/"));
38
+ const relative = path.relative(dest, target);
39
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) fail(`path escapes destination: ${rel}`);
40
+ return target;
41
+ }
42
+
43
+ function ensureSafeParent(target, create) {
44
+ const relative = path.relative(dest, path.dirname(target));
45
+ let current = dest;
46
+ for (const part of relative.split(path.sep).filter(Boolean)) {
47
+ current = path.join(current, part);
48
+ if (fs.existsSync(current)) {
49
+ const stat = fs.lstatSync(current);
50
+ if (stat.isSymbolicLink()) fail(`refusing to write through symlink: ${current}`);
51
+ if (!stat.isDirectory()) fail(`destination component is not a directory: ${current}`);
52
+ } else if (create) {
53
+ fs.mkdirSync(current);
54
+ }
55
+ }
56
+ }
57
+
58
+ function readManifest() {
59
+ if (!fs.existsSync(manifestPath)) return new Map();
60
+ ensureSafeParent(manifestPath, false);
61
+ const stat = fs.lstatSync(manifestPath);
62
+ if (stat.isSymbolicLink() || !stat.isFile()) fail(`ownership manifest is not a regular file: ${manifestPath}`);
63
+ let parsed;
64
+ try { parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8")); } catch (error) { fail(`invalid ownership manifest: ${error.message}`); }
65
+ if (parsed.schema_version !== "1.0" || !Array.isArray(parsed.files)) fail("unsupported ownership manifest; remove it only after manually auditing installed files");
66
+ return new Map(parsed.files.map((entry) => [String(entry.path), String(entry.sha256)]));
67
+ }
68
+
69
+ function legacyInstallCutoff() {
70
+ if (fs.existsSync(manifestPath)) return null;
71
+ const recordPath = path.join(dest, ".flow-agents", "install.json");
72
+ const hooksPath = path.join(dest, "hooks.json");
73
+ for (const file of [recordPath, hooksPath]) {
74
+ if (!fs.existsSync(file)) return null;
75
+ ensureSafeParent(file, false);
76
+ const stat = fs.lstatSync(file);
77
+ if (stat.isSymbolicLink() || !stat.isFile()) return null;
78
+ }
79
+ let record;
80
+ try { record = JSON.parse(fs.readFileSync(recordPath, "utf8")); } catch { return null; }
81
+ const installedAt = Date.parse(String(record.installedAt ?? ""));
82
+ if (record.runtime !== "codex" || !/^\d+\.\d+\.\d+(?:[-+].*)?$/.test(String(record.version ?? "")) || !Number.isFinite(installedAt)) return null;
83
+ const hooks = fs.readFileSync(hooksPath, "utf8");
84
+ if (!hooks.includes("Recording Flow Agents telemetry") && !hooks.includes("stop-goal-fit.js")) return null;
85
+ return installedAt + 2000;
86
+ }
87
+
88
+ // Exact path classes managed by the pre-manifest installer. These are an
89
+ // auditable historical contract, intentionally narrower than "all incoming
90
+ // files": root config/auth/hooks, kits/local, user-extensible skills/agents,
91
+ // and arbitrary destination files are never bootstrapped from timestamps.
92
+ function wasLegacyManagedPath(rel) {
93
+ const [top] = rel.split("/");
94
+ const managedTrees = new Set([
95
+ ".flow-agents", "agent-cards", "build", "context", "docs", "evals",
96
+ "integrations", "packaging", "powers", "prompts", "schemas", "scripts",
97
+ ]);
98
+ if (managedTrees.has(top)) return rel !== ".flow-agents/install.json";
99
+ if (top === "kits") return !rel.startsWith("kits/local/");
100
+ return new Set(["README.md", "console.telemetry.json", "install.sh"]).has(rel);
101
+ }
102
+
103
+ const previous = readManifest();
104
+ const legacyCutoff = legacyInstallCutoff();
105
+ const incoming = relativeFiles(source).map((rel) => ({ rel, source: path.join(source, ...rel.split("/")), hash: hashFile(path.join(source, ...rel.split("/"))) }));
106
+
107
+ // Complete preflight before the first destination mutation.
108
+ for (const entry of incoming) {
109
+ const target = targetFor(entry.rel);
110
+ ensureSafeParent(target, false);
111
+ if (!fs.existsSync(target)) continue;
112
+ const stat = fs.lstatSync(target);
113
+ if (stat.isSymbolicLink()) fail(`refusing to replace symlink: ${target}`);
114
+ if (!stat.isFile()) fail(`destination collision is not a regular file: ${target}`);
115
+ const oldHash = previous.get(entry.rel);
116
+ const currentHash = hashFile(target);
117
+ // Migration from the pre-manifest installer: byte-identical bundle files can
118
+ // be adopted without overwriting them. Any differing unowned file is a hard
119
+ // collision and remains untouched.
120
+ if (!oldHash) {
121
+ if (currentHash !== entry.hash) {
122
+ const recognizableLegacy = legacyCutoff !== null && wasLegacyManagedPath(entry.rel) && stat.mtimeMs <= legacyCutoff;
123
+ if (!recognizableLegacy) fail(`refusing to overwrite unowned or ambiguous file: ${target}`);
124
+ }
125
+ } else if (currentHash !== oldHash && currentHash !== entry.hash) {
126
+ fail(`refusing to overwrite modified Flow Agents file: ${target}`);
127
+ }
128
+ }
129
+ for (const [rel] of previous) {
130
+ const target = targetFor(rel);
131
+ ensureSafeParent(target, false);
132
+ if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) fail(`refusing to remove symlink replacing owned file: ${target}`);
133
+ }
134
+
135
+ for (const entry of incoming) {
136
+ const target = targetFor(entry.rel);
137
+ ensureSafeParent(target, true);
138
+ const temp = path.join(path.dirname(target), `.${path.basename(target)}.flow-agents-${process.pid}-${crypto.randomBytes(6).toString("hex")}.tmp`);
139
+ try {
140
+ fs.copyFileSync(entry.source, temp, fs.constants.COPYFILE_EXCL);
141
+ fs.chmodSync(temp, fs.statSync(entry.source).mode & 0o777);
142
+ fs.renameSync(temp, target);
143
+ } finally {
144
+ fs.rmSync(temp, { force: true });
145
+ }
146
+ }
147
+
148
+ const incomingPaths = new Set(incoming.map((entry) => entry.rel));
149
+ const staleParents = new Set();
150
+ for (const [rel, oldHash] of previous) {
151
+ if (incomingPaths.has(rel)) continue;
152
+ const target = targetFor(rel);
153
+ if (!fs.existsSync(target)) continue;
154
+ const stat = fs.lstatSync(target);
155
+ if (!stat.isFile() || hashFile(target) !== oldHash) {
156
+ process.stderr.write(`install-owned-files: preserving modified former Flow Agents file: ${target}\n`);
157
+ continue;
158
+ }
159
+ fs.rmSync(target);
160
+ staleParents.add(path.dirname(target));
161
+ }
162
+ for (const start of [...staleParents].sort((a, b) => b.length - a.length)) {
163
+ let current = start;
164
+ while (current !== dest) {
165
+ try { fs.rmdirSync(current); } catch { break; }
166
+ current = path.dirname(current);
167
+ }
168
+ }
169
+
170
+ ensureSafeParent(manifestPath, true);
171
+ if (fs.existsSync(manifestPath) && fs.lstatSync(manifestPath).isSymbolicLink()) fail(`refusing to replace symlink: ${manifestPath}`);
172
+ const manifestTemp = `${manifestPath}.tmp.${process.pid}.${crypto.randomBytes(6).toString("hex")}`;
173
+ try {
174
+ fs.writeFileSync(manifestTemp, `${JSON.stringify({ schema_version: "1.0", files: incoming.map((entry) => ({ path: entry.rel, sha256: entry.hash })) }, null, 2)}\n`, { flag: "wx" });
175
+ fs.renameSync(manifestTemp, manifestPath);
176
+ } finally {
177
+ fs.rmSync(manifestTemp, { force: true });
178
+ }