@kontourai/flow-agents 1.3.0 → 2.0.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 (214) hide show
  1. package/.github/CODEOWNERS +29 -0
  2. package/.github/actions/trust-verify/action.yml +145 -0
  3. package/.github/workflows/ci.yml +11 -4
  4. package/.github/workflows/kit-gates-demo.yml +2 -2
  5. package/.github/workflows/publish-npm.yml +10 -2
  6. package/.github/workflows/release-please.yml +1 -1
  7. package/.github/workflows/trust-reconcile.yml +113 -0
  8. package/AGENTS.md +13 -0
  9. package/CHANGELOG.md +103 -0
  10. package/CONTRIBUTING.md +4 -4
  11. package/README.md +1 -0
  12. package/agents/tool-planner.json +1 -1
  13. package/build/src/cli/console-learning-projection.d.ts +1 -0
  14. package/build/src/cli/effective-backlog-settings.d.ts +1 -0
  15. package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
  16. package/build/src/cli/init.d.ts +17 -0
  17. package/build/src/cli/init.js +242 -20
  18. package/build/src/cli/kit.d.ts +1 -0
  19. package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
  20. package/build/src/cli/publish-change-helper.d.ts +1 -0
  21. package/build/src/cli/pull-work-provider.d.ts +1 -0
  22. package/build/src/cli/runtime-adapter.d.ts +1 -0
  23. package/build/src/cli/telemetry-doctor.d.ts +1 -0
  24. package/build/src/cli/usage-feedback.d.ts +1 -0
  25. package/build/src/cli/utterance-check.d.ts +1 -0
  26. package/build/src/cli/validate-hook-influence.d.ts +1 -0
  27. package/build/src/cli/validate-source-tree.d.ts +1 -0
  28. package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
  29. package/build/src/cli/validate-workflow-artifacts.js +19 -2
  30. package/build/src/cli/verify.d.ts +1 -0
  31. package/build/src/cli/verify.js +90 -0
  32. package/build/src/cli/veritas-governance.d.ts +1 -0
  33. package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
  34. package/build/src/cli/workflow-sidecar.d.ts +324 -0
  35. package/build/src/cli/workflow-sidecar.js +1973 -90
  36. package/build/src/cli.d.ts +2 -0
  37. package/build/src/cli.js +2 -3
  38. package/build/src/flow-kit/validate.d.ts +81 -0
  39. package/build/src/index.d.ts +5 -0
  40. package/build/src/index.js +36 -0
  41. package/build/src/lib/args.d.ts +8 -0
  42. package/build/src/lib/flow-resolver.d.ts +82 -0
  43. package/build/src/lib/flow-resolver.js +237 -0
  44. package/build/src/lib/fs.d.ts +7 -0
  45. package/build/src/lib/workflow-learning-projection.d.ts +132 -0
  46. package/build/src/runtime-adapters.d.ts +18 -0
  47. package/build/src/tools/build-universal-bundles.d.ts +2 -0
  48. package/build/src/tools/build-universal-bundles.js +34 -22
  49. package/build/src/tools/common.d.ts +9 -0
  50. package/build/src/tools/generate-context-map.d.ts +2 -0
  51. package/build/src/tools/generate-context-map.js +3 -16
  52. package/build/src/tools/validate-package.d.ts +2 -0
  53. package/build/src/tools/validate-source-tree.d.ts +2 -0
  54. package/build/src/tools/validate-source-tree.js +42 -162
  55. package/context/contracts/artifact-contract.md +10 -0
  56. package/context/contracts/delivery-contract.md +1 -0
  57. package/context/contracts/review-contract.md +1 -0
  58. package/context/contracts/verification-contract.md +2 -0
  59. package/context/gate-awareness.md +39 -0
  60. package/context/scripts/hooks/stop-goal-fit.js +632 -70
  61. package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
  62. package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
  63. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  64. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  65. package/docs/adr/0007-skill-audit.md +1 -1
  66. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
  67. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
  68. package/docs/adr/0011-mcp-posture.md +100 -0
  69. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
  70. package/docs/adr/0013-context-lifecycle.md +151 -0
  71. package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
  72. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
  73. package/docs/adr/0016-three-hard-boundary-model.md +71 -0
  74. package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
  75. package/docs/agent-system-guidebook.md +5 -12
  76. package/docs/context-map.md +4 -10
  77. package/docs/developer-architecture.md +14 -0
  78. package/docs/index.md +3 -2
  79. package/docs/integrations/framework-adapter.md +19 -6
  80. package/docs/integrations/index.md +2 -2
  81. package/docs/north-star.md +4 -4
  82. package/docs/operating-layers.md +3 -3
  83. package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
  84. package/docs/repository-structure.md +2 -2
  85. package/docs/skills-map.md +1 -0
  86. package/docs/spec/runtime-hook-surface.md +78 -10
  87. package/docs/standards-register.md +3 -3
  88. package/docs/survey-utterance-check.md +1 -1
  89. package/docs/trust-anchor-adoption.md +197 -0
  90. package/docs/verifiable-trust.md +95 -0
  91. package/docs/veritas-integration.md +2 -2
  92. package/docs/workflow-usage-guide.md +69 -0
  93. package/evals/acceptance/DEMO-false-completion.md +144 -0
  94. package/evals/acceptance/demo-cast.sh +92 -0
  95. package/evals/acceptance/demo-false-completion.sh +72 -0
  96. package/evals/acceptance/demo-real-evidence.sh +104 -0
  97. package/evals/acceptance/demo.tape +29 -0
  98. package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
  99. package/evals/acceptance/prove-capture-teeth.sh +114 -0
  100. package/evals/acceptance/prove-teeth.sh +105 -0
  101. package/evals/ci/antigaming-suite.sh +54 -0
  102. package/evals/ci/run-baseline.sh +2 -0
  103. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
  104. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
  105. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
  106. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
  107. package/evals/integration/test_builder_step_producers.sh +379 -0
  108. package/evals/integration/test_bundle_install.sh +35 -71
  109. package/evals/integration/test_bundle_lifecycle.sh +39 -2
  110. package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
  111. package/evals/integration/test_checkpoint_signing.sh +489 -0
  112. package/evals/integration/test_claim_lookup.sh +352 -0
  113. package/evals/integration/test_command_log_integrity.sh +275 -0
  114. package/evals/integration/test_context_map.sh +0 -2
  115. package/evals/integration/test_dual_emit_flow_step.sh +278 -0
  116. package/evals/integration/test_enforcer_expects_driven.sh +281 -0
  117. package/evals/integration/test_evidence_capture_hook.sh +185 -0
  118. package/evals/integration/test_flow_kit_repository.sh +2 -0
  119. package/evals/integration/test_flowdef_session_activation.sh +273 -0
  120. package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
  121. package/evals/integration/test_gate_bypass_chain.sh +448 -0
  122. package/evals/integration/test_gate_lockdown.sh +1137 -0
  123. package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
  124. package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
  125. package/evals/integration/test_goal_fit_hook.sh +69 -4
  126. package/evals/integration/test_goal_fit_rederive.sh +263 -0
  127. package/evals/integration/test_hook_category_behaviors.sh +14 -0
  128. package/evals/integration/test_install_merge.sh +1176 -0
  129. package/evals/integration/test_mint_attestation.sh +373 -0
  130. package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
  131. package/evals/integration/test_publish_delivery.sh +269 -0
  132. package/evals/integration/test_reconcile_soundness.sh +528 -0
  133. package/evals/integration/test_resolvefirststep_security.sh +208 -0
  134. package/evals/integration/test_session_resume_roundtrip.sh +286 -0
  135. package/evals/integration/test_trust_checkpoint.sh +325 -0
  136. package/evals/integration/test_trust_reconcile.sh +293 -0
  137. package/evals/integration/test_verify_cli.sh +208 -0
  138. package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
  139. package/evals/lib/node.sh +0 -6
  140. package/evals/run.sh +47 -0
  141. package/evals/static/test_library_exports.sh +85 -0
  142. package/evals/static/test_universal_bundles.sh +15 -0
  143. package/evals/static/test_workflow_skills.sh +6 -13
  144. package/install.sh +0 -7
  145. package/integrations/strands-ts/README.md +25 -15
  146. package/integrations/veritas/flow-agents.adapter.json +1 -2
  147. package/kits/builder/flows/build.flow.json +59 -12
  148. package/kits/builder/kit.json +85 -15
  149. package/kits/builder/skills/continue-work/SKILL.md +116 -0
  150. package/kits/builder/skills/deliver/SKILL.md +36 -6
  151. package/kits/builder/skills/design-probe/SKILL.md +28 -0
  152. package/kits/builder/skills/execute-plan/SKILL.md +9 -1
  153. package/kits/builder/skills/gate-review/SKILL.md +234 -0
  154. package/kits/builder/skills/learning-review/SKILL.md +30 -0
  155. package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
  156. package/kits/builder/skills/plan-work/SKILL.md +13 -1
  157. package/kits/builder/skills/pull-work/SKILL.md +19 -0
  158. package/kits/knowledge/adapters/default-store/index.js +38 -0
  159. package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
  160. package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
  161. package/kits/knowledge/docs/store-contract.md +314 -0
  162. package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
  163. package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
  164. package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
  165. package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
  166. package/kits/knowledge/evals/entities/suite.test.js +40 -0
  167. package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
  168. package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
  169. package/kits/knowledge/evals/retirement/suite.test.js +145 -0
  170. package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
  171. package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
  172. package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
  173. package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
  174. package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
  175. package/kits/knowledge/kit.json +51 -1
  176. package/package.json +13 -4
  177. package/packaging/conformance/README.md +10 -2
  178. package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
  179. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
  180. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
  181. package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
  182. package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
  183. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
  184. package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
  185. package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
  186. package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
  187. package/packaging/conformance/run-conformance.js +1 -1
  188. package/scripts/README.md +2 -1
  189. package/scripts/build-universal-bundles.js +0 -1
  190. package/scripts/ci/mint-attestation.js +221 -0
  191. package/scripts/ci/trust-reconcile.js +545 -0
  192. package/scripts/hooks/config-protection.js +423 -1
  193. package/scripts/hooks/evidence-capture.js +348 -0
  194. package/scripts/hooks/lib/liveness-read.js +113 -0
  195. package/scripts/hooks/run-hook.js +6 -1
  196. package/scripts/hooks/stop-goal-fit.js +1471 -79
  197. package/scripts/hooks/workflow-steering.js +135 -5
  198. package/scripts/install-codex-home.sh +39 -0
  199. package/scripts/install-merge.js +330 -0
  200. package/src/cli/init.ts +218 -20
  201. package/src/cli/validate-workflow-artifacts.ts +18 -2
  202. package/src/cli/verify.ts +100 -0
  203. package/src/cli/workflow-sidecar.ts +2093 -84
  204. package/src/cli.ts +2 -3
  205. package/src/index.ts +53 -0
  206. package/src/lib/flow-resolver.ts +284 -0
  207. package/src/tools/build-universal-bundles.ts +34 -21
  208. package/src/tools/generate-context-map.ts +3 -17
  209. package/src/tools/validate-source-tree.ts +44 -104
  210. package/tsconfig.json +1 -0
  211. package/build/src/tools/filter-installed-packs.js +0 -135
  212. package/packaging/packs.json +0 -49
  213. package/scripts/filter-installed-packs.js +0 -2
  214. package/src/tools/filter-installed-packs.ts +0 -132
@@ -0,0 +1,17 @@
1
+ export declare const COLLISION_MARKER = "Recording Flow Agents telemetry";
2
+ /**
3
+ * Check whether a user-level Claude Code settings file already contains
4
+ * Flow Agents hook commands. If it does, print a WARNING explaining that
5
+ * Claude Code merges user-level and project-level settings and runs ALL
6
+ * matching hooks, so having flow-agents in both places causes duplicate
7
+ * hook execution (double telemetry, double policy enforcement).
8
+ *
9
+ * The check does NOT block the install; it is advisory only.
10
+ *
11
+ * @param userSettingsFile Path to inspect (defaults to $HOME/.claude/settings.json;
12
+ * overridable via FLOW_AGENTS_USER_CLAUDE_SETTINGS env var for testability).
13
+ * @returns true if a collision was detected, false otherwise.
14
+ */
15
+ export declare function checkScopeCollision(userSettingsFile?: string): boolean;
16
+ export declare function main(argv?: string[]): Promise<number>;
17
+ export declare function mainDogfood(argv?: string[]): Promise<number>;
@@ -1,6 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { createRequire } from "node:module";
4
5
  import * as os from "node:os";
5
6
  import * as path from "node:path";
6
7
  import { createInterface } from "node:readline/promises";
@@ -76,12 +77,19 @@ function usage() {
76
77
  Options:
77
78
  --runtime base|codex|claude-code|kiro|opencode|pi
78
79
  --dest PATH
80
+ --global Target the runtime's global/user-level config path.
81
+ claude-code: merges FA hooks into ~/.claude/settings.json.
82
+ Honors FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation.
83
+ opencode: merges opencode.json into ~/.config/opencode/opencode.json
84
+ (honors XDG_CONFIG_HOME; test isolation via FLOW_AGENTS_USER_OPENCODE_CONFIG).
85
+ codex: runs install-codex-home.sh into ~/.flow-agents/codex
86
+ (the isolated Codex HOME; hooks merged, not overwritten).
87
+ pi: NOT_VERIFIED (no documented global dir); warns and falls back to workspace default.
79
88
  --telemetry-sink local-files|local-kontour-console|kontour-hosted-console|user-hosted-console
80
89
  --console-url URL
81
90
  --console-endpoint URL
82
91
  --console-token-file PATH
83
92
  --console-tenant ID
84
- --packs LIST
85
93
  --activate-kits
86
94
  --yes, --headless
87
95
  `);
@@ -148,6 +156,35 @@ function defaultDest(runtime) {
148
156
  return path.join(os.homedir(), ".flow-agents");
149
157
  return process.cwd();
150
158
  }
159
+ function globalDest(runtime) {
160
+ if (runtime === "claude-code") {
161
+ // Honor FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation (same as checkScopeCollision).
162
+ const override = process.env["FLOW_AGENTS_USER_CLAUDE_SETTINGS"];
163
+ if (override)
164
+ return path.dirname(override);
165
+ return path.join(os.homedir(), ".claude");
166
+ }
167
+ if (runtime === "opencode") {
168
+ // Honor FLOW_AGENTS_USER_OPENCODE_CONFIG (points to the opencode.json FILE) for test isolation,
169
+ // mirroring FLOW_AGENTS_USER_CLAUDE_SETTINGS for claude-code.
170
+ const override = process.env["FLOW_AGENTS_USER_OPENCODE_CONFIG"];
171
+ if (override)
172
+ return path.dirname(override);
173
+ // Global opencode config: ~/.config/opencode/ (honor XDG_CONFIG_HOME when set, else ~/.config).
174
+ return path.join(process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config"), "opencode");
175
+ }
176
+ if (runtime === "codex") {
177
+ // codex --global routes to the isolated Codex HOME at ~/.flow-agents/codex.
178
+ // This is the same path used by install-codex-home.sh; --dest overrides it for sandbox testing.
179
+ return path.join(os.homedir(), ".flow-agents", "codex");
180
+ }
181
+ if (runtime === "pi") {
182
+ // pi has no documented global config dir.
183
+ // NOT_VERIFIED: fall back to workspace default and warn caller.
184
+ return defaultDest(runtime);
185
+ }
186
+ return defaultDest(runtime);
187
+ }
151
188
  function parseYesNo(value, fallback) {
152
189
  const normalized = value.trim().toLowerCase();
153
190
  if (!normalized)
@@ -165,7 +202,9 @@ async function interactiveOptions(argv) {
165
202
  const runtimeDefault = normalizeRuntime(flagString(args.flags, "runtime")) ?? "base";
166
203
  const runtimeAnswer = flagString(args.flags, "runtime") ?? await rl.question(`Runtime [${runtimeDefault}]: `);
167
204
  const runtime = normalizeRuntime(runtimeAnswer.trim() || runtimeDefault) ?? runtimeDefault;
168
- const destDefault = flagString(args.flags, "dest", defaultDest(runtime)) ?? defaultDest(runtime);
205
+ const isGlobal = flagBool(args.flags, "global");
206
+ const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
207
+ const destDefault = flagString(args.flags, "dest", destBase) ?? destBase;
169
208
  const destAnswer = flagString(args.flags, "dest") ?? await rl.question(`Install destination [${destDefault}]: `);
170
209
  const sinkDefault = telemetrySinksFromFlags(args.flags);
171
210
  const sinkAnswer = flagList(args.flags, "telemetry-sink").length || flagList(args.flags, "telemetry-sinks").length
@@ -186,6 +225,7 @@ async function interactiveOptions(argv) {
186
225
  : "no";
187
226
  return {
188
227
  runtime,
228
+ global: isGlobal,
189
229
  dest: path.resolve(destAnswer.trim() || destDefault),
190
230
  consoleUrl: consoleUrl?.trim() || undefined,
191
231
  consoleEndpoint: consoleEndpoint?.trim() || undefined,
@@ -193,7 +233,6 @@ async function interactiveOptions(argv) {
193
233
  consoleTokenValue: consoleTokenValue?.trim() || undefined,
194
234
  consoleTenant: consoleTenant?.trim() || undefined,
195
235
  telemetrySinks,
196
- packs: flagString(args.flags, "packs"),
197
236
  activateKits: runtime === "codex" && parseYesNo(activateAnswer, activateDefault),
198
237
  };
199
238
  }
@@ -204,15 +243,17 @@ async function interactiveOptions(argv) {
204
243
  function headlessOptions(argv) {
205
244
  const args = parseArgs(argv);
206
245
  const runtime = normalizeRuntime(flagString(args.flags, "runtime")) ?? "base";
246
+ const isGlobal = flagBool(args.flags, "global");
247
+ const destBase = isGlobal ? globalDest(runtime) : defaultDest(runtime);
207
248
  return {
208
249
  runtime,
209
- dest: path.resolve(flagString(args.flags, "dest", defaultDest(runtime)) ?? defaultDest(runtime)),
250
+ global: isGlobal,
251
+ dest: path.resolve(flagString(args.flags, "dest", destBase) ?? destBase),
210
252
  consoleUrl: flagString(args.flags, "console-url"),
211
253
  consoleEndpoint: flagString(args.flags, "console-endpoint") ?? flagString(args.flags, "console-endpoint-url"),
212
254
  consoleTokenFile: flagString(args.flags, "console-token-file"),
213
255
  consoleTenant: flagString(args.flags, "console-tenant") ?? flagString(args.flags, "console-tenant-id"),
214
256
  telemetrySinks: telemetrySinksFromFlags(args.flags),
215
- packs: flagString(args.flags, "packs"),
216
257
  activateKits: runtime === "codex" && flagBool(args.flags, "activate-kits"),
217
258
  };
218
259
  }
@@ -249,8 +290,6 @@ function installBundle(bundle, options) {
249
290
  if (options.consoleTenant)
250
291
  args.push("--console-tenant", options.consoleTenant);
251
292
  const env = { ...process.env };
252
- if (options.packs)
253
- env.FLOW_AGENTS_PACKS = options.packs;
254
293
  const result = spawnSync("bash", args, { cwd: bundle, env, encoding: "utf8", stdio: "inherit" });
255
294
  if (tempTokenFile)
256
295
  fs.rmSync(path.dirname(tempTokenFile), { recursive: true, force: true });
@@ -290,7 +329,136 @@ export async function main(argv = process.argv.slice(2)) {
290
329
  // There is no well-known user-level codex hooks file in our install paths,
291
330
  // so no collision check is needed for codex.
292
331
  if (options.runtime === "claude-code") {
293
- checkScopeCollision();
332
+ // Skip collision check when --global is set: the user is intentionally
333
+ // targeting the global settings, so the collision they'd create is expected.
334
+ if (!options.global)
335
+ checkScopeCollision();
336
+ }
337
+ // --global for claude-code: merge only into the global/user-level settings dir.
338
+ // This writes only the hook-wiring config (merge into ~/.claude/settings.json),
339
+ // not the full workspace bundle. The global settings dir is the claude config root,
340
+ // so the settings.json lives directly in dest (not dest/.claude/).
341
+ if (options.global && options.runtime === "claude-code") {
342
+ const bundle = ensureBundle(options.runtime);
343
+ // For --global, dest is ~/.claude/ (the global settings dir).
344
+ // dogfoodClaudeCode writes to dest/.claude/settings.json — but for global,
345
+ // the settings.json lives at dest/settings.json (dest IS ~/.claude/).
346
+ // We use a special global-merge path: merge directly into dest/settings.json.
347
+ const sourcePath = path.join(bundle, ".claude", "settings.json");
348
+ if (!fs.existsSync(sourcePath)) {
349
+ console.error(`flow-agents init: bundle settings missing: ${sourcePath}`);
350
+ return 1;
351
+ }
352
+ const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
353
+ // Remove permissive defaults (not appropriate for global user settings).
354
+ delete managed["permissions"];
355
+ delete managed["skipDangerousModePermissionPrompt"];
356
+ fs.mkdirSync(options.dest, { recursive: true });
357
+ const destSettingsPath = path.join(options.dest, "settings.json");
358
+ const installMergePath = path.join(root, "scripts", "install-merge.js");
359
+ const _require = createRequire(import.meta.url);
360
+ const { mergeSettings } = _require(installMergePath);
361
+ let existing = {};
362
+ if (fs.existsSync(destSettingsPath)) {
363
+ try {
364
+ existing = JSON.parse(fs.readFileSync(destSettingsPath, "utf8"));
365
+ }
366
+ catch {
367
+ existing = {};
368
+ }
369
+ }
370
+ const merged = mergeSettings(existing, managed);
371
+ const tmp = `${destSettingsPath}.tmp.${process.pid}`;
372
+ fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
373
+ fs.renameSync(tmp, destSettingsPath);
374
+ // Write version stamp.
375
+ const installRecordDir = path.join(options.dest, ".flow-agents");
376
+ fs.mkdirSync(installRecordDir, { recursive: true });
377
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
378
+ const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code", global: true };
379
+ const recordPath = path.join(installRecordDir, "install.json");
380
+ const recordTmp = `${recordPath}.tmp.${process.pid}`;
381
+ fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
382
+ fs.renameSync(recordTmp, recordPath);
383
+ console.log(`Flow Agents global hooks merged for claude-code in ${options.dest}`);
384
+ return 0;
385
+ }
386
+ // --global for opencode: merge FA opencode.json into the global opencode config dir.
387
+ // Global path: ~/.config/opencode/opencode.json (honor XDG_CONFIG_HOME).
388
+ // Test isolation: FLOW_AGENTS_USER_OPENCODE_CONFIG points to the opencode.json FILE.
389
+ if (options.global && options.runtime === "opencode") {
390
+ const bundle = ensureBundle(options.runtime);
391
+ const sourcePath = path.join(bundle, "opencode.json");
392
+ if (!fs.existsSync(sourcePath)) {
393
+ console.error(`flow-agents init: bundle opencode.json missing: ${sourcePath}`);
394
+ return 1;
395
+ }
396
+ const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
397
+ fs.mkdirSync(options.dest, { recursive: true });
398
+ // The global opencode.json lives directly at dest/opencode.json.
399
+ const destConfigPath = path.join(options.dest, "opencode.json");
400
+ const installMergePath = path.join(root, "scripts", "install-merge.js");
401
+ const _require = createRequire(import.meta.url);
402
+ const { mergeSettings } = _require(installMergePath);
403
+ let existing = {};
404
+ if (fs.existsSync(destConfigPath)) {
405
+ try {
406
+ existing = JSON.parse(fs.readFileSync(destConfigPath, "utf8"));
407
+ }
408
+ catch {
409
+ existing = {};
410
+ }
411
+ }
412
+ const merged = mergeSettings(existing, managed);
413
+ const tmp = `${destConfigPath}.tmp.${process.pid}`;
414
+ fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}
415
+ `, "utf8");
416
+ fs.renameSync(tmp, destConfigPath);
417
+ // Write version stamp.
418
+ const installRecordDir = path.join(options.dest, ".flow-agents");
419
+ fs.mkdirSync(installRecordDir, { recursive: true });
420
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
421
+ const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "opencode", global: true };
422
+ const recordPath = path.join(installRecordDir, "install.json");
423
+ const recordTmp = `${recordPath}.tmp.${process.pid}`;
424
+ fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}
425
+ `, "utf8");
426
+ fs.renameSync(recordTmp, recordPath);
427
+ console.log(`Flow Agents global config merged for opencode in ${options.dest}`);
428
+ return 0;
429
+ }
430
+ // --global for codex: run install-codex-home.sh to install into the isolated Codex HOME.
431
+ // The codex --global path is ~/.flow-agents/codex (the isolated home IS codex's global).
432
+ // Pass through telemetry/console args and honor --dest override for sandbox testing.
433
+ if (options.global && options.runtime === "codex") {
434
+ const codexHomeScript = path.join(root, "scripts", "install-codex-home.sh");
435
+ if (!fs.existsSync(codexHomeScript)) {
436
+ console.error(`flow-agents init: install-codex-home.sh missing: ${codexHomeScript}`);
437
+ return 1;
438
+ }
439
+ const scriptArgs = [options.dest];
440
+ for (const sink of options.telemetrySinks)
441
+ scriptArgs.push("--telemetry-sink", sink);
442
+ if (options.consoleUrl)
443
+ scriptArgs.push("--console-url", options.consoleUrl);
444
+ if (options.consoleEndpoint)
445
+ scriptArgs.push("--console-endpoint", options.consoleEndpoint);
446
+ if (options.consoleTokenFile)
447
+ scriptArgs.push("--console-token-file", options.consoleTokenFile);
448
+ if (options.consoleTenant)
449
+ scriptArgs.push("--console-tenant", options.consoleTenant);
450
+ const result = spawnSync("bash", [codexHomeScript, ...scriptArgs], { env: { ...process.env }, encoding: "utf8", stdio: "inherit" });
451
+ if (result.error) {
452
+ console.error(`flow-agents init: unable to run install-codex-home.sh: ${result.error.message}`);
453
+ return 1;
454
+ }
455
+ return result.status ?? 1;
456
+ }
457
+ // --global for pi: NOT_VERIFIED (no documented global dir). Warn and fall through to workspace install.
458
+ if (options.global && options.runtime === "pi") {
459
+ console.warn(`flow-agents init: NOT_VERIFIED: pi has no documented global config directory. ` +
460
+ `The --global flag for pi is not verified against pi documentation. ` +
461
+ `Falling back to workspace default destination: ${options.dest}`);
294
462
  }
295
463
  const bundle = ensureBundle(options.runtime);
296
464
  const installed = installBundle(bundle, options);
@@ -337,39 +505,93 @@ function normalizeDogfoodRuntime(value) {
337
505
  throw new Error(`dogfood: unsupported runtime '${value}'; expected claude-code, codex, opencode, or pi`);
338
506
  }
339
507
  /**
340
- * Write the claude-code hook-wiring artifacts into dest.
508
+ * Write the claude-code hook-wiring artifacts into dest using merge semantics.
341
509
  * Reads dist/claude-code/.claude/settings.json (generated by build-bundles),
342
- * strips the permissive-mode permission keys (defaultMode, skipDangerousModePermissionPrompt),
343
- * and writes .claude/settings.json to dest.
510
+ * strips the permissive-mode permission keys (defaultMode, skipDangerousModePermissionPrompt)
511
+ * from the managed bundle settings, then MERGES into any existing dest settings.json —
512
+ * preserving user keys, auth, and non-flow-agents hooks. Writes version stamp.
344
513
  */
345
514
  function dogfoodClaudeCode(bundleRoot, dest) {
346
515
  const sourcePath = path.join(bundleRoot, ".claude", "settings.json");
347
516
  if (!fs.existsSync(sourcePath))
348
517
  throw new Error(`dogfood: bundle settings missing: ${sourcePath}`);
349
- const settings = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
518
+ const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
350
519
  // Remove permissive defaults that are only appropriate for installed workspaces.
351
520
  // These keys must not be present in the source repo's .claude/settings.json.
352
- delete settings["permissions"];
353
- delete settings["skipDangerousModePermissionPrompt"];
521
+ delete managed["permissions"];
522
+ delete managed["skipDangerousModePermissionPrompt"];
354
523
  const outDir = path.join(dest, ".claude");
355
524
  fs.mkdirSync(outDir, { recursive: true });
356
- fs.writeFileSync(path.join(outDir, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`, "utf8");
525
+ const destSettingsPath = path.join(outDir, "settings.json");
526
+ // Merge: read existing, strip FA hooks, append new FA hooks, preserve all other keys.
527
+ const installMergePath = path.join(root, "scripts", "install-merge.js");
528
+ const _require = createRequire(import.meta.url);
529
+ const { mergeSettings } = _require(installMergePath);
530
+ let existing = {};
531
+ if (fs.existsSync(destSettingsPath)) {
532
+ try {
533
+ existing = JSON.parse(fs.readFileSync(destSettingsPath, "utf8"));
534
+ }
535
+ catch {
536
+ existing = {};
537
+ }
538
+ }
539
+ const merged = mergeSettings(existing, managed);
540
+ const tmp = `${destSettingsPath}.tmp.${process.pid}`;
541
+ fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
542
+ fs.renameSync(tmp, destSettingsPath);
543
+ // Write version stamp.
544
+ const installRecordDir = path.join(dest, ".flow-agents");
545
+ fs.mkdirSync(installRecordDir, { recursive: true });
546
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
547
+ const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code" };
548
+ const recordPath = path.join(installRecordDir, "install.json");
549
+ const recordTmp = `${recordPath}.tmp.${process.pid}`;
550
+ fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
551
+ fs.renameSync(recordTmp, recordPath);
357
552
  }
358
553
  /**
359
- * Write the codex hook-wiring artifacts into dest.
360
- * Reads dist/codex/.codex/hooks.json and writes .codex/hooks.json to dest.
554
+ * Write the codex hook-wiring artifacts into dest using merge semantics.
555
+ * Reads dist/codex/.codex/hooks.json and MERGES into any existing dest hooks.json
556
+ * preserving user non-FA hook groups. Writes version stamp.
361
557
  * The monolithic .codex/config.toml is not written here because it contains
362
558
  * workspace settings (approvals_reviewer, features) that would override the
363
- * developer's existing codex configuration. Only the hooks file is written.
559
+ * developer's existing codex configuration. Only the hooks file is merged.
364
560
  */
365
561
  function dogfoodCodex(bundleRoot, dest) {
366
562
  const sourcePath = path.join(bundleRoot, ".codex", "hooks.json");
367
563
  if (!fs.existsSync(sourcePath))
368
564
  throw new Error(`dogfood: bundle hooks.json missing: ${sourcePath}`);
369
- const hooks = fs.readFileSync(sourcePath, "utf8");
565
+ const managed = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
370
566
  const outDir = path.join(dest, ".codex");
371
567
  fs.mkdirSync(outDir, { recursive: true });
372
- fs.writeFileSync(path.join(outDir, "hooks.json"), hooks, "utf8");
568
+ const destHooksPath = path.join(outDir, "hooks.json");
569
+ // Merge: read existing, strip FA hook-groups, append new FA hook-groups, preserve user groups.
570
+ const installMergePath = path.join(root, "scripts", "install-merge.js");
571
+ const _require = createRequire(import.meta.url);
572
+ const { mergeSettings } = _require(installMergePath);
573
+ let existing = {};
574
+ if (fs.existsSync(destHooksPath)) {
575
+ try {
576
+ existing = JSON.parse(fs.readFileSync(destHooksPath, "utf8"));
577
+ }
578
+ catch {
579
+ existing = {};
580
+ }
581
+ }
582
+ const merged = mergeSettings(existing, managed);
583
+ const tmp = `${destHooksPath}.tmp.${process.pid}`;
584
+ fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
585
+ fs.renameSync(tmp, destHooksPath);
586
+ // Write version stamp.
587
+ const installRecordDir = path.join(dest, ".flow-agents");
588
+ fs.mkdirSync(installRecordDir, { recursive: true });
589
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
590
+ const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "codex" };
591
+ const recordPath = path.join(installRecordDir, "install.json");
592
+ const recordTmp = `${recordPath}.tmp.${process.pid}`;
593
+ fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
594
+ fs.renameSync(recordTmp, recordPath);
373
595
  }
374
596
  /**
375
597
  * Write the opencode hook-wiring artifacts into dest.
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -438,12 +438,29 @@ function validateSidecarGroup(inputs, markdown, requireSidecars, requireCritique
438
438
  for (const dir of dirs) {
439
439
  const deliver = markdown.find((p) => path.dirname(p) === dir && p.includes("deliver") && !p.includes("plan") && !p.includes("review"));
440
440
  const delivered = deliver ? /status:\s*(delivered|accepted|archived)/i.test(readText(deliver)) : true;
441
- for (const name of ["state.json", "acceptance.json", ...(delivered ? ["evidence.json"] : []), "handoff.json"]) {
441
+ // ADR 0010 Phase 4b: trust.bundle is the primary artifact at the delivered phase.
442
+ // evidence.json is OPTIONAL when trust.bundle is present (still schema-validated when present).
443
+ // Hard-fail on evidence.json absence only when no trust.bundle exists for a delivered session.
444
+ const hasTrustBundle = fs.existsSync(path.join(dir, "trust.bundle"));
445
+ const evidenceRequired = delivered && !hasTrustBundle;
446
+ for (const name of ["state.json", "acceptance.json", ...(evidenceRequired ? ["evidence.json"] : []), "handoff.json"]) {
442
447
  if (!fs.existsSync(path.join(dir, name)))
443
448
  issues.push({ path: path.join(dir, name), message: "required sidecar is missing" });
444
449
  }
445
- if (requireCritique && !fs.existsSync(path.join(dir, "critique.json")))
450
+ // ADR 0010 Phase 4c: critique.json no longer written; trust.bundle carries critique claims. Accept either.
451
+ if (requireCritique && !fs.existsSync(path.join(dir, "critique.json")) && !fs.existsSync(path.join(dir, "trust.bundle")))
446
452
  issues.push({ path: path.join(dir, "critique.json"), message: "required sidecar is missing" });
453
+ // ADR 0010 Phase 4c: validate critique claims in trust.bundle (sole verification artifact).
454
+ const trustBundlePath = path.join(dir, "trust.bundle");
455
+ if (requireCritique && fs.existsSync(trustBundlePath)) {
456
+ const { value: bundleValue } = readJson(trustBundlePath);
457
+ if (bundleValue) {
458
+ const claims = Array.isArray(bundleValue.claims) ? bundleValue.claims : [];
459
+ const critiqueClaims = claims.filter((c) => c && c.claimType === "workflow.critique.review");
460
+ if (critiqueClaims.some((c) => c.value === "fail" || c.status === "disputed"))
461
+ issues.push({ path: trustBundlePath, message: "required critique must pass" });
462
+ }
463
+ }
447
464
  const acceptance = path.join(dir, "acceptance.json");
448
465
  if (deliver && fs.existsSync(acceptance)) {
449
466
  const expected = definitionAcceptanceCriteria(readText(deliver)).length;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;
@@ -0,0 +1,90 @@
1
+ import { createRequire } from "node:module";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs, flagString, flagList } from "../lib/args.js";
6
+ import { root } from "../tools/common.js";
7
+ /**
8
+ * flow-agents verify — CI trust anchor for downstream repos.
9
+ *
10
+ * Re-runs canonical verification FRESH in the current environment, then reconciles
11
+ * a delivered trust.bundle's claimed passes against those fresh results. Exits 1
12
+ * on divergence, fresh-verify failure, laundered commands, or no verify configured.
13
+ * Exits 0 on clean pass.
14
+ *
15
+ * This is a thin wrapper around scripts/ci/trust-reconcile.js: it resolves the
16
+ * script from the installed package root (via createRequire) and calls the exported
17
+ * runTrustReconcile() function directly so all output goes to the same process
18
+ * stdout/stderr without an extra subprocess layer.
19
+ *
20
+ * The script ships in the npm package `files` list, so downstream repos always have
21
+ * access to it at the same relative path from the package root.
22
+ */
23
+ const _require = createRequire(import.meta.url);
24
+ function usage() {
25
+ process.stderr.write("usage: flow-agents verify [--commands <cmd[,cmd...]>] [--bundle <path>] [--repo-root <path>]\n" +
26
+ "\n" +
27
+ "Re-runs canonical verification fresh and reconciles a delivered trust.bundle's\n" +
28
+ "claimed passes against CI results. Exits 1 on divergence, fresh-verify failure,\n" +
29
+ "compile-only/no-verify, or laundered commands. Exits 0 on clean pass.\n" +
30
+ "\n" +
31
+ "Options:\n" +
32
+ " --commands <cmd,...> Canonical verify command(s). Comma-separated or repeated.\n" +
33
+ " Falls back to TRUST_RECONCILE_COMMANDS env or\n" +
34
+ " package.json scripts['trust-reconcile-verify'].\n" +
35
+ " No-commands → fail-closed (compile-only refused).\n" +
36
+ " --bundle <path> Delivered trust.bundle or trust.checkpoint.json path.\n" +
37
+ " Falls back to TRUST_RECONCILE_BUNDLE env, then auto-\n" +
38
+ " discovers delivery/trust.bundle or delivery/trust.checkpoint.json.\n" +
39
+ " Absent bundle: only fresh verify is enforced (fail-open).\n" +
40
+ " --repo-root <path> Repository root. Default: TRUST_RECONCILE_REPO_ROOT env or cwd.\n" +
41
+ "\n" +
42
+ "Examples:\n" +
43
+ " # Re-run build+test fresh; reconcile against a delivered bundle:\n" +
44
+ " flow-agents verify --commands 'npm run build,npm test' --bundle delivery/trust.bundle\n" +
45
+ "\n" +
46
+ " # Fresh-verify only (no bundle), using package.json trust-reconcile-verify script:\n" +
47
+ " flow-agents verify\n");
48
+ }
49
+ export async function main(argv = process.argv.slice(2)) {
50
+ if (argv.includes("--help") || argv.includes("-h")) {
51
+ usage();
52
+ return 0;
53
+ }
54
+ const { flags } = parseArgs(argv);
55
+ // --commands may be specified multiple times or comma-separated within a single value.
56
+ const commandsRaw = flagList(flags, "commands");
57
+ const commands = commandsRaw.flatMap((c) => c.split(",").map((s) => s.trim()).filter(Boolean));
58
+ const bundle = flagString(flags, "bundle") ?? null;
59
+ const repoRoot = flagString(flags, "repo-root") ?? null;
60
+ // Resolve the trust-reconcile.js script from the installed package root.
61
+ // `root` (from tools/common.ts) walks up from this compiled file's directory until
62
+ // it finds a directory with both package.json and packaging/ — the package root.
63
+ // In a downstream installed package: node_modules/@kontourai/flow-agents/
64
+ // In the dev repo: the repo root itself.
65
+ // scripts/ci/trust-reconcile.js ships in the npm package `files` list.
66
+ const reconcilePath = path.join(root, "scripts", "ci", "trust-reconcile.js");
67
+ if (!fs.existsSync(reconcilePath)) {
68
+ process.stderr.write(`[flow-agents verify] error: trust-reconcile.js not found at ${reconcilePath}\n` +
69
+ "[flow-agents verify] Is the package correctly installed? Expected scripts/ci/trust-reconcile.js.\n");
70
+ return 1;
71
+ }
72
+ const { runTrustReconcile } = _require(reconcilePath);
73
+ return runTrustReconcile({ bundle, commands, repoRoot });
74
+ }
75
+ // Direct-script guard (mirrors pattern from other CLI subcommands).
76
+ const _selfReal = (() => { try {
77
+ return fs.realpathSync(fileURLToPath(import.meta.url));
78
+ }
79
+ catch {
80
+ return fileURLToPath(import.meta.url);
81
+ } })();
82
+ const _argv1Real = (() => { try {
83
+ return fs.realpathSync(process.argv[1]);
84
+ }
85
+ catch {
86
+ return process.argv[1];
87
+ } })();
88
+ if (_selfReal === _argv1Real) {
89
+ process.exitCode = await main();
90
+ }
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): number;