@kontourai/flow-agents 0.1.2 → 0.3.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 (117) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/release-please.yml +31 -0
  3. package/.github/workflows/runtime-compat.yml +118 -0
  4. package/CHANGELOG.md +46 -0
  5. package/CONTRIBUTING.md +4 -0
  6. package/README.md +80 -18
  7. package/build/src/cli/flow-kit.js +9 -4
  8. package/build/src/cli/init.js +215 -5
  9. package/build/src/cli/runtime-adapter.js +9 -5
  10. package/build/src/cli/telemetry-doctor.js +4 -1
  11. package/build/src/cli/utterance-check.js +65 -1
  12. package/build/src/runtime-adapters.js +34 -0
  13. package/build/src/tools/build-universal-bundles.js +285 -0
  14. package/build/src/tools/filter-installed-packs.js +3 -0
  15. package/build/src/tools/validate-source-tree.js +5 -1
  16. package/console.telemetry.json +115 -20
  17. package/context/scripts/telemetry/lib/config.sh +5 -1
  18. package/context/settings/flow-agents-settings.json +7 -0
  19. package/docs/_layouts/default.html +2 -0
  20. package/docs/context-map.md +1 -0
  21. package/docs/index.md +53 -4
  22. package/docs/integrations/conformance.md +246 -0
  23. package/docs/integrations/framework-adapter.md +275 -0
  24. package/docs/integrations/harness-install.md +213 -0
  25. package/docs/integrations/index.md +58 -0
  26. package/docs/integrations/knowledge-kit-live.md +211 -0
  27. package/docs/kit-authoring-guide.md +169 -0
  28. package/docs/north-star.md +2 -2
  29. package/docs/spec/runtime-hook-surface.md +525 -0
  30. package/docs/survey-utterance-check.md +211 -94
  31. package/docs/vision.md +45 -0
  32. package/evals/acceptance/run.sh +13 -2
  33. package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
  34. package/evals/acceptance/test_opencode_harness.sh +121 -0
  35. package/evals/acceptance/test_pi_harness.sh +113 -0
  36. package/evals/integration/test_bundle_install.sh +226 -1
  37. package/evals/integration/test_bundle_lifecycle.sh +641 -0
  38. package/evals/integration/test_runtime_adapter_activation.sh +113 -1
  39. package/evals/integration/test_utterance_check.sh +291 -44
  40. package/evals/run.sh +2 -0
  41. package/evals/static/test_universal_bundles.sh +137 -2
  42. package/integrations/strands/README.md +256 -0
  43. package/integrations/strands/example.py +74 -0
  44. package/integrations/strands/examples/knowledge_kit_live.py +461 -0
  45. package/integrations/strands/flow_agents_strands/__init__.py +27 -0
  46. package/integrations/strands/flow_agents_strands/hooks.py +194 -0
  47. package/integrations/strands/flow_agents_strands/policy.py +348 -0
  48. package/integrations/strands/flow_agents_strands/steering.py +225 -0
  49. package/integrations/strands/flow_agents_strands/telemetry.py +238 -0
  50. package/integrations/strands/pyproject.toml +38 -0
  51. package/integrations/strands/tests/__init__.py +0 -0
  52. package/integrations/strands/tests/test_hooks.py +392 -0
  53. package/integrations/strands/tests/test_policy.py +315 -0
  54. package/integrations/strands/tests/test_telemetry.py +184 -0
  55. package/integrations/strands-ts/README.md +224 -0
  56. package/integrations/strands-ts/bin/conformance-shim.mjs +257 -0
  57. package/integrations/strands-ts/package.json +53 -0
  58. package/integrations/strands-ts/src/hooks.ts +312 -0
  59. package/integrations/strands-ts/src/index.ts +22 -0
  60. package/integrations/strands-ts/src/policy.ts +345 -0
  61. package/integrations/strands-ts/src/telemetry.ts +251 -0
  62. package/integrations/strands-ts/test/test-policy.ts +322 -0
  63. package/integrations/strands-ts/test/test-steering.ts +159 -0
  64. package/integrations/strands-ts/test/test-telemetry.ts +226 -0
  65. package/integrations/strands-ts/tsconfig.json +20 -0
  66. package/kits/catalog.json +6 -0
  67. package/kits/knowledge/adapters/default-store/index.js +821 -0
  68. package/kits/knowledge/adapters/flow-runner/index.js +1179 -0
  69. package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
  70. package/kits/knowledge/docs/README.md +135 -0
  71. package/kits/knowledge/docs/store-contract.md +526 -0
  72. package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
  73. package/kits/knowledge/evals/contract-suite/suite.test.js +670 -0
  74. package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
  75. package/kits/knowledge/evals/synthesis/suite.test.js +909 -0
  76. package/kits/knowledge/flows/compile.flow.json +60 -0
  77. package/kits/knowledge/flows/consolidate.flow.json +77 -0
  78. package/kits/knowledge/flows/ingest.flow.json +60 -0
  79. package/kits/knowledge/flows/store-contract.flow.json +48 -0
  80. package/kits/knowledge/flows/synthesize.flow.json +77 -0
  81. package/kits/knowledge/kit.json +78 -0
  82. package/package.json +7 -2
  83. package/packaging/conformance/README.md +142 -0
  84. package/packaging/conformance/fixtures/config-protection--allow-no-path.json +18 -0
  85. package/packaging/conformance/fixtures/config-protection--allow-safe-file.json +20 -0
  86. package/packaging/conformance/fixtures/config-protection--block-biome.json +20 -0
  87. package/packaging/conformance/fixtures/config-protection--block-eslintrc.json +20 -0
  88. package/packaging/conformance/fixtures/quality-gate--allow-no-path.json +17 -0
  89. package/packaging/conformance/fixtures/quality-gate--allow-nonexistent-file.json +19 -0
  90. package/packaging/conformance/fixtures/stop-goal-fit--allow-clean-cwd.json +17 -0
  91. package/packaging/conformance/fixtures/stop-goal-fit--block-strict-mode.json +23 -0
  92. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +21 -0
  93. package/packaging/conformance/fixtures/workflow-steering--allow-no-state.json +16 -0
  94. package/packaging/conformance/fixtures/workflow-steering--inject-active-state.json +29 -0
  95. package/packaging/conformance/fixtures/workflow-steering--inject-subagent-steering.json +25 -0
  96. package/packaging/conformance/package.json +4 -0
  97. package/packaging/conformance/run-conformance.js +322 -0
  98. package/packaging/manifest.json +59 -0
  99. package/schemas/flow-agents-settings.schema.json +48 -0
  100. package/scripts/README.md +4 -0
  101. package/scripts/dogfood.js +16 -0
  102. package/scripts/hooks/opencode-hook-adapter.js +123 -0
  103. package/scripts/hooks/opencode-telemetry-hook.js +101 -0
  104. package/scripts/hooks/pi-hook-adapter.js +123 -0
  105. package/scripts/hooks/pi-telemetry-hook.js +105 -0
  106. package/scripts/hooks/run-hook.js +8 -0
  107. package/scripts/hooks/utterance-check.js +124 -22
  108. package/scripts/telemetry/lib/config.sh +5 -1
  109. package/src/cli/flow-kit.ts +10 -4
  110. package/src/cli/init.ts +219 -6
  111. package/src/cli/runtime-adapter.ts +10 -5
  112. package/src/cli/telemetry-doctor.ts +4 -1
  113. package/src/cli/utterance-check.ts +71 -1
  114. package/src/runtime-adapters.ts +35 -0
  115. package/src/tools/build-universal-bundles.ts +283 -0
  116. package/src/tools/filter-installed-packs.ts +3 -0
  117. package/src/tools/validate-source-tree.ts +5 -1
@@ -13,12 +13,67 @@ const runtimeBundles = {
13
13
  codex: "codex",
14
14
  "claude-code": "claude-code",
15
15
  kiro: "kiro",
16
+ opencode: "opencode",
17
+ pi: "pi",
16
18
  };
19
+ // Stable marker present in every Flow Agents claude-code hook command.
20
+ // Used by scope-collision detection to identify an existing flow-agents install.
21
+ // Marker must be distinctive to Flow Agents generated settings. Sibling
22
+ // products from the same lineage ship identically named hook scripts such
23
+ // as claude-hook-adapter.js, so script filenames are NOT a safe marker.
24
+ export const COLLISION_MARKER = "Recording Flow Agents telemetry";
25
+ /**
26
+ * Check whether a user-level Claude Code settings file already contains
27
+ * Flow Agents hook commands. If it does, print a WARNING explaining that
28
+ * Claude Code merges user-level and project-level settings and runs ALL
29
+ * matching hooks, so having flow-agents in both places causes duplicate
30
+ * hook execution (double telemetry, double policy enforcement).
31
+ *
32
+ * The check does NOT block the install; it is advisory only.
33
+ *
34
+ * @param userSettingsFile Path to inspect (defaults to $HOME/.claude/settings.json;
35
+ * overridable via FLOW_AGENTS_USER_CLAUDE_SETTINGS env var for testability).
36
+ * @returns true if a collision was detected, false otherwise.
37
+ */
38
+ export function checkScopeCollision(userSettingsFile) {
39
+ const filePath = userSettingsFile
40
+ ?? process.env["FLOW_AGENTS_USER_CLAUDE_SETTINGS"]
41
+ ?? path.join(os.homedir(), ".claude", "settings.json");
42
+ if (!fs.existsSync(filePath))
43
+ return false;
44
+ let text;
45
+ try {
46
+ text = fs.readFileSync(filePath, "utf8");
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ if (!text.includes(COLLISION_MARKER))
52
+ return false;
53
+ console.warn(`\nWARNING: Flow Agents scope collision detected.\n` +
54
+ ` ${filePath}\n` +
55
+ `already contains Flow Agents hook commands (marker: ${COLLISION_MARKER}).\n` +
56
+ `\n` +
57
+ `Claude Code merges user-level (~/.claude/settings.json) and project-level\n` +
58
+ `(.claude/settings.json) settings, then runs ALL matching hooks from both files.\n` +
59
+ `Installing Flow Agents at the project level while it is also present at the\n` +
60
+ `user level will cause duplicate hook execution: telemetry events are recorded\n` +
61
+ `twice and policy hooks (workflow-steering, config-protection, quality-gate,\n` +
62
+ `stop-goal-fit) run twice per event.\n` +
63
+ `\n` +
64
+ `To resolve:\n` +
65
+ ` - Remove the hooks section from ${filePath} and rely solely on the\n` +
66
+ ` project-level .claude/settings.json installed by flow-agents init, OR\n` +
67
+ ` - Remove the project-level install and keep only the user-level one.\n` +
68
+ `\n` +
69
+ `The install will continue; resolve the collision before running Claude Code.\n`);
70
+ return true;
71
+ }
17
72
  function usage() {
18
73
  console.error(`usage: flow-agents init [options]
19
74
 
20
75
  Options:
21
- --runtime base|codex|claude-code|kiro
76
+ --runtime base|codex|claude-code|kiro|opencode|pi
22
77
  --dest PATH
23
78
  --telemetry-sink local-files|local-kontour-console|kontour-hosted-console|user-hosted-console
24
79
  --console-url URL
@@ -35,9 +90,9 @@ function normalizeRuntime(value) {
35
90
  return undefined;
36
91
  if (value === "claude")
37
92
  return "claude-code";
38
- if (value === "base" || value === "codex" || value === "claude-code" || value === "kiro")
93
+ if (value === "base" || value === "codex" || value === "claude-code" || value === "kiro" || value === "opencode" || value === "pi")
39
94
  return value;
40
- throw new Error(`unknown runtime '${value}'; expected base, codex, claude-code, or kiro`);
95
+ throw new Error(`unknown runtime '${value}'; expected base, codex, claude-code, kiro, opencode, or pi`);
41
96
  }
42
97
  function normalizeTelemetrySink(value) {
43
98
  if (value === "local-files" || value === "local-kontour-console" || value === "kontour-hosted-console" || value === "user-hosted-console" || value === "kontour-cloud" || value === "hosted-kontour-console")
@@ -67,7 +122,7 @@ async function questionHidden(prompt) {
67
122
  let value = "";
68
123
  const onData = (buffer) => {
69
124
  const text = buffer.toString("utf8");
70
- if (text === "\u0003") {
125
+ if (text === "") {
71
126
  stdout.write("\n");
72
127
  process.exit(130);
73
128
  }
@@ -78,7 +133,7 @@ async function questionHidden(prompt) {
78
133
  resolve(value);
79
134
  return;
80
135
  }
81
- if (text === "\u007f") {
136
+ if (text === "") {
82
137
  value = value.slice(0, -1);
83
138
  return;
84
139
  }
@@ -224,6 +279,18 @@ export async function main(argv = process.argv.slice(2)) {
224
279
  const headless = argv.includes("--yes") || argv.includes("--headless") || !process.stdin.isTTY;
225
280
  try {
226
281
  const options = headless ? headlessOptions(argv) : await interactiveOptions(argv);
282
+ // Scope-collision check for claude-code: Claude Code merges user-level
283
+ // (~/.claude/settings.json) and project-level (.claude/settings.json) settings
284
+ // and runs ALL matching hooks from both files. If a user-level settings file
285
+ // already contains flow-agents hooks, installing at the project level will
286
+ // cause duplicate hook execution. We warn but do not block.
287
+ //
288
+ // Codex note: Codex hooks live in .codex/hooks.json (project-level only).
289
+ // There is no well-known user-level codex hooks file in our install paths,
290
+ // so no collision check is needed for codex.
291
+ if (options.runtime === "claude-code") {
292
+ checkScopeCollision();
293
+ }
227
294
  const bundle = ensureBundle(options.runtime);
228
295
  const installed = installBundle(bundle, options);
229
296
  if (installed !== 0)
@@ -235,5 +302,148 @@ export async function main(argv = process.argv.slice(2)) {
235
302
  return 2;
236
303
  }
237
304
  }
305
+ // ---------------------------------------------------------------------------
306
+ // Dogfood subcommand
307
+ //
308
+ // `flow-agents dogfood --runtime claude-code [--dest PATH]`
309
+ //
310
+ // Writes only the hook-wiring artifacts for the specified runtime into the
311
+ // target directory (default: cwd). Unlike a full install, dogfood:
312
+ // - Does NOT rsync the full bundle (no agents/skills duplication).
313
+ // - Reads the generated settings/config from dist/<runtime>/ so the output
314
+ // cannot drift from what the bundle generates (DRY guarantee).
315
+ // - For claude-code: OMITS permissions.defaultMode and
316
+ // skipDangerousModePermissionPrompt (permissive defaults are for installed
317
+ // workspaces, not source repos).
318
+ // - Runs the same scope-collision warning as init.
319
+ // ---------------------------------------------------------------------------
320
+ function dogfoodUsage() {
321
+ console.error(`usage: flow-agents dogfood [options]
322
+
323
+ Options:
324
+ --runtime claude-code|codex|opencode|pi (required)
325
+ --dest PATH (default: cwd)
326
+ --yes, --headless
327
+ `);
328
+ }
329
+ function normalizeDogfoodRuntime(value) {
330
+ if (!value)
331
+ return undefined;
332
+ if (value === "claude" || value === "claude-code")
333
+ return "claude-code";
334
+ if (value === "codex" || value === "opencode" || value === "pi")
335
+ return value;
336
+ throw new Error(`dogfood: unsupported runtime '${value}'; expected claude-code, codex, opencode, or pi`);
337
+ }
338
+ /**
339
+ * Write the claude-code hook-wiring artifacts into dest.
340
+ * Reads dist/claude-code/.claude/settings.json (generated by build-bundles),
341
+ * strips the permissive-mode permission keys (defaultMode, skipDangerousModePermissionPrompt),
342
+ * and writes .claude/settings.json to dest.
343
+ */
344
+ function dogfoodClaudeCode(bundleRoot, dest) {
345
+ const sourcePath = path.join(bundleRoot, ".claude", "settings.json");
346
+ if (!fs.existsSync(sourcePath))
347
+ throw new Error(`dogfood: bundle settings missing: ${sourcePath}`);
348
+ const settings = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
349
+ // Remove permissive defaults that are only appropriate for installed workspaces.
350
+ // These keys must not be present in the source repo's .claude/settings.json.
351
+ delete settings["permissions"];
352
+ delete settings["skipDangerousModePermissionPrompt"];
353
+ const outDir = path.join(dest, ".claude");
354
+ fs.mkdirSync(outDir, { recursive: true });
355
+ fs.writeFileSync(path.join(outDir, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`, "utf8");
356
+ }
357
+ /**
358
+ * Write the codex hook-wiring artifacts into dest.
359
+ * Reads dist/codex/.codex/hooks.json and writes .codex/hooks.json to dest.
360
+ * The monolithic .codex/config.toml is not written here because it contains
361
+ * workspace settings (approvals_reviewer, features) that would override the
362
+ * developer's existing codex configuration. Only the hooks file is written.
363
+ */
364
+ function dogfoodCodex(bundleRoot, dest) {
365
+ const sourcePath = path.join(bundleRoot, ".codex", "hooks.json");
366
+ if (!fs.existsSync(sourcePath))
367
+ throw new Error(`dogfood: bundle hooks.json missing: ${sourcePath}`);
368
+ const hooks = fs.readFileSync(sourcePath, "utf8");
369
+ const outDir = path.join(dest, ".codex");
370
+ fs.mkdirSync(outDir, { recursive: true });
371
+ fs.writeFileSync(path.join(outDir, "hooks.json"), hooks, "utf8");
372
+ }
373
+ /**
374
+ * Write the opencode hook-wiring artifacts into dest.
375
+ * Reads dist/opencode/.opencode/plugins/flow-agents.js and opencode.json,
376
+ * and writes them into dest. These are the minimal hook-wiring files; the
377
+ * full skill/agent tree is not copied.
378
+ */
379
+ function dogfoodOpencode(bundleRoot, dest) {
380
+ const pluginSource = path.join(bundleRoot, ".opencode", "plugins", "flow-agents.js");
381
+ const configSource = path.join(bundleRoot, "opencode.json");
382
+ if (!fs.existsSync(pluginSource))
383
+ throw new Error(`dogfood: bundle plugin missing: ${pluginSource}`);
384
+ const pluginDir = path.join(dest, ".opencode", "plugins");
385
+ fs.mkdirSync(pluginDir, { recursive: true });
386
+ fs.copyFileSync(pluginSource, path.join(pluginDir, "flow-agents.js"));
387
+ // Write opencode.json only if it does not already exist to avoid clobbering
388
+ // any workspace-specific opencode configuration.
389
+ const destConfig = path.join(dest, "opencode.json");
390
+ if (!fs.existsSync(destConfig) && fs.existsSync(configSource)) {
391
+ fs.copyFileSync(configSource, destConfig);
392
+ }
393
+ }
394
+ /**
395
+ * Write the pi hook-wiring artifacts into dest.
396
+ * Reads dist/pi/.pi/extensions/flow-agents.ts and writes it to dest.
397
+ * The extension is the only hook-wiring file needed for pi.
398
+ */
399
+ function dogfoodPi(bundleRoot, dest) {
400
+ const extSource = path.join(bundleRoot, ".pi", "extensions", "flow-agents.ts");
401
+ if (!fs.existsSync(extSource))
402
+ throw new Error(`dogfood: bundle extension missing: ${extSource}`);
403
+ const extDir = path.join(dest, ".pi", "extensions");
404
+ fs.mkdirSync(extDir, { recursive: true });
405
+ fs.copyFileSync(extSource, path.join(extDir, "flow-agents.ts"));
406
+ }
407
+ export async function mainDogfood(argv = process.argv.slice(2)) {
408
+ if (argv.includes("--help") || argv.includes("-h")) {
409
+ dogfoodUsage();
410
+ return 0;
411
+ }
412
+ const args = parseArgs(argv);
413
+ try {
414
+ const runtimeRaw = flagString(args.flags, "runtime");
415
+ const runtime = normalizeDogfoodRuntime(runtimeRaw);
416
+ if (!runtime) {
417
+ console.error("dogfood: --runtime is required (claude-code, codex, opencode, or pi)");
418
+ dogfoodUsage();
419
+ return 2;
420
+ }
421
+ const dest = path.resolve(flagString(args.flags, "dest") ?? process.cwd());
422
+ // Ensure the bundle for the requested runtime is built.
423
+ const bundleRuntime = runtime;
424
+ const bundleRoot = ensureBundle(bundleRuntime);
425
+ // Scope-collision check: warn if user-level claude settings already has flow-agents.
426
+ // Codex: no user-level hooks file in our install paths — skip with note above.
427
+ if (runtime === "claude-code") {
428
+ checkScopeCollision();
429
+ }
430
+ // Write only the hook-wiring artifacts, not the full bundle.
431
+ fs.mkdirSync(dest, { recursive: true });
432
+ if (runtime === "claude-code")
433
+ dogfoodClaudeCode(bundleRoot, dest);
434
+ else if (runtime === "codex")
435
+ dogfoodCodex(bundleRoot, dest);
436
+ else if (runtime === "opencode")
437
+ dogfoodOpencode(bundleRoot, dest);
438
+ else if (runtime === "pi")
439
+ dogfoodPi(bundleRoot, dest);
440
+ console.log(`Flow Agents dogfood hooks wired for ${runtime} in ${dest}`);
441
+ return 0;
442
+ }
443
+ catch (error) {
444
+ console.error(`flow-agents dogfood: ${error.message}`);
445
+ return 2;
446
+ }
447
+ }
238
448
  if (import.meta.url === `file://${process.argv[1]}`)
239
449
  process.exit(await main());
@@ -1,21 +1,25 @@
1
1
  import * as path from "node:path";
2
2
  import { parseArgs, flagString } from "../lib/args.js";
3
- import { activateCodexLocal } from "../runtime-adapters.js";
3
+ import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
4
+ const AVAILABLE_ADAPTERS = ["codex-local", "strands-local"];
4
5
  export function main(argv = process.argv.slice(2)) {
5
6
  const [command, ...rest] = argv;
6
7
  if (command !== "activate") {
7
- console.error("usage: runtime-adapter activate [--dest DIR] [--source-root DIR] [--adapter codex-local]");
8
+ console.error("usage: runtime-adapter activate [--dest DIR] [--source-root DIR] [--adapter codex-local|strands-local]");
8
9
  return 2;
9
10
  }
10
11
  const args = parseArgs(rest);
11
12
  const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
12
13
  const sourceRoot = path.resolve(flagString(args.flags, "source-root", path.resolve(path.dirname(process.argv[1]), "..")) ?? ".");
13
14
  const adapter = flagString(args.flags, "adapter");
14
- if (adapter && adapter !== "codex-local") {
15
- console.log(JSON.stringify({ selected_adapter: null, available_adapters: ["codex-local"], supported_asset_classes: [], generated_runtime_files: [], skipped_assets: [], warnings: [], errors: [`unknown runtime adapter '${adapter}'; available adapters: codex-local`] }, null, 2));
15
+ if (adapter && !AVAILABLE_ADAPTERS.includes(adapter)) {
16
+ console.log(JSON.stringify({ selected_adapter: null, available_adapters: AVAILABLE_ADAPTERS, supported_asset_classes: [], generated_runtime_files: [], skipped_assets: [], warnings: [], errors: [`unknown runtime adapter '${adapter}'; available adapters: ${AVAILABLE_ADAPTERS.join(", ")}`] }, null, 2));
16
17
  return 2;
17
18
  }
18
- const result = activateCodexLocal(sourceRoot, dest);
19
+ // Default to codex-local for backward compatibility; strands-local is opt-in via --adapter.
20
+ const result = adapter === "strands-local"
21
+ ? activateStrandsLocal(sourceRoot, dest)
22
+ : activateCodexLocal(sourceRoot, dest);
19
23
  console.log(JSON.stringify(result, null, 2));
20
24
  return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
21
25
  }
@@ -46,7 +46,10 @@ function channelConfigValue(config, channel, key, fallback = "") {
46
46
  }
47
47
  function telemetryDataDir(dest) {
48
48
  const configured = process.env.TELEMETRY_DATA_DIR;
49
- return configured ? path.resolve(dest, configured) : path.resolve(dest, "..", ".telemetry");
49
+ // Must mirror scripts/telemetry/lib/config.sh: the sink lives INSIDE the
50
+ // workspace at <dest>/.telemetry. The previous "../.telemetry" duplicated
51
+ // the parent-escape bug fixed in config.sh on 2026-06-11.
52
+ return configured ? path.resolve(dest, configured) : path.resolve(dest, ".telemetry");
50
53
  }
51
54
  function deriveConsoleEndpoint(consoleUrl, explicitEndpoint) {
52
55
  if (explicitEndpoint)
@@ -15,6 +15,9 @@ function usage() {
15
15
  " --utterance TEXT Utterance text to check (required unless --not-configured).",
16
16
  " --bundle-path FILE Trust bundle JSON file. Omit for an empty bundle (all unsupported).",
17
17
  " --agent-id ID Agent identifier for provenance (default: flow-agents-utterance-check).",
18
+ " --extractor NAME Extractor to use: 'reference' (default, pattern-based) or 'anthropic'",
19
+ " (model-backed, requires ANTHROPIC_API_KEY and @kontourai/survey/anthropic).",
20
+ " --model MODEL Model for the anthropic extractor (e.g. claude-haiku-4-5).",
18
21
  " --not-configured Skip survey call; output not_configured without error.",
19
22
  " --strict Exit non-zero when any badge is disputed, rejected, or unsupported.",
20
23
  " --help Show this help.",
@@ -51,6 +54,43 @@ async function loadSurvey() {
51
54
  return undefined;
52
55
  }
53
56
  }
57
+ /**
58
+ * Dynamically import @kontourai/survey/anthropic and create the Anthropic extractor.
59
+ * Fails open with a clear not_configured message when the key or peer dep is missing.
60
+ */
61
+ async function loadAnthropicExtractor(model) {
62
+ const apiKey = process.env.ANTHROPIC_API_KEY;
63
+ if (!apiKey) {
64
+ return {
65
+ notConfigured: true,
66
+ reason: "anthropic extractor requires ANTHROPIC_API_KEY to be set. " +
67
+ "Set the environment variable or switch extractor to 'reference'.",
68
+ };
69
+ }
70
+ try {
71
+ const pkg = "@kontourai/survey/anthropic";
72
+ const mod = await Function("m", "return import(m)")(pkg);
73
+ if (typeof mod.createAnthropicUtteranceExtractor !== "function") {
74
+ return {
75
+ notConfigured: true,
76
+ reason: "@kontourai/survey/anthropic does not export createAnthropicUtteranceExtractor. " +
77
+ "Update @kontourai/survey to a version that supports the anthropic extractor.",
78
+ };
79
+ }
80
+ const opts = { apiKey };
81
+ if (model)
82
+ opts.model = model;
83
+ return mod.createAnthropicUtteranceExtractor(opts);
84
+ }
85
+ catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ return {
88
+ notConfigured: true,
89
+ reason: `@kontourai/survey/anthropic is not available: ${msg}. ` +
90
+ "Install @kontourai/survey with the anthropic subpath export, or switch extractor to 'reference'.",
91
+ };
92
+ }
93
+ }
54
94
  // ---------------------------------------------------------------------------
55
95
  // Core check logic
56
96
  // ---------------------------------------------------------------------------
@@ -63,6 +103,8 @@ async function runCheck(argv) {
63
103
  const agentId = flagString(flags, "agent-id") ?? "flow-agents-utterance-check";
64
104
  const notConfigured = flagBool(flags, "not-configured");
65
105
  const strict = flagBool(flags, "strict");
106
+ const extractorName = flagString(flags, "extractor") ?? "reference";
107
+ const model = flagString(flags, "model");
66
108
  if (notConfigured) {
67
109
  const report = {
68
110
  status: "not_configured",
@@ -106,9 +148,31 @@ async function runCheck(argv) {
106
148
  return 1;
107
149
  }
108
150
  const { surveyAgentUtterance, referenceUtteranceExtractor } = survey;
151
+ // Resolve which extractor to use.
152
+ let extractor;
153
+ if (extractorName === "anthropic") {
154
+ const anthropicResult = await loadAnthropicExtractor(model);
155
+ if ("notConfigured" in anthropicResult) {
156
+ // Fail open: emit not_configured with a clear reason rather than erroring.
157
+ const report = {
158
+ status: "not_configured",
159
+ agent_id: agentId,
160
+ utterance_excerpt: excerptText(utterance),
161
+ statements: [],
162
+ summary: anthropicResult.reason,
163
+ };
164
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
165
+ process.stderr.write(`[UtteranceCheck] not_configured: ${anthropicResult.reason}\n`);
166
+ return 0;
167
+ }
168
+ extractor = anthropicResult;
169
+ }
170
+ else {
171
+ extractor = referenceUtteranceExtractor;
172
+ }
109
173
  let trustReport;
110
174
  try {
111
- trustReport = await surveyAgentUtterance(utterance, referenceUtteranceExtractor, {
175
+ trustReport = await surveyAgentUtterance(utterance, extractor, {
112
176
  bundle,
113
177
  agentId,
114
178
  });
@@ -144,3 +144,37 @@ export function activateCodexLocal(sourceRoot, dest) {
144
144
  generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "codex-local.activation", source_path: manifestPath.split(path.sep).join("/") });
145
145
  return { selected_adapter: "codex-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
146
146
  }
147
+ // Decision Q3 (Issue #32): Option (a) — new adapter id "strands-local" rather than
148
+ // loading kit flows inside FlowAgentsHooks. Rationale: activation is a workspace-prep
149
+ // concern (reads catalog + installed kits, writes runtime files, produces diagnostics).
150
+ // Keeping it in the CLI adapter layer maximises reuse of readKitInventory and safeSegment,
151
+ // mirrors the codex-local pattern exactly, and keeps framework adapters free of catalog-
152
+ // layout knowledge. The Strands steering layer then reads the written runtime files.
153
+ export function activateStrandsLocal(sourceRoot, dest) {
154
+ const inventory = readKitInventory(sourceRoot, dest);
155
+ // Runtime flows land at .flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json
156
+ // so the Strands steering context can glob for *.flow.json under this path.
157
+ const runtimeDir = path.join(dest, ".flow-agents", "runtime", "strands");
158
+ const generated = [];
159
+ const skipped = [];
160
+ for (const asset of inventory.assets) {
161
+ if (asset.asset_class !== "flows") {
162
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is diagnostic-only for strands-local" });
163
+ continue;
164
+ }
165
+ if (!asset.asset_id) {
166
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: null, reason: "flow asset is missing an id" });
167
+ continue;
168
+ }
169
+ const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
170
+ fs.mkdirSync(path.dirname(output), { recursive: true });
171
+ fs.copyFileSync(asset.source_path, output);
172
+ generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
173
+ }
174
+ fs.mkdirSync(runtimeDir, { recursive: true });
175
+ const manifest = { schema_version: "1.0", adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
176
+ const manifestPath = path.join(runtimeDir, "activation.json");
177
+ writeJson(manifestPath, manifest);
178
+ generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "strands-local.activation", source_path: manifestPath.split(path.sep).join("/") });
179
+ return { selected_adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
180
+ }