@kontourai/flow-agents 3.3.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 (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +41 -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/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +54 -1
  34. package/build/src/lib/flow-resolver.js +112 -5
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +117 -4
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -10,7 +10,9 @@ import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
10
10
  import { activateCodexLocal } from "../runtime-adapters.js";
11
11
  import { main as buildBundles } from "../tools/build-universal-bundles.js";
12
12
  import { root } from "../tools/common.js";
13
- import { defaultCodexHome, durableInstallRecordPath } from "../lib/local-artifact-root.js";
13
+ import { defaultCodexHome, durableInstallRecordPath, skillsManifestPath } from "../lib/local-artifact-root.js";
14
+ import { runConsoleConnectWizard, describeConsoleStatus, buildPostInstallSummaryLines } from "../lib/console-connect-options.js";
15
+ import { buildReport } from "./telemetry-doctor.js";
14
16
  const runtimeBundles = {
15
17
  base: "base",
16
18
  codex: "codex",
@@ -92,9 +94,39 @@ Options:
92
94
  --console-token-file PATH
93
95
  --console-tenant ID
94
96
  --activate-kits
97
+ --activate-kit KIT_ID Activate one catalog kit by id. Repeat for multiple kits.
95
98
  --yes, --headless
96
99
  `);
97
100
  }
101
+ function catalogKitIds() {
102
+ const catalogPath = path.join(root, "kits", "catalog.json");
103
+ if (!fs.existsSync(catalogPath))
104
+ return [];
105
+ const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
106
+ if (!Array.isArray(catalog.kits))
107
+ return [];
108
+ return catalog.kits
109
+ .map((entry) => typeof entry === "object" && entry !== null ? String(entry.id ?? "") : "")
110
+ .filter(Boolean);
111
+ }
112
+ function selectedKitIdsFromFlags(flags) {
113
+ const explicit = flagList(flags, "activate-kit").map((id) => id.trim()).filter(Boolean);
114
+ if (explicit.length)
115
+ return [...new Set(explicit)];
116
+ if (flagBool(flags, "activate-kits"))
117
+ return catalogKitIds();
118
+ return [];
119
+ }
120
+ function writeInstallRecord(dest, runtime, global, activeKitIds = []) {
121
+ const recordPath = durableInstallRecordPath(dest);
122
+ const installRecordDir = path.dirname(recordPath);
123
+ fs.mkdirSync(installRecordDir, { recursive: true });
124
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
125
+ const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime, ...(global ? { global: true } : {}), active_kit_ids: activeKitIds };
126
+ const recordTmp = `${recordPath}.tmp.${process.pid}`;
127
+ fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
128
+ fs.renameSync(recordTmp, recordPath);
129
+ }
98
130
  function normalizeRuntime(value) {
99
131
  if (!value)
100
132
  return undefined;
@@ -104,6 +136,91 @@ function normalizeRuntime(value) {
104
136
  return value;
105
137
  throw new Error(`unknown runtime '${value}'; expected base, codex, claude-code, kiro, opencode, or pi`);
106
138
  }
139
+ /**
140
+ * The three candidate global config directories a runtime's own tooling uses,
141
+ * shared between `globalDest` (the `--global` install target) and
142
+ * `detectRuntimeFromFilesystem` (auto-detection fallback) so "where does
143
+ * runtime X's global config live" is defined in exactly one place.
144
+ */
145
+ function runtimeGlobalConfigCandidates(homedir, env) {
146
+ return {
147
+ "claude-code": path.join(homedir, ".claude"),
148
+ codex: defaultCodexHome(env, homedir),
149
+ opencode: path.join(env["XDG_CONFIG_HOME"] ?? path.join(homedir, ".config"), "opencode"),
150
+ };
151
+ }
152
+ /**
153
+ * Detect the current runtime from environment-variable signals already
154
+ * ambient in the invoking process (Claude Code / Codex / opencode session
155
+ * markers). Reuses the single existing `detectRuntime` implementation from
156
+ * `scripts/hooks/lib/actor-identity.js` (via the same `createRequire` pattern
157
+ * used elsewhere in this file for `install-merge.js`/`skill-drift.js`)
158
+ * instead of forking a second copy of the env-var contract. `pi`'s env-var
159
+ * contract is unverified there (see actor-identity.js's own docstring) and is
160
+ * intentionally out of scope for auto-detection, so it maps down to
161
+ * `"unknown"` here (pi still falls back to `"base"` like today).
162
+ */
163
+ export function detectRuntimeFromProcessEnv(env = process.env) {
164
+ const actorIdentityPath = path.join(root, "scripts", "hooks", "lib", "actor-identity.js");
165
+ const _require = createRequire(import.meta.url);
166
+ const { detectRuntime } = _require(actorIdentityPath);
167
+ const detected = detectRuntime(env);
168
+ if (detected === "claude-code" || detected === "codex" || detected === "opencode")
169
+ return detected;
170
+ return "unknown";
171
+ }
172
+ /**
173
+ * Detect the current runtime by probing for exactly one runtime's own global
174
+ * config directory on disk (the same paths `globalDest` computes for
175
+ * `--global` installs — see `runtimeGlobalConfigCandidates`). Zero matches or
176
+ * 2+ matches are both ambiguous and must never guess: only a single
177
+ * unambiguous hit returns a runtime; everything else is `"unknown"`.
178
+ */
179
+ export function detectRuntimeFromFilesystem(homedir = os.homedir(), env = process.env) {
180
+ const candidates = runtimeGlobalConfigCandidates(homedir, env);
181
+ const matches = Object.keys(candidates).filter((runtime) => {
182
+ try {
183
+ return fs.existsSync(candidates[runtime]);
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ });
189
+ return matches.length === 1 ? matches[0] : "unknown";
190
+ }
191
+ /**
192
+ * Layered runtime auto-detection default used to pre-fill both the
193
+ * interactive prompt and the headless `--runtime` fallback: (1) env-var
194
+ * signals from the invoking process, (2) failing that, an unambiguous
195
+ * filesystem probe, (3) failing that, today's `"base"` default. An explicit
196
+ * `--runtime` flag or interactive answer always overrides this — this
197
+ * function only supplies the fallback default (see `interactiveOptions` /
198
+ * `headlessOptions`).
199
+ */
200
+ export function detectDefaultRuntime(env = process.env, homedir = os.homedir()) {
201
+ const fromEnv = detectRuntimeFromProcessEnv(env);
202
+ if (fromEnv !== "unknown")
203
+ return fromEnv;
204
+ const fromFilesystem = detectRuntimeFromFilesystem(homedir, env);
205
+ if (fromFilesystem !== "unknown")
206
+ return fromFilesystem;
207
+ return "base";
208
+ }
209
+ /**
210
+ * Returns the runtime the layered detector actually matched (env signal
211
+ * first, then an unambiguous filesystem probe), or "unknown" if neither
212
+ * fired. Distinct from `detectDefaultRuntime()`, which additionally falls
213
+ * back to `"base"` when detection is unknown -- the post-install summary's
214
+ * "(auto-detected)" annotation needs to distinguish "detection fired" from
215
+ * "detection defaulted to base," which `detectDefaultRuntime()`'s return
216
+ * value alone cannot express.
217
+ */
218
+ function detectedRuntimeSignal(env = process.env, homedir = os.homedir()) {
219
+ const fromEnv = detectRuntimeFromProcessEnv(env);
220
+ if (fromEnv !== "unknown")
221
+ return fromEnv;
222
+ return detectRuntimeFromFilesystem(homedir, env);
223
+ }
107
224
  function normalizeTelemetrySink(value) {
108
225
  if (value === "local-files" || value === "local-kontour-console" || value === "kontour-hosted-console" || value === "user-hosted-console" || value === "kontour-cloud" || value === "hosted-kontour-console")
109
226
  return value;
@@ -119,6 +236,24 @@ function telemetrySinksFromFlags(flags) {
119
236
  function needsConsoleCredentials(sinks) {
120
237
  return sinks.some((sink) => sink !== "local-files");
121
238
  }
239
+ const DEFAULT_HOSTED_CONSOLE_URL = "https://console.kontourai.io";
240
+ const DEFAULT_LOCAL_CONSOLE_URL = "http://127.0.0.1:3737";
241
+ /**
242
+ * Resolve one of `console-presets.sh`'s bash functions (by name) against the
243
+ * source root, so the interactive wizard's printed defaults are never a
244
+ * second hardcoded literal competing with the one bash source of truth
245
+ * (itself overridable via FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL /
246
+ * FLOW_AGENTS_LOCAL_KONTOUR_CONSOLE_URL, depending on which function is
247
+ * named). `fallback` is only a defensive floor for the rare case the
248
+ * subprocess produces no output (e.g. an unreadable source tree) -- it is not
249
+ * a second default.
250
+ */
251
+ function presetConsoleUrl(bashFnName, fallback) {
252
+ const scriptPath = path.join(root, "scripts", "telemetry", "console-presets.sh");
253
+ const result = spawnSync("bash", ["-c", `source "${scriptPath}" && ${bashFnName}`], { encoding: "utf8" });
254
+ const stdout = (result.stdout ?? "").trim();
255
+ return stdout || fallback;
256
+ }
122
257
  async function questionHidden(prompt) {
123
258
  if (!process.stdin.isTTY)
124
259
  return "";
@@ -157,13 +292,16 @@ function defaultDest(runtime) {
157
292
  return path.join(os.homedir(), ".flow-agents");
158
293
  return process.cwd();
159
294
  }
160
- function globalDest(runtime) {
295
+ export function globalDest(runtime) {
296
+ // Shared with detectRuntimeFromFilesystem so the "where is runtime X's global
297
+ // config" path logic is defined exactly once.
298
+ const candidates = runtimeGlobalConfigCandidates(os.homedir(), process.env);
161
299
  if (runtime === "claude-code") {
162
300
  // Honor FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation (same as checkScopeCollision).
163
301
  const override = process.env["FLOW_AGENTS_USER_CLAUDE_SETTINGS"];
164
302
  if (override)
165
303
  return path.dirname(override);
166
- return path.join(os.homedir(), ".claude");
304
+ return candidates["claude-code"];
167
305
  }
168
306
  if (runtime === "opencode") {
169
307
  // Honor FLOW_AGENTS_USER_OPENCODE_CONFIG (points to the opencode.json FILE) for test isolation,
@@ -172,11 +310,15 @@ function globalDest(runtime) {
172
310
  if (override)
173
311
  return path.dirname(override);
174
312
  // Global opencode config: ~/.config/opencode/ (honor XDG_CONFIG_HOME when set, else ~/.config).
175
- return path.join(process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config"), "opencode");
313
+ return candidates.opencode;
176
314
  }
177
315
  if (runtime === "codex") {
178
316
  // codex --global routes to the standard Codex home. --dest remains an explicit override.
179
- return defaultCodexHome();
317
+ // `candidates.codex` is built from the same `CODEX_HOME || ~/.codex` resolution as
318
+ // `defaultCodexHome()` (see runtimeGlobalConfigCandidates above), so this is byte-identical
319
+ // to calling `defaultCodexHome()` directly while keeping the candidate map the single
320
+ // source of truth for "where does runtime X's global config live".
321
+ return candidates.codex;
180
322
  }
181
323
  if (runtime === "pi") {
182
324
  // pi has no documented global config dir.
@@ -185,46 +327,94 @@ function globalDest(runtime) {
185
327
  }
186
328
  return defaultDest(runtime);
187
329
  }
188
- function parseYesNo(value, fallback) {
189
- const normalized = value.trim().toLowerCase();
190
- if (!normalized)
191
- return fallback;
192
- if (["y", "yes", "true", "1"].includes(normalized))
193
- return true;
194
- if (["n", "no", "false", "0"].includes(normalized))
195
- return false;
196
- return fallback;
330
+ /**
331
+ * Console-connect sink/credential gathering for interactiveOptions(),
332
+ * extracted as a pure refactor (identical behavior, identical returned
333
+ * shape) so interactiveOptions() itself stays a thin top-level orchestration.
334
+ * Covers both branches of "did the caller already give --telemetry-sink(s)
335
+ * on argv":
336
+ * - sinkFlagsGiven: the guided wizard is bypassed entirely (same as the old
337
+ * hidden sinkAnswer skip); only whichever of URL/token/tenant was not
338
+ * already supplied via flag gets its own prompt.
339
+ * - not given: the full guided wizard runs, with askWithFlagOverrides /
340
+ * askHiddenWithFlagOverrides short-circuiting any individual sub-prompt
341
+ * whose answer was already supplied via flag -- same flag-wins precedence
342
+ * as the sinkFlagsGiven branch, just applied per-sub-prompt instead of
343
+ * skipping the whole wizard.
344
+ * Flag-wins-over-prompt precedence is unchanged from before this wizard
345
+ * existed.
346
+ */
347
+ async function gatherConsoleConnectOptions(rl, args, consoleTokenFile) {
348
+ const sinkFlagsGiven = flagList(args.flags, "telemetry-sink").length > 0 || flagList(args.flags, "telemetry-sinks").length > 0;
349
+ const consoleUrlFlag = flagString(args.flags, "console-url");
350
+ const consoleTenantFlag = flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id");
351
+ if (sinkFlagsGiven) {
352
+ const telemetrySinks = telemetrySinksFromFlags(args.flags);
353
+ const needsUserHostedUrl = telemetrySinks.includes("user-hosted-console") || telemetrySinks.includes("hosted-kontour-console");
354
+ const consoleUrl = consoleUrlFlag ?? (needsUserHostedUrl ? await rl.question("User-hosted Console URL: ") : undefined);
355
+ const consoleTokenValue = consoleTokenFile ? undefined : (needsConsoleCredentials(telemetrySinks) ? await questionHidden("Console telemetry token (blank to skip): ") : undefined);
356
+ const consoleTenant = consoleTenantFlag ?? (needsConsoleCredentials(telemetrySinks) ? await rl.question("Console tenant ID (blank to skip): ") : undefined);
357
+ return { telemetrySinks, consoleUrl, consoleTokenValue, consoleTenant };
358
+ }
359
+ const askWithFlagOverrides = async (prompt) => {
360
+ if (prompt.includes("Self-hosted Console URL") && consoleUrlFlag !== undefined)
361
+ return consoleUrlFlag;
362
+ if (prompt.includes("Console tenant ID") && consoleTenantFlag !== undefined)
363
+ return consoleTenantFlag;
364
+ return rl.question(prompt);
365
+ };
366
+ const askHiddenWithFlagOverrides = async (prompt) => {
367
+ if (consoleTokenFile)
368
+ return "";
369
+ return questionHidden(prompt);
370
+ };
371
+ const wizardResult = await runConsoleConnectWizard({ ask: askWithFlagOverrides, askHidden: askHiddenWithFlagOverrides }, {
372
+ hostedUrl: presetConsoleUrl("flow_agents_kontour_hosted_console_url", DEFAULT_HOSTED_CONSOLE_URL),
373
+ localUrl: presetConsoleUrl("flow_agents_local_kontour_console_url", DEFAULT_LOCAL_CONSOLE_URL),
374
+ });
375
+ for (const warning of wizardResult.warnings)
376
+ console.warn(`flow-agents init: ${warning}`);
377
+ return {
378
+ telemetrySinks: wizardResult.telemetrySinks,
379
+ consoleUrl: consoleUrlFlag ?? wizardResult.consoleUrl,
380
+ consoleTokenValue: consoleTokenFile ? undefined : wizardResult.consoleTokenValue,
381
+ consoleTenant: consoleTenantFlag ?? wizardResult.consoleTenant,
382
+ };
197
383
  }
198
384
  async function interactiveOptions(argv) {
199
385
  const args = parseArgs(argv);
200
386
  const rl = createInterface({ input, output });
201
387
  try {
202
- const runtimeDefault = normalizeRuntime(flagString(args.flags, "runtime")) ?? "base";
203
- const runtimeAnswer = flagString(args.flags, "runtime") ?? await rl.question(`Runtime [${runtimeDefault}]: `);
204
- const runtime = normalizeRuntime(runtimeAnswer.trim() || runtimeDefault) ?? runtimeDefault;
388
+ const explicitRuntimeFlag = flagString(args.flags, "runtime");
389
+ const runtimeDefault = normalizeRuntime(explicitRuntimeFlag) ?? detectDefaultRuntime();
390
+ const runtimeAnswer = explicitRuntimeFlag ?? await rl.question(`Runtime [${runtimeDefault}]: `);
391
+ const runtimeAnswerTrimmed = runtimeAnswer.trim();
392
+ const runtime = normalizeRuntime(runtimeAnswerTrimmed || runtimeDefault) ?? runtimeDefault;
393
+ // "(auto-detected)" is only accurate when the runtime was neither an
394
+ // explicit --runtime flag nor a typed interactive answer AND the layered
395
+ // detector actually matched a signal (not merely defaulted to "base").
396
+ const runtimeAutoDetected = !explicitRuntimeFlag && !runtimeAnswerTrimmed && detectedRuntimeSignal() === runtime;
205
397
  const isGlobal = flagBool(args.flags, "global");
206
398
  const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
207
399
  const destDefault = flagString(args.flags, "dest", destBase) ?? destBase;
208
400
  const destAnswer = flagString(args.flags, "dest") ?? await rl.question(`Install destination [${destDefault}]: `);
209
- const sinkDefault = telemetrySinksFromFlags(args.flags);
210
- const sinkAnswer = flagList(args.flags, "telemetry-sink").length || flagList(args.flags, "telemetry-sinks").length
211
- ? sinkDefault.join(",")
212
- : await rl.question(`Telemetry sinks [${sinkDefault.join(",")}]: `);
213
- const telemetrySinks = telemetrySinksFromValues([sinkAnswer || sinkDefault.join(",")]);
214
- const needsUserHostedUrl = telemetrySinks.includes("user-hosted-console") || telemetrySinks.includes("hosted-kontour-console");
215
- const consoleUrl = flagString(args.flags, "console-url") ?? (needsUserHostedUrl ? await rl.question("User-hosted Console URL: ") : undefined);
401
+ // Guided "Connect to Kontour Console?" wizard (Hosted/Local/Self-hosted/Skip)
402
+ // replaces the old hidden sink-keyword prompt -- see gatherConsoleConnectOptions
403
+ // for the flag-override / sink-flags-given branching.
216
404
  const consoleEndpoint = flagString(args.flags, "console-endpoint") ?? flagString(args.flags, "console-endpoint-url");
217
405
  const consoleTokenFile = flagString(args.flags, "console-token-file");
218
- const consoleTokenValue = consoleTokenFile ? undefined : (needsConsoleCredentials(telemetrySinks) ? await questionHidden("Console telemetry token (blank to skip): ") : undefined);
219
- const consoleTenant = flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id") ?? (needsConsoleCredentials(telemetrySinks) ? await rl.question("Console tenant ID (blank to skip): ") : undefined);
220
- const activateDefault = false;
221
- const activateAnswer = flagBool(args.flags, "activate-kits")
222
- ? "yes"
223
- : runtime === "codex"
224
- ? await rl.question(`Activate local Builder Kit runtime assets [${activateDefault ? "Y/n" : "y/N"}]: `)
225
- : "no";
406
+ const { telemetrySinks, consoleUrl, consoleTokenValue, consoleTenant } = await gatherConsoleConnectOptions(rl, args, consoleTokenFile);
407
+ const activeKitIds = selectedKitIdsFromFlags(args.flags);
408
+ if (!activeKitIds.length && !flagBool(args.flags, "activate-kits") && !flagList(args.flags, "activate-kit").length) {
409
+ const available = catalogKitIds();
410
+ if (available.length) {
411
+ const answer = await rl.question(`Activate kits by id (comma-separated, default none) [none]: `);
412
+ activeKitIds.push(...answer.split(",").map((id) => id.trim()).filter((id) => available.includes(id)));
413
+ }
414
+ }
226
415
  return {
227
416
  runtime,
417
+ runtimeAutoDetected,
228
418
  global: isGlobal,
229
419
  dest: path.resolve(destAnswer.trim() || destDefault),
230
420
  consoleUrl: consoleUrl?.trim() || undefined,
@@ -233,7 +423,8 @@ async function interactiveOptions(argv) {
233
423
  consoleTokenValue: consoleTokenValue?.trim() || undefined,
234
424
  consoleTenant: consoleTenant?.trim() || undefined,
235
425
  telemetrySinks,
236
- activateKits: runtime === "codex" && parseYesNo(activateAnswer, activateDefault),
426
+ activateKits: activeKitIds.length > 0,
427
+ activeKitIds,
237
428
  };
238
429
  }
239
430
  finally {
@@ -242,7 +433,7 @@ async function interactiveOptions(argv) {
242
433
  }
243
434
  function headlessOptions(argv) {
244
435
  const args = parseArgs(argv);
245
- const runtime = normalizeRuntime(flagString(args.flags, "runtime")) ?? "base";
436
+ const runtime = normalizeRuntime(flagString(args.flags, "runtime")) ?? detectDefaultRuntime();
246
437
  const isGlobal = flagBool(args.flags, "global");
247
438
  const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
248
439
  return {
@@ -254,10 +445,11 @@ function headlessOptions(argv) {
254
445
  consoleTokenFile: flagString(args.flags, "console-token-file"),
255
446
  consoleTenant: flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id"),
256
447
  telemetrySinks: telemetrySinksFromFlags(args.flags),
257
- activateKits: runtime === "codex" && flagBool(args.flags, "activate-kits"),
448
+ activateKits: selectedKitIdsFromFlags(args.flags).length > 0,
449
+ activeKitIds: selectedKitIdsFromFlags(args.flags),
258
450
  };
259
451
  }
260
- function ensureBundle(runtime) {
452
+ export function ensureBundle(runtime) {
261
453
  const bundle = path.join(root, "dist", runtimeBundles[runtime]);
262
454
  if (!fs.existsSync(path.join(bundle, "install.sh"))) {
263
455
  const rc = buildBundles();
@@ -278,6 +470,17 @@ function ensureBundle(runtime) {
278
470
  // installs need an absolute, session-independent path instead.
279
471
  const GLOBAL_INSTALL_PROJECT_DIR_PREFIX = /root="\$\{CLAUDE_PROJECT_DIR:-\$\(pwd\)\}";\s*/g;
280
472
  const GLOBAL_INSTALL_PROJECT_DIR_VAR = /"\$root\//g;
473
+ function warnInstallMergeConflicts(conflicts) {
474
+ for (const conflict of conflicts) {
475
+ console.warn(`install-merge: conflict: preserving existing setting '${conflict.path}' and not applying Flow Agents managed value`);
476
+ }
477
+ }
478
+ function mergeInstallSettings(mergeSettings, existing, managed) {
479
+ const conflicts = [];
480
+ const merged = mergeSettings(existing, managed, { onConflict: (conflict) => conflicts.push(conflict) });
481
+ warnInstallMergeConflicts(conflicts);
482
+ return merged;
483
+ }
281
484
  function rewriteCommandForGlobalInstall(command, sourceRoot) {
282
485
  return command
283
486
  .replace(GLOBAL_INSTALL_PROJECT_DIR_PREFIX, "")
@@ -369,9 +572,10 @@ function installBundle(bundle, options) {
369
572
  return result.status ?? 1;
370
573
  }
371
574
  function activateKits(options) {
372
- if (!options.activateKits)
575
+ const activeKitIds = options.activeKitIds ?? [];
576
+ if (!options.activateKits || activeKitIds.length === 0 || options.runtime !== "codex")
373
577
  return 0;
374
- const result = activateCodexLocal(options.dest, options.dest);
578
+ const result = activateCodexLocal(options.dest, options.dest, { kitIdFilter: activeKitIds });
375
579
  if (Array.isArray(result.errors) && result.errors.length > 0) {
376
580
  console.error(JSON.stringify(result, null, 2));
377
581
  return 1;
@@ -380,6 +584,70 @@ function activateKits(options) {
380
584
  console.log(`Activated ${generated.length} Codex local runtime asset(s)`);
381
585
  return 0;
382
586
  }
587
+ /**
588
+ * Headless counterpart of interactiveOptions()'s inline `runtimeAutoDetected`
589
+ * computation. headlessOptions() itself is untouched (see its doc note) --
590
+ * this is computed separately in main() and merged onto its result, so the
591
+ * "(auto-detected)" annotation applies to both install paths without adding
592
+ * a new required prompt or changing headlessOptions()'s own behavior.
593
+ */
594
+ function headlessRuntimeAutoDetected(argv) {
595
+ const args = parseArgs(argv);
596
+ if (flagString(args.flags, "runtime"))
597
+ return false;
598
+ return detectedRuntimeSignal() !== "unknown";
599
+ }
600
+ /**
601
+ * G2 auto-verify + G3 post-install summary, shared by both the headless and
602
+ * interactive install paths (applies identically regardless of which one
603
+ * reached main()'s successful non-global install tail). Calls
604
+ * telemetry-doctor.ts's buildReport() in-process against the endpoint
605
+ * install-console-config.sh just wrote. Never allowed to change the exit
606
+ * code or hang the install: buildReport already has its own bounded default
607
+ * reachability timeout (2000ms unless overridden), and any thrown/rejected
608
+ * error here is caught and downgraded to a warning line only.
609
+ */
610
+ async function printPostInstallSummary(options) {
611
+ let doctorConsole = { sink: "local-only", reachability: { checked: false, ok: null } };
612
+ let tokenConfigured = false;
613
+ let tenantConfigured = false;
614
+ try {
615
+ const report = await buildReport(["--dest", options.dest]);
616
+ doctorConsole = report.console;
617
+ tokenConfigured = report.console.tokenConfigured;
618
+ tenantConfigured = report.console.tenantConfigured;
619
+ }
620
+ catch (error) {
621
+ console.warn(`flow-agents init: WARNING: could not verify the Console connection: ${error.message} (telemetry still installed; run 'flow-agents telemetry-doctor' to re-check).`);
622
+ }
623
+ const consoleStatus = describeConsoleStatus({ console: doctorConsole });
624
+ const nextSteps = [`Run your agent inside ${options.dest} -- Flow Agents hooks are already wired.`];
625
+ if (consoleStatus.status !== "local-only") {
626
+ nextSteps.push("Telemetry now flows to Kontour Console; visit the Console for ROI/economics views.");
627
+ }
628
+ // Only for the "reachability was never attempted" case (self-hosted/BYO
629
+ // HTTPS hosts default to not-allowed without --allow-network) -- never for
630
+ // local-only (nothing to verify) or the already-verified case (nothing left
631
+ // to do). doctorConsole.reachability.checked is the same raw signal
632
+ // describeConsoleStatus used to pick its NOT_CHECKED_DETAIL branch, so this
633
+ // stays in sync with that classifier without re-parsing its detail string.
634
+ if (consoleStatus.status === "connected-unverified" && doctorConsole.reachability.checked === false) {
635
+ nextSteps.push("Self-hosted/BYO Console reachability was not checked; run `flow-agents telemetry-doctor --allow-network` to verify it.");
636
+ }
637
+ nextSteps.push("Re-run `flow-agents init` anytime to reconfigure runtime, destination, or Console connection.");
638
+ const summaryLines = buildPostInstallSummaryLines({
639
+ runtime: options.runtime,
640
+ runtimeAutoDetected: Boolean(options.runtimeAutoDetected),
641
+ dest: options.dest,
642
+ telemetrySinks: options.telemetrySinks,
643
+ consoleStatus,
644
+ tokenConfigured,
645
+ tenantConfigured,
646
+ nextSteps,
647
+ });
648
+ for (const line of summaryLines)
649
+ console.log(line);
650
+ }
383
651
  export async function main(argv = process.argv.slice(2)) {
384
652
  if (argv.includes("--help") || argv.includes("-h")) {
385
653
  usage();
@@ -387,7 +655,9 @@ export async function main(argv = process.argv.slice(2)) {
387
655
  }
388
656
  const headless = argv.includes("--yes") || argv.includes("--headless") || !process.stdin.isTTY;
389
657
  try {
390
- const options = headless ? headlessOptions(argv) : await interactiveOptions(argv);
658
+ const options = headless
659
+ ? { ...headlessOptions(argv), runtimeAutoDetected: headlessRuntimeAutoDetected(argv) }
660
+ : await interactiveOptions(argv);
391
661
  // Scope-collision check for claude-code: Claude Code merges user-level
392
662
  // (~/.claude/settings.json) and project-level (.claude/settings.json) settings
393
663
  // and runs ALL matching hooks from both files. If a user-level settings file
@@ -443,7 +713,7 @@ export async function main(argv = process.argv.slice(2)) {
443
713
  existing = {};
444
714
  }
445
715
  }
446
- const merged = mergeSettings(existing, managed);
716
+ const merged = mergeInstallSettings(mergeSettings, existing, managed);
447
717
  const tmp = `${destSettingsPath}.tmp.${process.pid}`;
448
718
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
449
719
  fs.renameSync(tmp, destSettingsPath);
@@ -452,16 +722,27 @@ export async function main(argv = process.argv.slice(2)) {
452
722
  const skillsSync = copyDirMerge(path.join(bundle, ".claude", "skills"), path.join(options.dest, "skills"));
453
723
  const agentsSync = copyDirMerge(path.join(bundle, ".claude", "agents"), path.join(options.dest, "agents"));
454
724
  // Write version stamp.
455
- const recordPath = durableInstallRecordPath(options.dest);
456
- const installRecordDir = path.dirname(recordPath);
457
- fs.mkdirSync(installRecordDir, { recursive: true });
458
- const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
459
- const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code", global: true };
460
- const recordTmp = `${recordPath}.tmp.${process.pid}`;
461
- fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
462
- fs.renameSync(recordTmp, recordPath);
725
+ writeInstallRecord(options.dest, "claude-code", true, options.activeKitIds ?? []);
463
726
  console.log(`Flow Agents global hooks merged for claude-code in ${options.dest}`);
464
727
  console.log(`Synced skills (+${skillsSync.added} new, ~${skillsSync.updated} updated) and agents (+${agentsSync.added} new, ~${agentsSync.updated} updated) in ${options.dest}`);
728
+ // Write a per-skill-file sha256 content-hash manifest, sibling of install.json, so
729
+ // `flow-agents skill-drift-check` and the SessionStart advisory can classify installed
730
+ // skill files as in_sync/kit_updated/user_modified/unbaselined/missing_install/kit_removed
731
+ // without re-deriving a second hashing convention (kontourai/flow-agents#439). This is an
732
+ // additive convenience feature layered on top of the install that already succeeded above
733
+ // (settings merge + skills/agents sync) — a failure writing this manifest must never fail
734
+ // the whole `init --global` run, so it is contained here: warn and continue.
735
+ try {
736
+ const skillDriftLibPath = path.join(root, "scripts", "hooks", "lib", "skill-drift.js");
737
+ const _requireSkillDrift = createRequire(import.meta.url);
738
+ const { buildManifest, writeManifestAtomic } = _requireSkillDrift(skillDriftLibPath);
739
+ const skillsManifest = buildManifest({ skillsSourceDir: path.join(bundle, ".claude", "skills"), runtime: "claude-code" });
740
+ writeManifestAtomic(skillsManifestPath(options.dest), skillsManifest);
741
+ }
742
+ catch (error) {
743
+ console.error(`flow-agents init: WARNING: could not write skills drift-detection manifest: ${error.message} ` +
744
+ "(drift detection will report unbaselined until the next successful --global sync).");
745
+ }
465
746
  return 0;
466
747
  }
467
748
  // --global for opencode: merge FA opencode.json into the global opencode config dir.
@@ -490,21 +771,13 @@ export async function main(argv = process.argv.slice(2)) {
490
771
  existing = {};
491
772
  }
492
773
  }
493
- const merged = mergeSettings(existing, managed);
774
+ const merged = mergeInstallSettings(mergeSettings, existing, managed);
494
775
  const tmp = `${destConfigPath}.tmp.${process.pid}`;
495
776
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
496
777
  `, "utf8");
497
778
  fs.renameSync(tmp, destConfigPath);
498
779
  // Write version stamp.
499
- const recordPath = durableInstallRecordPath(options.dest);
500
- const installRecordDir = path.dirname(recordPath);
501
- fs.mkdirSync(installRecordDir, { recursive: true });
502
- const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
503
- const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "opencode", global: true };
504
- const recordTmp = `${recordPath}.tmp.${process.pid}`;
505
- fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}
506
- `, "utf8");
507
- fs.renameSync(recordTmp, recordPath);
780
+ writeInstallRecord(options.dest, "opencode", true, options.activeKitIds ?? []);
508
781
  console.log(`Flow Agents global config merged for opencode in ${options.dest}`);
509
782
  return 0;
510
783
  }
@@ -535,6 +808,7 @@ export async function main(argv = process.argv.slice(2)) {
535
808
  const installed = result.status ?? 1;
536
809
  if (installed !== 0)
537
810
  return installed;
811
+ writeInstallRecord(options.dest, "codex", true, options.activeKitIds ?? []);
538
812
  return activateKits(options);
539
813
  }
540
814
  // --global for pi: NOT_VERIFIED (no documented global dir). Warn and fall through to workspace install.
@@ -547,7 +821,13 @@ export async function main(argv = process.argv.slice(2)) {
547
821
  const installed = installBundle(bundle, options);
548
822
  if (installed !== 0)
549
823
  return installed;
550
- return activateKits(options);
824
+ writeInstallRecord(options.dest, options.runtime, options.global, options.activeKitIds ?? []);
825
+ const activated = activateKits(options);
826
+ // G2/G3: shared post-install auto-verify + summary tail. Applies
827
+ // identically whether main() reached here via headlessOptions() or
828
+ // interactiveOptions() -- never changes `activated`'s exit code.
829
+ await printPostInstallSummary(options);
830
+ return activated;
551
831
  }
552
832
  catch (error) {
553
833
  console.error(`flow-agents init: ${error.message}`);
@@ -619,7 +899,7 @@ function dogfoodClaudeCode(bundleRoot, dest) {
619
899
  existing = {};
620
900
  }
621
901
  }
622
- const merged = mergeSettings(existing, managed);
902
+ const merged = mergeInstallSettings(mergeSettings, existing, managed);
623
903
  const tmp = `${destSettingsPath}.tmp.${process.pid}`;
624
904
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
625
905
  fs.renameSync(tmp, destSettingsPath);
@@ -662,7 +942,7 @@ function dogfoodCodex(bundleRoot, dest) {
662
942
  existing = {};
663
943
  }
664
944
  }
665
- const merged = mergeSettings(existing, managed);
945
+ const merged = mergeInstallSettings(mergeSettings, existing, managed);
666
946
  const tmp = `${destHooksPath}.tmp.${process.pid}`;
667
947
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
668
948
  fs.renameSync(tmp, destHooksPath);