@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
@@ -86,16 +86,128 @@ elif node - "$UNKNOWN_OUT" <<'NODE'
86
86
  const fs = require("node:fs");
87
87
  const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
88
88
  if (!data.available_adapters?.includes("codex-local")) throw new Error("available adapters missing codex-local");
89
+ if (!data.available_adapters?.includes("strands-local")) throw new Error("available adapters missing strands-local");
89
90
  if (!data.errors?.length) throw new Error("unknown adapter did not report errors");
90
91
  console.log("ok");
91
92
  NODE
92
93
  then
93
- pass "unknown adapter reports available adapters"
94
+ pass "unknown adapter reports available adapters (codex-local and strands-local)"
94
95
  else
95
96
  fail "unknown adapter diagnostics missing"
96
97
  sed -n '1,120p' "$UNKNOWN_OUT"
97
98
  fi
98
99
 
100
+ # -------------------------------------------------------------------------
101
+ # strands-local adapter activation (Issue #32 AC1)
102
+ # -------------------------------------------------------------------------
103
+
104
+ echo ""
105
+ echo "=== strands-local Adapter Activation Checks (Issue #32 AC1) ==="
106
+
107
+ STRANDS_DEST="$TMP_DIR/strands-dest"
108
+ STRANDS_OUT="$TMP_DIR/strands-activation.json"
109
+ mkdir -p "$STRANDS_DEST"
110
+
111
+ # Use the builder kit (stable fixture) — activate for strands-local from the repo source root
112
+ if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --adapter strands-local --format json >"$STRANDS_OUT" 2>&1; then
113
+ pass "strands-local activation succeeds"
114
+ else
115
+ fail "strands-local activation failed"
116
+ sed -n '1,220p' "$STRANDS_OUT"
117
+ fi
118
+
119
+ if node - "$STRANDS_OUT" "$STRANDS_DEST" "$ROOT/kits/catalog.json" <<'NODE'
120
+ const fs = require("node:fs");
121
+ const path = require("node:path");
122
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
123
+ const dest = process.argv[3];
124
+ const catalog = process.argv[4];
125
+
126
+ // Verify selected_adapter
127
+ if (data.selected_adapter !== "strands-local") throw new Error(`expected strands-local, got: ${data.selected_adapter}`);
128
+ if (JSON.stringify(data.supported_asset_classes) !== JSON.stringify(["flows"])) throw new Error(`unexpected supported_asset_classes: ${JSON.stringify(data.supported_asset_classes)}`);
129
+
130
+ // Verify builder kit flows are generated (builder kit is in catalog.json)
131
+ const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
132
+ for (const expected of ["builder.shape", "builder.build", "strands-local.activation"]) {
133
+ if (!ids.has(expected)) throw new Error(`missing generated asset: ${expected}`);
134
+ }
135
+
136
+ // Verify generated runtime files actually exist on disk
137
+ for (const item of data.generated_runtime_files) {
138
+ if (item.asset_class === "activation-manifest") continue;
139
+ const generatedPath = path.join(dest, item.path);
140
+ if (!fs.existsSync(generatedPath)) throw new Error(`generated file missing: ${generatedPath}`);
141
+ // Verify runtime files are under .flow-agents/runtime/strands/flows/
142
+ if (!item.path.includes(".flow-agents/runtime/strands/flows/")) {
143
+ throw new Error(`generated path not under strands runtime dir: ${item.path}`);
144
+ }
145
+ }
146
+
147
+ // Verify activation.json written at strands runtime dir
148
+ const manifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
149
+ if (!fs.existsSync(manifestPath)) throw new Error("strands runtime activation.json missing");
150
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
151
+ if (manifest.adapter !== "strands-local") throw new Error(`activation.json adapter mismatch: ${manifest.adapter}`);
152
+ if (!Array.isArray(manifest.skipped_assets)) throw new Error("activation.json missing skipped_assets array");
153
+
154
+ // Verify skipped_assets have expected fields (parity with codex-local)
155
+ for (const item of manifest.skipped_assets) {
156
+ for (const key of ["asset_class", "path", "kit_id", "asset_id", "reason"]) {
157
+ if (!(key in item)) throw new Error(`skipped asset missing ${key}: ${JSON.stringify(item)}`);
158
+ }
159
+ if (!item.reason.includes("diagnostic-only")) throw new Error(`unexpected skip reason: ${item.reason}`);
160
+ }
161
+
162
+ // Non-flow asset classes should appear in skipped_assets
163
+ const skippedClasses = new Set(manifest.skipped_assets.map((item) => item.asset_class));
164
+ // builder kit has flows only; skipped_assets check requires a kit with non-flow assets,
165
+ // which the codex-local path already validates via mixed-runtime-kit above.
166
+ // Here we just confirm the field structure is present.
167
+ if (!Array.isArray(data.skipped_assets)) throw new Error("result skipped_assets is not an array");
168
+
169
+ // Catalog not mutated
170
+ if (path.resolve(catalog) === path.resolve(path.join(dest, ".flow-agents/runtime/strands/activation.json"))) {
171
+ throw new Error("activation generated over kits/catalog.json");
172
+ }
173
+
174
+ console.log("ok");
175
+ NODE
176
+ then
177
+ pass "strands-local: runtime flow files, activation.json, and skipped_assets present with correct structure"
178
+ else
179
+ fail "strands-local: activation diagnostics incomplete or incorrect"
180
+ sed -n '1,220p' "$STRANDS_OUT"
181
+ fi
182
+
183
+ # Verify codex-local activation is still intact (AC3 — existing tests still pass)
184
+ if flow_agents_node "$CLI" activate --dest "$STRANDS_DEST" --source-root "$ROOT" --format json >"$TMP_DIR/codex-after-strands.json" 2>&1; then
185
+ pass "codex-local still activates after strands-local has run"
186
+ else
187
+ fail "codex-local activation failed after strands-local activation"
188
+ sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
189
+ fi
190
+
191
+ if node - "$TMP_DIR/codex-after-strands.json" "$STRANDS_DEST" <<'NODE'
192
+ const fs = require("node:fs");
193
+ const path = require("node:path");
194
+ const data = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
195
+ const dest = process.argv[3];
196
+ if (data.selected_adapter !== "codex-local") throw new Error(`expected codex-local, got: ${data.selected_adapter}`);
197
+ const manifestPath = path.join(dest, ".flow-agents/runtime/codex/activation.json");
198
+ if (!fs.existsSync(manifestPath)) throw new Error("codex activation.json still not present");
199
+ // Strands runtime dir must also still exist
200
+ const strandsManifestPath = path.join(dest, ".flow-agents/runtime/strands/activation.json");
201
+ if (!fs.existsSync(strandsManifestPath)) throw new Error("strands activation.json was removed by codex-local run");
202
+ console.log("ok");
203
+ NODE
204
+ then
205
+ pass "codex-local and strands-local runtime dirs co-exist independently (AC3)"
206
+ else
207
+ fail "co-existence check failed"
208
+ sed -n '1,220p' "$TMP_DIR/codex-after-strands.json"
209
+ fi
210
+
99
211
  echo ""
100
212
  if [[ "$errors" -eq 0 ]]; then
101
213
  echo "Runtime adapter activation checks passed."
@@ -22,17 +22,17 @@ HOOK="$ROOT/scripts/hooks/utterance-check.js"
22
22
  RUN_HOOK="$ROOT/scripts/hooks/run-hook.js"
23
23
 
24
24
  # ---------------------------------------------------------------------------
25
- # Hook: pass-through when disabled (default)
25
+ # Hook: pass-through when disabled by default (no config, no env var)
26
26
  # ---------------------------------------------------------------------------
27
27
 
28
28
  echo ""
29
- echo "--- hook: disabled by default ---"
29
+ echo "--- hook: disabled by default (no config, no env var) ---"
30
30
 
31
31
  INPUT_JSON='{"hook_event_name":"PostToolUse","tool_response":"The coverage is 92% and all tests pass."}'
32
32
 
33
33
  if node "$HOOK" >"$TMPDIR_EVAL/disabled.out" 2>"$TMPDIR_EVAL/disabled.err" <<< "$INPUT_JSON"; then
34
34
  if grep -qF '"hook_event_name"' "$TMPDIR_EVAL/disabled.out"; then
35
- _pass "utterance check hook passes through when FLOW_AGENTS_UTTERANCE_CHECK_ENABLED is unset"
35
+ _pass "utterance check hook passes through when no config and FLOW_AGENTS_UTTERANCE_CHECK_ENABLED is unset"
36
36
  else
37
37
  _fail "utterance check hook pass-through output was not the raw input"
38
38
  fi
@@ -40,6 +40,63 @@ else
40
40
  _fail "utterance check hook should exit 0 when disabled"
41
41
  fi
42
42
 
43
+ # ---------------------------------------------------------------------------
44
+ # Hook: env var force-off overrides a config that would enable
45
+ # ---------------------------------------------------------------------------
46
+
47
+ echo ""
48
+ echo "--- hook: env var force-off overrides config ---"
49
+
50
+ # Create a temp repo dir with a config that has enabled:true
51
+ FAKE_REPO="$TMPDIR_EVAL/fake-repo"
52
+ mkdir -p "$FAKE_REPO/context/settings"
53
+ cat > "$FAKE_REPO/AGENTS.md" <<'AGENTS_EOF'
54
+ # Fake repo for testing
55
+ AGENTS_EOF
56
+ cat > "$FAKE_REPO/context/settings/flow-agents-settings.json" <<'CONFIG_EOF'
57
+ {"schema_version":"1.0","utteranceCheck":{"enabled":true,"mode":"report","extractor":"reference"}}
58
+ CONFIG_EOF
59
+
60
+ INPUT_WITH_CWD="{\"hook_event_name\":\"PostToolUse\",\"tool_response\":\"text\",\"cwd\":\"$FAKE_REPO\"}"
61
+
62
+ if FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=false \
63
+ node "$HOOK" >"$TMPDIR_EVAL/forceoff.out" 2>"$TMPDIR_EVAL/forceoff.err" <<< "$INPUT_WITH_CWD"; then
64
+ if grep -qF '"hook_event_name"' "$TMPDIR_EVAL/forceoff.out"; then
65
+ _pass "env var FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=false forces hook off even when config has enabled:true"
66
+ else
67
+ _fail "force-off pass-through output did not match raw input"
68
+ fi
69
+ else
70
+ _fail "hook should exit 0 when force-off via env var"
71
+ fi
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Hook: config-based enable (no env var override) passes through to CLI
75
+ # ---------------------------------------------------------------------------
76
+
77
+ echo ""
78
+ echo "--- hook: config-based enable reaches CLI (fail-open on missing CLI is acceptable) ---"
79
+
80
+ if node "$HOOK" >"$TMPDIR_EVAL/config-enable.out" 2>"$TMPDIR_EVAL/config-enable.err" <<< "$INPUT_WITH_CWD"; then
81
+ _pass "hook with config enabled exits 0 (fails open when CLI or survey is unavailable)"
82
+ else
83
+ _fail "hook with config enabled should exit 0 (fail-open)"
84
+ fi
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Hook: env var force-on (legacy behavior still works)
88
+ # ---------------------------------------------------------------------------
89
+
90
+ echo ""
91
+ echo "--- hook: env var force-on still works ---"
92
+
93
+ if FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=true \
94
+ node "$HOOK" >"$TMPDIR_EVAL/forceon.out" 2>"$TMPDIR_EVAL/forceon.err" <<< "$INPUT_JSON"; then
95
+ _pass "FLOW_AGENTS_UTTERANCE_CHECK_ENABLED=true still enables the hook (legacy env var override)"
96
+ else
97
+ _fail "hook with force-on env var should exit 0"
98
+ fi
99
+
43
100
  # ---------------------------------------------------------------------------
44
101
  # Hook: pass-through with empty input
45
102
  # ---------------------------------------------------------------------------
@@ -84,8 +141,7 @@ if SA_DISABLED_HOOKS=post:utterance-check \
84
141
  node "$RUN_HOOK" post:utterance-check utterance-check.js standard,strict \
85
142
  >"$TMPDIR_EVAL/disabled-runner.out" 2>"$TMPDIR_EVAL/disabled-runner.err" <<< "$HOOK_INPUT"
86
143
  then
87
- if cmp -s "$TMPDIR_EVAL/disabled-runner.out" <(printf '%s
88
- ' "$HOOK_INPUT"); then
144
+ if cmp -s "$TMPDIR_EVAL/disabled-runner.out" <(printf '%s\n' "$HOOK_INPUT"); then
89
145
  _pass "run-hook.js passes input through when hook id is in SA_DISABLED_HOOKS"
90
146
  else
91
147
  _fail "run-hook.js disabled hook output did not match raw input"
@@ -94,6 +150,184 @@ else
94
150
  _fail "run-hook.js with disabled hook should exit 0"
95
151
  fi
96
152
 
153
+ # ---------------------------------------------------------------------------
154
+ # Hook: module.exports shape
155
+ # ---------------------------------------------------------------------------
156
+
157
+ echo ""
158
+ echo "--- hook: module.exports contract ---"
159
+
160
+ if node -e '
161
+ const h = require(process.argv[1]);
162
+ if (typeof h.run !== "function") { console.error("run missing"); process.exit(1); }
163
+ if (typeof h.extractUtteranceText !== "function") { console.error("extractUtteranceText missing"); process.exit(2); }
164
+ if (typeof h.findPackageRoot !== "function") { console.error("findPackageRoot missing"); process.exit(3); }
165
+ if (typeof h.findRepoRoot !== "function") { console.error("findRepoRoot missing"); process.exit(4); }
166
+ if (typeof h.loadRepoConfig !== "function") { console.error("loadRepoConfig missing"); process.exit(5); }
167
+ if (typeof h.resolvePolicy !== "function") { console.error("resolvePolicy missing"); process.exit(6); }
168
+ ' "$HOOK"; then
169
+ _pass "utterance-check hook exports run, extractUtteranceText, findPackageRoot, findRepoRoot, loadRepoConfig, resolvePolicy"
170
+ else
171
+ _fail "utterance-check hook module.exports is missing expected functions"
172
+ fi
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Hook: loadRepoConfig reads utteranceCheck from settings file
176
+ # ---------------------------------------------------------------------------
177
+
178
+ echo ""
179
+ echo "--- hook: loadRepoConfig reads from context/settings/flow-agents-settings.json ---"
180
+
181
+ if node -e '
182
+ const { loadRepoConfig } = require(process.argv[1]);
183
+ const fakeRepo = process.argv[2];
184
+ const cfg = loadRepoConfig(fakeRepo);
185
+ if (!cfg) { console.error("loadRepoConfig returned null for a repo with settings"); process.exit(1); }
186
+ if (cfg.enabled !== true) { console.error("expected enabled:true, got:", cfg.enabled); process.exit(2); }
187
+ if (cfg.mode !== "report") { console.error("expected mode:report, got:", cfg.mode); process.exit(3); }
188
+ if (cfg.extractor !== "reference") { console.error("expected extractor:reference, got:", cfg.extractor); process.exit(4); }
189
+ ' "$HOOK" "$FAKE_REPO"; then
190
+ _pass "loadRepoConfig correctly reads utteranceCheck fields from settings file"
191
+ else
192
+ _fail "loadRepoConfig did not return expected config from settings file"
193
+ fi
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # Hook: loadRepoConfig returns null when settings file is absent
197
+ # ---------------------------------------------------------------------------
198
+
199
+ echo ""
200
+ echo "--- hook: loadRepoConfig returns null when file is absent ---"
201
+
202
+ MISSING_REPO="$TMPDIR_EVAL/no-settings-repo"
203
+ mkdir -p "$MISSING_REPO"
204
+ touch "$MISSING_REPO/AGENTS.md"
205
+
206
+ if node -e '
207
+ const { loadRepoConfig } = require(process.argv[1]);
208
+ const cfg = loadRepoConfig(process.argv[2]);
209
+ if (cfg !== null) { console.error("expected null, got:", JSON.stringify(cfg)); process.exit(1); }
210
+ ' "$HOOK" "$MISSING_REPO"; then
211
+ _pass "loadRepoConfig returns null when context/settings/flow-agents-settings.json is absent"
212
+ else
213
+ _fail "loadRepoConfig should return null for a repo without the settings file"
214
+ fi
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # Hook: resolvePolicy respects config enabled:false as default-off
218
+ # ---------------------------------------------------------------------------
219
+
220
+ echo ""
221
+ echo "--- hook: resolvePolicy returns disabled when config has enabled:false ---"
222
+
223
+ mkdir -p "$TMPDIR_EVAL/disabled-repo/context/settings"
224
+ touch "$TMPDIR_EVAL/disabled-repo/AGENTS.md"
225
+ cat > "$TMPDIR_EVAL/disabled-repo/context/settings/flow-agents-settings.json" <<'DCFG_EOF'
226
+ {"schema_version":"1.0","utteranceCheck":{"enabled":false}}
227
+ DCFG_EOF
228
+
229
+ if node -e '
230
+ const { resolvePolicy } = require(process.argv[1]);
231
+ const policy = resolvePolicy(process.argv[2]);
232
+ if (policy.enabled !== false) { console.error("expected enabled:false, got:", policy.enabled); process.exit(1); }
233
+ ' "$HOOK" "$TMPDIR_EVAL/disabled-repo"; then
234
+ _pass "resolvePolicy returns {enabled:false} when config has enabled:false"
235
+ else
236
+ _fail "resolvePolicy should return disabled policy when config has enabled:false"
237
+ fi
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Hook: resolvePolicy applies strict mode from config
241
+ # ---------------------------------------------------------------------------
242
+
243
+ echo ""
244
+ echo "--- hook: resolvePolicy applies mode:strict from config ---"
245
+
246
+ mkdir -p "$TMPDIR_EVAL/strict-repo/context/settings"
247
+ touch "$TMPDIR_EVAL/strict-repo/AGENTS.md"
248
+ cat > "$TMPDIR_EVAL/strict-repo/context/settings/flow-agents-settings.json" <<'SCFG_EOF'
249
+ {"schema_version":"1.0","utteranceCheck":{"enabled":true,"mode":"strict","extractor":"reference"}}
250
+ SCFG_EOF
251
+
252
+ if node -e '
253
+ const { resolvePolicy } = require(process.argv[1]);
254
+ const policy = resolvePolicy(process.argv[2]);
255
+ if (policy.enabled !== true) { console.error("expected enabled:true, got:", policy.enabled); process.exit(1); }
256
+ if (policy.mode !== "strict") { console.error("expected mode:strict, got:", policy.mode); process.exit(2); }
257
+ ' "$HOOK" "$TMPDIR_EVAL/strict-repo"; then
258
+ _pass "resolvePolicy applies mode:strict from config"
259
+ else
260
+ _fail "resolvePolicy did not apply strict mode from config"
261
+ fi
262
+
263
+ # ---------------------------------------------------------------------------
264
+ # Hook: resolvePolicy applies anthropic extractor from config
265
+ # ---------------------------------------------------------------------------
266
+
267
+ echo ""
268
+ echo "--- hook: resolvePolicy applies extractor:anthropic from config ---"
269
+
270
+ mkdir -p "$TMPDIR_EVAL/anthropic-repo/context/settings"
271
+ touch "$TMPDIR_EVAL/anthropic-repo/AGENTS.md"
272
+ cat > "$TMPDIR_EVAL/anthropic-repo/context/settings/flow-agents-settings.json" <<'ACFG_EOF'
273
+ {"schema_version":"1.0","utteranceCheck":{"enabled":true,"mode":"report","extractor":"anthropic","model":"claude-haiku-4-5"}}
274
+ ACFG_EOF
275
+
276
+ if node -e '
277
+ const { resolvePolicy } = require(process.argv[1]);
278
+ const policy = resolvePolicy(process.argv[2]);
279
+ if (policy.extractor !== "anthropic") { console.error("expected extractor:anthropic, got:", policy.extractor); process.exit(1); }
280
+ if (policy.model !== "claude-haiku-4-5") { console.error("expected model:claude-haiku-4-5, got:", policy.model); process.exit(2); }
281
+ ' "$HOOK" "$TMPDIR_EVAL/anthropic-repo"; then
282
+ _pass "resolvePolicy applies extractor:anthropic and model from config"
283
+ else
284
+ _fail "resolvePolicy did not apply anthropic extractor from config"
285
+ fi
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # Hook: resolvePolicy env var STRICT overrides report mode from config
289
+ # ---------------------------------------------------------------------------
290
+
291
+ echo ""
292
+ echo "--- hook: env var STRICT overrides report mode in config ---"
293
+
294
+ if node -e '
295
+ const { resolvePolicy } = require(process.argv[1]);
296
+ // Set env var before requiring resolvePolicy
297
+ process.env.FLOW_AGENTS_UTTERANCE_CHECK_STRICT = "true";
298
+ const policy = resolvePolicy(process.argv[2]);
299
+ delete process.env.FLOW_AGENTS_UTTERANCE_CHECK_STRICT;
300
+ if (policy.mode !== "strict") { console.error("expected mode:strict from env var, got:", policy.mode); process.exit(1); }
301
+ ' "$HOOK" "$FAKE_REPO"; then
302
+ _pass "FLOW_AGENTS_UTTERANCE_CHECK_STRICT=true env var overrides report mode in config"
303
+ else
304
+ _fail "env var STRICT did not override report mode from config"
305
+ fi
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # Hook: extractUtteranceText extracts from PostToolUse and Stop events
309
+ # ---------------------------------------------------------------------------
310
+
311
+ echo ""
312
+ echo "--- hook: extractUtteranceText ---"
313
+
314
+ if node -e '
315
+ const { extractUtteranceText } = require(process.argv[1]);
316
+ const postToolUse = { hook_event_name: "PostToolUse", tool_response: "The answer is 42." };
317
+ const text = extractUtteranceText(postToolUse);
318
+ if (text !== "The answer is 42.") { console.error("PostToolUse extract failed:", text); process.exit(1); }
319
+ const stopWithContent = { hook_event_name: "Stop", content: [{ type: "text", text: "Done!" }] };
320
+ const text2 = extractUtteranceText(stopWithContent);
321
+ if (text2 !== "Done!") { console.error("Stop content extract failed:", text2); process.exit(2); }
322
+ const emptyEvent = { hook_event_name: "PostToolUse" };
323
+ const text3 = extractUtteranceText(emptyEvent);
324
+ if (text3 !== null) { console.error("Empty event should return null, got:", text3); process.exit(3); }
325
+ ' "$HOOK"; then
326
+ _pass "extractUtteranceText handles PostToolUse, Stop content, and empty events"
327
+ else
328
+ _fail "extractUtteranceText behavior was unexpected"
329
+ fi
330
+
97
331
  # ---------------------------------------------------------------------------
98
332
  # CLI: build and test --not-configured
99
333
  # ---------------------------------------------------------------------------
@@ -148,6 +382,24 @@ else
148
382
  _fail "CLI --help should exit 0"
149
383
  fi
150
384
 
385
+ # ---------------------------------------------------------------------------
386
+ # CLI: --extractor flag appears in help
387
+ # ---------------------------------------------------------------------------
388
+
389
+ echo ""
390
+ echo "--- cli: --extractor flag in help ---"
391
+
392
+ if node "$ROOT/build/src/cli.js" utterance-check --help \
393
+ >"$TMPDIR_EVAL/help2.out" 2>"$TMPDIR_EVAL/help2.err"; then
394
+ if grep -q '\-\-extractor' "$TMPDIR_EVAL/help2.err"; then
395
+ _pass "CLI --help mentions --extractor flag"
396
+ else
397
+ _fail "CLI --help does not mention --extractor flag"
398
+ fi
399
+ else
400
+ _fail "CLI --help should exit 0"
401
+ fi
402
+
151
403
  # ---------------------------------------------------------------------------
152
404
  # CLI: missing --utterance exits non-zero
153
405
  # ---------------------------------------------------------------------------
@@ -203,58 +455,53 @@ else
203
455
  fi
204
456
 
205
457
  # ---------------------------------------------------------------------------
206
- # CLI: utterance check registers as a valid flow-agents command
207
- # ---------------------------------------------------------------------------
208
-
209
- echo ""
210
- echo "--- cli: command registration ---"
211
-
212
- if node "$ROOT/build/src/cli.js" commands 2>/dev/null | grep -q 'utterance-check'; then
213
- _pass "utterance-check is registered as a flow-agents CLI command"
214
- else
215
- _fail "utterance-check is not registered in flow-agents CLI commands"
216
- fi
217
-
218
- # ---------------------------------------------------------------------------
219
- # Hook: module.exports shape
458
+ # CLI: --extractor anthropic without ANTHROPIC_API_KEY fails open (exit 0)
220
459
  # ---------------------------------------------------------------------------
221
460
 
222
461
  echo ""
223
- echo "--- hook: module.exports contract ---"
462
+ echo "--- cli: anthropic extractor without API key fails open ---"
224
463
 
225
- if node -e '
226
- const h = require(process.argv[1]);
227
- if (typeof h.run !== "function") { console.error("run missing"); process.exit(1); }
228
- if (typeof h.extractUtteranceText !== "function") { console.error("extractUtteranceText missing"); process.exit(2); }
229
- if (typeof h.findPackageRoot !== "function") { console.error("findPackageRoot missing"); process.exit(3); }
230
- ' "$HOOK"; then
231
- _pass "utterance-check hook exports run, extractUtteranceText, findPackageRoot"
464
+ # Run without ANTHROPIC_API_KEY set.
465
+ # The CLI should emit not_configured JSON and exit 0 (fail open).
466
+ if env -u ANTHROPIC_API_KEY \
467
+ node "$ROOT/build/src/cli.js" utterance-check check \
468
+ --utterance "The test coverage is 92%." \
469
+ --extractor anthropic \
470
+ >"$TMPDIR_EVAL/no-apikey.out" 2>"$TMPDIR_EVAL/no-apikey.err"
471
+ then
472
+ status_val=$(node -e '
473
+ const r = JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
474
+ console.log(r.status);
475
+ ' "$TMPDIR_EVAL/no-apikey.out" 2>/dev/null || echo "parse-error")
476
+ if [[ "$status_val" == "not_configured" ]]; then
477
+ _pass "CLI --extractor anthropic without ANTHROPIC_API_KEY emits not_configured and exits 0 (fail open)"
478
+ elif [[ "$status_val" == "ok" || "$status_val" == "error" ]]; then
479
+ # If survey is installed and somehow proceeded (shouldn't happen without key), still accept
480
+ _pass "CLI --extractor anthropic produced a valid report (status: $status_val)"
481
+ else
482
+ _fail "CLI --extractor anthropic without API key produced unexpected output (status: $status_val)"
483
+ fi
232
484
  else
233
- _fail "utterance-check hook module.exports is missing expected functions"
485
+ exit_code=$?
486
+ # Exit 1 means survey not installed — that's a different fail-open path, acceptable
487
+ if [[ "$exit_code" -eq 1 ]]; then
488
+ _pass "CLI --extractor anthropic: survey not installed, exits 1 (not_configured)"
489
+ else
490
+ _fail "CLI --extractor anthropic without API key should exit 0 or 1 (fail open), got: $exit_code"
491
+ fi
234
492
  fi
235
493
 
236
494
  # ---------------------------------------------------------------------------
237
- # Hook: extractUtteranceText extracts from PostToolUse and Stop events
495
+ # CLI: utterance check registers as a valid flow-agents command
238
496
  # ---------------------------------------------------------------------------
239
497
 
240
498
  echo ""
241
- echo "--- hook: extractUtteranceText ---"
499
+ echo "--- cli: command registration ---"
242
500
 
243
- if node -e '
244
- const { extractUtteranceText } = require(process.argv[1]);
245
- const postToolUse = { hook_event_name: "PostToolUse", tool_response: "The answer is 42." };
246
- const text = extractUtteranceText(postToolUse);
247
- if (text !== "The answer is 42.") { console.error("PostToolUse extract failed:", text); process.exit(1); }
248
- const stopWithContent = { hook_event_name: "Stop", content: [{ type: "text", text: "Done!" }] };
249
- const text2 = extractUtteranceText(stopWithContent);
250
- if (text2 !== "Done!") { console.error("Stop content extract failed:", text2); process.exit(2); }
251
- const emptyEvent = { hook_event_name: "PostToolUse" };
252
- const text3 = extractUtteranceText(emptyEvent);
253
- if (text3 !== null) { console.error("Empty event should return null, got:", text3); process.exit(3); }
254
- ' "$HOOK"; then
255
- _pass "extractUtteranceText handles PostToolUse, Stop content, and empty events"
501
+ if node "$ROOT/build/src/cli.js" commands 2>/dev/null | grep -q 'utterance-check'; then
502
+ _pass "utterance-check is registered as a flow-agents CLI command"
256
503
  else
257
- _fail "extractUtteranceText behavior was unexpected"
504
+ _fail "utterance-check is not registered in flow-agents CLI commands"
258
505
  fi
259
506
 
260
507
  # ---------------------------------------------------------------------------
package/evals/run.sh CHANGED
@@ -190,6 +190,8 @@ run_integration() {
190
190
  bash "$EVAL_DIR/integration/test_runtime_adapter_activation.sh" || result=1
191
191
  echo ""
192
192
  bash "$EVAL_DIR/integration/test_bundle_install.sh" || result=1
193
+ echo ""
194
+ bash "$EVAL_DIR/integration/test_bundle_lifecycle.sh" || result=1
193
195
  return $result
194
196
  }
195
197