@kontourai/flow-agents 1.4.0 → 2.0.1

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 (184) 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/runtime-compat.yml +1 -1
  8. package/.github/workflows/trust-reconcile.yml +113 -0
  9. package/AGENTS.md +13 -0
  10. package/CHANGELOG.md +103 -0
  11. package/CONTRIBUTING.md +4 -4
  12. package/README.md +1 -0
  13. package/agents/tool-planner.json +1 -1
  14. package/build/src/cli/init.js +242 -20
  15. package/build/src/cli/validate-workflow-artifacts.js +19 -2
  16. package/build/src/cli/verify.d.ts +1 -0
  17. package/build/src/cli/verify.js +90 -0
  18. package/build/src/cli/workflow-sidecar.d.ts +316 -8
  19. package/build/src/cli/workflow-sidecar.js +1996 -91
  20. package/build/src/cli.js +2 -3
  21. package/build/src/lib/flow-resolver.d.ts +111 -0
  22. package/build/src/lib/flow-resolver.js +308 -0
  23. package/build/src/tools/build-universal-bundles.js +34 -22
  24. package/build/src/tools/generate-context-map.js +3 -16
  25. package/build/src/tools/validate-source-tree.d.ts +1 -1
  26. package/build/src/tools/validate-source-tree.js +42 -162
  27. package/context/contracts/artifact-contract.md +10 -0
  28. package/context/contracts/delivery-contract.md +1 -0
  29. package/context/contracts/review-contract.md +1 -0
  30. package/context/contracts/verification-contract.md +2 -0
  31. package/context/gate-awareness.md +39 -0
  32. package/context/scripts/hooks/stop-goal-fit.js +632 -70
  33. package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
  34. package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
  35. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  36. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  37. package/docs/adr/0007-skill-audit.md +1 -1
  38. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
  39. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
  40. package/docs/adr/0011-mcp-posture.md +100 -0
  41. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
  42. package/docs/adr/0013-context-lifecycle.md +151 -0
  43. package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
  44. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
  45. package/docs/adr/0016-three-hard-boundary-model.md +71 -0
  46. package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
  47. package/docs/agent-system-guidebook.md +5 -12
  48. package/docs/context-map.md +4 -10
  49. package/docs/index.md +3 -2
  50. package/docs/integrations/framework-adapter.md +19 -6
  51. package/docs/integrations/index.md +2 -2
  52. package/docs/north-star.md +4 -4
  53. package/docs/operating-layers.md +3 -3
  54. package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
  55. package/docs/repository-structure.md +2 -2
  56. package/docs/skills-map.md +1 -0
  57. package/docs/spec/runtime-hook-surface.md +62 -9
  58. package/docs/standards-register.md +3 -3
  59. package/docs/survey-utterance-check.md +1 -1
  60. package/docs/trust-anchor-adoption.md +197 -0
  61. package/docs/verifiable-trust.md +95 -0
  62. package/docs/veritas-integration.md +2 -2
  63. package/docs/workflow-usage-guide.md +69 -0
  64. package/evals/acceptance/DEMO-false-completion.md +144 -0
  65. package/evals/acceptance/demo-cast.sh +92 -0
  66. package/evals/acceptance/demo-false-completion.sh +72 -0
  67. package/evals/acceptance/demo-real-evidence.sh +104 -0
  68. package/evals/acceptance/demo.tape +29 -0
  69. package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
  70. package/evals/acceptance/prove-capture-teeth.sh +114 -0
  71. package/evals/acceptance/prove-teeth.sh +105 -0
  72. package/evals/ci/antigaming-suite.sh +55 -0
  73. package/evals/ci/run-baseline.sh +2 -0
  74. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
  75. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
  76. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
  77. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
  78. package/evals/integration/test_builder_step_producers.sh +379 -0
  79. package/evals/integration/test_bundle_install.sh +35 -71
  80. package/evals/integration/test_bundle_lifecycle.sh +39 -2
  81. package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
  82. package/evals/integration/test_checkpoint_signing.sh +489 -0
  83. package/evals/integration/test_claim_lookup.sh +352 -0
  84. package/evals/integration/test_command_log_fork_classification.sh +134 -0
  85. package/evals/integration/test_command_log_integrity.sh +275 -0
  86. package/evals/integration/test_context_map.sh +0 -2
  87. package/evals/integration/test_dual_emit_flow_step.sh +278 -0
  88. package/evals/integration/test_enforcer_expects_driven.sh +281 -0
  89. package/evals/integration/test_evidence_capture_hook.sh +185 -0
  90. package/evals/integration/test_flow_kit_repository.sh +2 -0
  91. package/evals/integration/test_flowdef_session_activation.sh +273 -0
  92. package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
  93. package/evals/integration/test_gate_bypass_chain.sh +448 -0
  94. package/evals/integration/test_gate_lockdown.sh +1137 -0
  95. package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
  96. package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
  97. package/evals/integration/test_goal_fit_hook.sh +69 -4
  98. package/evals/integration/test_goal_fit_rederive.sh +263 -0
  99. package/evals/integration/test_install_merge.sh +1176 -0
  100. package/evals/integration/test_kit_identity_trust.sh +393 -0
  101. package/evals/integration/test_mint_attestation.sh +373 -0
  102. package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
  103. package/evals/integration/test_publish_delivery.sh +269 -0
  104. package/evals/integration/test_reconcile_soundness.sh +528 -0
  105. package/evals/integration/test_resolvefirststep_security.sh +208 -0
  106. package/evals/integration/test_session_resume_roundtrip.sh +286 -0
  107. package/evals/integration/test_trust_checkpoint.sh +325 -0
  108. package/evals/integration/test_trust_reconcile.sh +293 -0
  109. package/evals/integration/test_verify_cli.sh +208 -0
  110. package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
  111. package/evals/lib/node.sh +0 -6
  112. package/evals/run.sh +47 -0
  113. package/evals/static/test_workflow_skills.sh +6 -13
  114. package/install.sh +0 -7
  115. package/integrations/strands-ts/README.md +25 -15
  116. package/integrations/veritas/flow-agents.adapter.json +1 -2
  117. package/kits/builder/flows/build.flow.json +59 -12
  118. package/kits/builder/kit.json +85 -15
  119. package/kits/builder/skills/continue-work/SKILL.md +116 -0
  120. package/kits/builder/skills/deliver/SKILL.md +36 -6
  121. package/kits/builder/skills/design-probe/SKILL.md +28 -0
  122. package/kits/builder/skills/execute-plan/SKILL.md +9 -1
  123. package/kits/builder/skills/gate-review/SKILL.md +234 -0
  124. package/kits/builder/skills/learning-review/SKILL.md +30 -0
  125. package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
  126. package/kits/builder/skills/plan-work/SKILL.md +13 -1
  127. package/kits/builder/skills/pull-work/SKILL.md +19 -0
  128. package/kits/knowledge/adapters/default-store/index.js +38 -0
  129. package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
  130. package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
  131. package/kits/knowledge/docs/store-contract.md +314 -0
  132. package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
  133. package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
  134. package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
  135. package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
  136. package/kits/knowledge/evals/entities/suite.test.js +40 -0
  137. package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
  138. package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
  139. package/kits/knowledge/evals/retirement/suite.test.js +145 -0
  140. package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
  141. package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
  142. package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
  143. package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
  144. package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
  145. package/kits/knowledge/kit.json +51 -1
  146. package/package.json +6 -6
  147. package/packaging/conformance/README.md +10 -2
  148. package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
  149. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
  150. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
  151. package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
  152. package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
  153. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
  154. package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
  155. package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
  156. package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
  157. package/packaging/conformance/run-conformance.js +1 -1
  158. package/scripts/README.md +2 -1
  159. package/scripts/build-universal-bundles.js +0 -1
  160. package/scripts/ci/mint-attestation.js +221 -0
  161. package/scripts/ci/trust-reconcile.js +545 -0
  162. package/scripts/hooks/config-protection.js +423 -1
  163. package/scripts/hooks/evidence-capture.js +348 -0
  164. package/scripts/hooks/lib/liveness-read.js +113 -0
  165. package/scripts/hooks/run-hook.js +6 -1
  166. package/scripts/hooks/stop-goal-fit.js +1524 -79
  167. package/scripts/hooks/workflow-steering.js +135 -5
  168. package/scripts/install-codex-home.sh +39 -0
  169. package/scripts/install-merge.js +330 -0
  170. package/scripts/repair-command-log.js +115 -0
  171. package/src/cli/init.ts +218 -20
  172. package/src/cli/validate-workflow-artifacts.ts +18 -2
  173. package/src/cli/verify.ts +100 -0
  174. package/src/cli/workflow-sidecar.ts +2127 -84
  175. package/src/cli.ts +2 -3
  176. package/src/lib/flow-resolver.ts +369 -0
  177. package/src/tools/build-universal-bundles.ts +34 -21
  178. package/src/tools/generate-context-map.ts +3 -17
  179. package/src/tools/validate-source-tree.ts +44 -104
  180. package/build/src/tools/filter-installed-packs.d.ts +0 -2
  181. package/build/src/tools/filter-installed-packs.js +0 -135
  182. package/packaging/packs.json +0 -49
  183. package/scripts/filter-installed-packs.js +0 -2
  184. package/src/tools/filter-installed-packs.ts +0 -132
@@ -0,0 +1,1176 @@
1
+ #!/usr/bin/env bash
2
+ # test_install_merge.sh — Install merge-aware tests for claude-code + codex
3
+ #
4
+ # Covers (claude-code):
5
+ # 1. Seeded-user-config: user keys + non-FA hook survive install (AC1).
6
+ # 2. Version-stamped first install: .flow-agents/install.json written (AC2).
7
+ # 3. Idempotent re-run: two consecutive installs produce identical settings.json (AC2).
8
+ # 4. In-place upgrade: FA hook block is replaced, user keys survive (AC2).
9
+ # 5. Global target: --global flag merges into FLOW_AGENTS_USER_CLAUDE_SETTINGS path (AC3).
10
+ # 6. Fresh-install with no prior settings.json: same result as original behavior (AC4).
11
+ #
12
+ # Covers (codex):
13
+ # C1. Seeded codex hooks.json: user non-FA hook survives install.
14
+ # C2. Version-stamped first install: .flow-agents/install.json written with runtime=codex.
15
+ # C3. Idempotent re-run: two consecutive installs produce identical hooks.json.
16
+ # C4. Manual proof: user Stop hook survives + FA added + idempotent.
17
+ #
18
+ # Runtime scope: claude-code + codex. opencode/pi/kiro deferred per plan.
19
+ # Self-cleaning: all temp dirs removed on exit.
20
+ #
21
+ set -euo pipefail
22
+
23
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
24
+ TMPDIR_EVAL="$(mktemp -d /tmp/install-merge.XXXXXX)"
25
+ pass=0
26
+ fail=0
27
+
28
+ cleanup() {
29
+ rm -rf "$TMPDIR_EVAL"
30
+ }
31
+ trap cleanup EXIT
32
+
33
+ _pass() { echo " ✓ $1"; pass=$((pass + 1)); }
34
+ _fail() { echo " ✗ $1"; fail=$((fail + 1)); }
35
+
36
+ echo "=== Install Merge-Aware Tests (claude-code) ==="
37
+ echo ""
38
+
39
+ # Ensure bundles are built
40
+ echo "--- Build ---"
41
+ if (cd "$ROOT_DIR" && npm run build:bundles >/dev/null 2>&1); then
42
+ _pass "bundle build completed"
43
+ else
44
+ _fail "bundle build failed"
45
+ echo "Results: 0/$((pass + fail + 1)) passed, $((fail + 1)) failed"
46
+ exit 1
47
+ fi
48
+ echo ""
49
+
50
+ # ─── Scenario 1: Seeded user config ──────────────────────────────────────────
51
+ echo "--- Scenario 1: Seeded user config (user keys + non-FA hook survive) ---"
52
+
53
+ SEEDED_DEST="$TMPDIR_EVAL/seeded-claude"
54
+ mkdir -p "$SEEDED_DEST/.claude"
55
+
56
+ # Seed a settings.json with user keys AND a non-flow-agents hook
57
+ cat > "$SEEDED_DEST/.claude/settings.json" << 'JSON'
58
+ {
59
+ "permissions": {
60
+ "allow": ["Bash(usertool:*)"],
61
+ "customPermission": true
62
+ },
63
+ "myUserKey": "preserved-value",
64
+ "hooks": {
65
+ "Stop": [
66
+ {
67
+ "hooks": [
68
+ {
69
+ "type": "command",
70
+ "command": "echo user-stop-hook",
71
+ "timeout": 5
72
+ }
73
+ ]
74
+ }
75
+ ]
76
+ }
77
+ }
78
+ JSON
79
+
80
+ # Run install
81
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$SEEDED_DEST" >/dev/null 2>&1)
82
+
83
+ # Assert: FA hooks present
84
+ if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
85
+ const fs = require("node:fs");
86
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
87
+ const hooks = s.hooks || {};
88
+ const hasFA = Object.values(hooks).flat().some(
89
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
90
+ );
91
+ if (!hasFA) throw new Error("Flow Agents telemetry hooks not found");
92
+ console.log("ok");
93
+ NODE
94
+ then
95
+ _pass "seeded: flow-agents hooks are present after install"
96
+ else
97
+ _fail "seeded: flow-agents hooks missing after install"
98
+ fi
99
+
100
+ # Assert: user key 'myUserKey' survived
101
+ if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
102
+ const fs = require("node:fs");
103
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
104
+ if (s.myUserKey !== "preserved-value") throw new Error("myUserKey not preserved: " + JSON.stringify(s.myUserKey));
105
+ console.log("ok");
106
+ NODE
107
+ then
108
+ _pass "seeded: user key 'myUserKey' preserved"
109
+ else
110
+ _fail "seeded: user key 'myUserKey' was clobbered"
111
+ fi
112
+
113
+ # Assert: user non-FA Stop hook survived
114
+ if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
115
+ const fs = require("node:fs");
116
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
117
+ const stopGroups = (s.hooks || {}).Stop || [];
118
+ const hasUserHook = stopGroups.some(
119
+ (g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-stop-hook"))
120
+ );
121
+ if (!hasUserHook) throw new Error("User Stop hook not found in: " + JSON.stringify(stopGroups));
122
+ console.log("ok");
123
+ NODE
124
+ then
125
+ _pass "seeded: non-FA user Stop hook survived"
126
+ else
127
+ _fail "seeded: non-FA user Stop hook was removed"
128
+ fi
129
+
130
+ # Assert: user's custom permissions are PRESERVED via deep-merge (#117 core promise).
131
+ # permissions deep-merges — flow-agents UNIONs its required allow/deny/ask entries
132
+ # and preserves the user's defaultMode + custom sub-keys; it never clobbers them.
133
+ if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
134
+ const fs = require("node:fs");
135
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
136
+ const p = s.permissions || {};
137
+ if (p.customPermission !== true) throw new Error("user permissions.customPermission was clobbered: " + JSON.stringify(p));
138
+ if (!JSON.stringify(p.allow || []).includes("usertool")) throw new Error("user permissions.allow entry not preserved by union: " + JSON.stringify(p.allow));
139
+ console.log("ok");
140
+ NODE
141
+ then
142
+ _pass "seeded: user custom permissions preserved (deep-merge union, not clobbered)"
143
+ else
144
+ _fail "seeded: user custom permissions were clobbered"
145
+ fi
146
+
147
+ echo ""
148
+
149
+ # ─── Scenario 2: Version-stamped first install ───────────────────────────────
150
+ echo "--- Scenario 2: Version-stamped first install ---"
151
+
152
+ STAMP_DEST="$TMPDIR_EVAL/stamp-claude"
153
+ mkdir -p "$STAMP_DEST"
154
+
155
+ # Fresh install (no prior settings.json)
156
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$STAMP_DEST" >/dev/null 2>&1)
157
+
158
+ # Assert: .flow-agents/install.json exists with version and installedAt
159
+ if node - "$STAMP_DEST/.flow-agents/install.json" << 'NODE'
160
+ const fs = require("node:fs");
161
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
162
+ if (!record.version) throw new Error("install.json missing version");
163
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
164
+ if (record.runtime !== "claude-code") throw new Error("install.json wrong runtime: " + record.runtime);
165
+ // Validate ISO 8601 format
166
+ const d = new Date(record.installedAt);
167
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
168
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
169
+ NODE
170
+ then
171
+ _pass "first-install: .flow-agents/install.json written with version+runtime+installedAt"
172
+ else
173
+ _fail "first-install: .flow-agents/install.json missing or invalid"
174
+ fi
175
+
176
+ # Assert: fresh install produces valid settings.json with FA hooks
177
+ if node - "$STAMP_DEST/.claude/settings.json" << 'NODE'
178
+ const fs = require("node:fs");
179
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
180
+ const hooks = s.hooks || {};
181
+ const events = Object.keys(hooks);
182
+ if (events.length === 0) throw new Error("no hooks in settings.json");
183
+ const hasFA = Object.values(hooks).flat().some(
184
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
185
+ );
186
+ if (!hasFA) throw new Error("FA telemetry hooks not found");
187
+ console.log("ok: events=" + events.join(","));
188
+ NODE
189
+ then
190
+ _pass "first-install: settings.json contains FA hooks (fresh-install path unchanged)"
191
+ else
192
+ _fail "first-install: settings.json missing or FA hooks absent"
193
+ fi
194
+
195
+ echo ""
196
+
197
+ # ─── Scenario 3: Idempotent re-run ───────────────────────────────────────────
198
+ echo "--- Scenario 3: Idempotent re-run ---"
199
+
200
+ IDEM_DEST="$TMPDIR_EVAL/idem-merge-claude"
201
+ mkdir -p "$IDEM_DEST"
202
+
203
+ # First install
204
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$IDEM_DEST" >/dev/null 2>&1)
205
+
206
+ # Capture hook count after first install
207
+ HOOKS_BEFORE=$(node -e "
208
+ const s = JSON.parse(require('fs').readFileSync('$IDEM_DEST/.claude/settings.json','utf8'));
209
+ const hooks = s.hooks || {};
210
+ let count = 0;
211
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
212
+ console.log(count);
213
+ " 2>/dev/null || echo "0")
214
+
215
+ # Second install (idempotent)
216
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$IDEM_DEST" >/dev/null 2>&1)
217
+
218
+ HOOKS_AFTER=$(node -e "
219
+ const s = JSON.parse(require('fs').readFileSync('$IDEM_DEST/.claude/settings.json','utf8'));
220
+ const hooks = s.hooks || {};
221
+ let count = 0;
222
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
223
+ console.log(count);
224
+ " 2>/dev/null || echo "0")
225
+
226
+ if [[ "$HOOKS_BEFORE" == "$HOOKS_AFTER" && -n "$HOOKS_BEFORE" && "$HOOKS_BEFORE" != "0" ]]; then
227
+ _pass "idempotent: re-install did not grow hooks array (before=$HOOKS_BEFORE after=$HOOKS_AFTER)"
228
+ else
229
+ _fail "idempotent: hook count changed (before=$HOOKS_BEFORE after=$HOOKS_AFTER)"
230
+ fi
231
+
232
+ echo ""
233
+
234
+ # ─── Scenario 4: User keys survive re-install ────────────────────────────────
235
+ echo "--- Scenario 4: User keys survive re-install (upgrade semantics) ---"
236
+
237
+ UPGRADE_DEST="$TMPDIR_EVAL/upgrade-claude"
238
+ mkdir -p "$UPGRADE_DEST/.claude"
239
+
240
+ # Seed with user key + non-FA hook
241
+ cat > "$UPGRADE_DEST/.claude/settings.json" << 'JSON'
242
+ {
243
+ "permissions": {"x": 1},
244
+ "hooks": {
245
+ "Stop": [
246
+ {
247
+ "hooks": [
248
+ {
249
+ "type": "command",
250
+ "command": "echo user-hook"
251
+ }
252
+ ]
253
+ }
254
+ ]
255
+ }
256
+ }
257
+ JSON
258
+
259
+ # First install
260
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$UPGRADE_DEST" >/dev/null 2>&1)
261
+
262
+ # Second install (upgrade / re-install)
263
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$UPGRADE_DEST" >/dev/null 2>&1)
264
+
265
+ # Assert: user Stop hook survived the second install
266
+ if node - "$UPGRADE_DEST/.claude/settings.json" << 'NODE'
267
+ const fs = require("node:fs");
268
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
269
+ const stopGroups = (s.hooks || {}).Stop || [];
270
+ const hasUser = stopGroups.some(
271
+ (g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-hook"))
272
+ );
273
+ if (!hasUser) throw new Error("User hook not found after re-install: " + JSON.stringify(stopGroups));
274
+ // Also assert FA hooks present (not stripped)
275
+ const hasFA = stopGroups.some(
276
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Running Flow Agents hook policy"))
277
+ );
278
+ if (!hasFA) throw new Error("FA policy hook missing from Stop after re-install");
279
+ console.log("ok");
280
+ NODE
281
+ then
282
+ _pass "upgrade: user Stop hook and FA policy hook both present after re-install"
283
+ else
284
+ _fail "upgrade: user Stop hook or FA policy hook missing after re-install"
285
+ fi
286
+
287
+ # Assert: FA hooks not duplicated
288
+ if node - "$UPGRADE_DEST/.claude/settings.json" << 'NODE'
289
+ const fs = require("node:fs");
290
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
291
+ const hooks = s.hooks || {};
292
+ let maxFA = 0;
293
+ for (const [event, groups] of Object.entries(hooks)) {
294
+ const faCount = groups.filter(
295
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
296
+ ).length;
297
+ maxFA = Math.max(maxFA, faCount);
298
+ }
299
+ if (maxFA > 1) throw new Error("FA telemetry hooks duplicated (max " + maxFA + " per event)");
300
+ console.log("ok");
301
+ NODE
302
+ then
303
+ _pass "upgrade: FA hooks are not duplicated after re-install"
304
+ else
305
+ _fail "upgrade: FA hooks duplicated after re-install"
306
+ fi
307
+
308
+ echo ""
309
+
310
+ # ─── Scenario 5: Global target ───────────────────────────────────────────────
311
+ echo "--- Scenario 5: Global target (--global flag merges into user settings) ---"
312
+
313
+ GLOBAL_SETTINGS_DIR="$TMPDIR_EVAL/global-settings"
314
+ mkdir -p "$GLOBAL_SETTINGS_DIR"
315
+
316
+ # Seed a "user-level" settings with a user key
317
+ cat > "$GLOBAL_SETTINGS_DIR/settings.json" << 'JSON'
318
+ {
319
+ "myGlobalKey": "global-preserved",
320
+ "hooks": {
321
+ "PreToolUse": [
322
+ {
323
+ "hooks": [
324
+ {
325
+ "type": "command",
326
+ "command": "echo global-user-hook"
327
+ }
328
+ ]
329
+ }
330
+ ]
331
+ }
332
+ }
333
+ JSON
334
+
335
+ # Run init --global, overriding the target via FLOW_AGENTS_USER_CLAUDE_SETTINGS.
336
+ # --global for claude-code: dest = dirname(FLOW_AGENTS_USER_CLAUDE_SETTINGS) = GLOBAL_SETTINGS_DIR.
337
+ # The global path writes settings.json directly at dest/settings.json (dest IS ~/.claude/).
338
+ FLOW_AGENTS_USER_CLAUDE_SETTINGS="$GLOBAL_SETTINGS_DIR/settings.json" \
339
+ node "$ROOT_DIR/build/src/cli.js" init --runtime claude-code --global --yes >/dev/null 2>&1 || true
340
+
341
+ # The settings.json was merged in-place at GLOBAL_SETTINGS_DIR/settings.json.
342
+ GLOBAL_SETTINGS_JSON="$GLOBAL_SETTINGS_DIR/settings.json"
343
+
344
+ if [[ -f "$GLOBAL_SETTINGS_JSON" ]] && node - "$GLOBAL_SETTINGS_JSON" << 'NODE'
345
+ const fs = require("node:fs");
346
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
347
+ if (s.myGlobalKey !== "global-preserved") throw new Error("myGlobalKey not preserved: " + JSON.stringify(s.myGlobalKey));
348
+ const hooks = s.hooks || {};
349
+ const hasFA = Object.values(hooks).flat().some(
350
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
351
+ );
352
+ if (!hasFA) throw new Error("FA hooks not present in global settings after --global install");
353
+ const preuseGroups = (hooks.PreToolUse || []);
354
+ const hasUserHook = preuseGroups.some(
355
+ (g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo global-user-hook"))
356
+ );
357
+ if (!hasUserHook) throw new Error("global user hook not preserved in PreToolUse");
358
+ console.log("ok");
359
+ NODE
360
+ then
361
+ _pass "--global: user key + user hook survived; FA hooks present in global settings"
362
+ else
363
+ _fail "--global: merge into global settings failed or user key/hook lost"
364
+ fi
365
+
366
+ echo ""
367
+
368
+ # ─── Scenario 6: Manual proof (user-visible) ─────────────────────────────────
369
+ echo "--- Scenario 6: Manual proof — permissions + user hook survive, FA hooks added ---"
370
+
371
+ PROOF_DEST="$TMPDIR_EVAL/proof-claude"
372
+ mkdir -p "$PROOF_DEST/.claude"
373
+
374
+ cat > "$PROOF_DEST/.claude/settings.json" << 'JSON'
375
+ {
376
+ "permissions": {"x": 1},
377
+ "hooks": {
378
+ "Stop": [
379
+ {
380
+ "hooks": [
381
+ {
382
+ "type": "command",
383
+ "command": "echo user-hook"
384
+ }
385
+ ]
386
+ }
387
+ ]
388
+ }
389
+ }
390
+ JSON
391
+
392
+ echo "BEFORE install:"
393
+ node -e "
394
+ const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
395
+ console.log(' permissions:', JSON.stringify(s.permissions));
396
+ console.log(' Stop hook count:', (s.hooks?.Stop || []).length);
397
+ console.log(' User hook present:', (s.hooks?.Stop || []).some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-hook'))));
398
+ "
399
+
400
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$PROOF_DEST" >/dev/null 2>&1)
401
+
402
+ echo "AFTER first install:"
403
+ node -e "
404
+ const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
405
+ const stopGroups = s.hooks?.Stop || [];
406
+ console.log(' permissions:', JSON.stringify(s.permissions));
407
+ console.log(' Stop hook count:', stopGroups.length);
408
+ console.log(' User hook present:', stopGroups.some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-hook'))));
409
+ console.log(' FA goal-fit hook present:', stopGroups.some(g => (g.hooks||[]).some(h => String(h.statusMessage||'').includes('Running Flow Agents hook policy'))));
410
+ "
411
+
412
+ (cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$PROOF_DEST" >/dev/null 2>&1)
413
+
414
+ echo "AFTER second install (idempotence check):"
415
+ HOOKS_AFTER_SECOND=$(node -e "
416
+ const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
417
+ const hooks = s.hooks || {};
418
+ let count = 0;
419
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
420
+ console.log(count);
421
+ " 2>/dev/null || echo "err")
422
+ echo " Total hook groups: $HOOKS_AFTER_SECOND"
423
+
424
+ HOOKS_AFTER_FIRST=$(node -e "
425
+ const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
426
+ const hooks = s.hooks || {};
427
+ let count = 0;
428
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
429
+ console.log(count);
430
+ " 2>/dev/null || echo "err")
431
+
432
+ if [[ "$HOOKS_AFTER_FIRST" == "$HOOKS_AFTER_SECOND" ]]; then
433
+ _pass "manual proof: second install is idempotent (hook count stable at $HOOKS_AFTER_FIRST)"
434
+ else
435
+ _fail "manual proof: hook count changed from first to second install"
436
+ fi
437
+
438
+
439
+ # ─── Codex: Scenario C1: Seeded user hooks + non-FA hook survive ──────────────
440
+ echo "=== Install Merge-Aware Tests (codex) ==="
441
+ echo ""
442
+ echo "--- Codex Scenario C1: Seeded user hooks survive install ---"
443
+
444
+ CODEX_SEEDED="$TMPDIR_EVAL/codex-seeded"
445
+ mkdir -p "$CODEX_SEEDED/.codex"
446
+
447
+ # Seed a hooks.json with a user non-FA hook in Stop
448
+ cat > "$CODEX_SEEDED/.codex/hooks.json" << 'JSON'
449
+ {
450
+ "hooks": {
451
+ "Stop": [
452
+ {
453
+ "hooks": [
454
+ {
455
+ "type": "command",
456
+ "command": "echo user-codex-stop-hook",
457
+ "timeout": 5
458
+ }
459
+ ]
460
+ }
461
+ ]
462
+ }
463
+ }
464
+ JSON
465
+
466
+ (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_SEEDED" >/dev/null 2>&1)
467
+
468
+ # Assert: FA telemetry hooks present
469
+ if node - "$CODEX_SEEDED/.codex/hooks.json" << 'NODE'
470
+ const fs = require("node:fs");
471
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
472
+ const hooks = s.hooks || {};
473
+ const hasFA = Object.values(hooks).flat().some(
474
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
475
+ );
476
+ if (!hasFA) throw new Error("Flow Agents telemetry hooks not found in codex hooks.json");
477
+ console.log("ok");
478
+ NODE
479
+ then
480
+ _pass "codex seeded: FA telemetry hooks present after install"
481
+ else
482
+ _fail "codex seeded: FA telemetry hooks missing after install"
483
+ fi
484
+
485
+ # Assert: user non-FA Stop hook survived
486
+ if node - "$CODEX_SEEDED/.codex/hooks.json" << 'NODE'
487
+ const fs = require("node:fs");
488
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
489
+ const stopGroups = (s.hooks || {}).Stop || [];
490
+ const hasUserHook = stopGroups.some(
491
+ (g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-codex-stop-hook"))
492
+ );
493
+ if (!hasUserHook) throw new Error("User codex Stop hook not found: " + JSON.stringify(stopGroups));
494
+ console.log("ok");
495
+ NODE
496
+ then
497
+ _pass "codex seeded: non-FA user Stop hook survived install"
498
+ else
499
+ _fail "codex seeded: non-FA user Stop hook was removed"
500
+ fi
501
+
502
+ echo ""
503
+
504
+ # ─── Codex: Scenario C2: Version-stamped first install ───────────────────────
505
+ echo "--- Codex Scenario C2: Version-stamped first install ---"
506
+
507
+ CODEX_STAMP="$TMPDIR_EVAL/codex-stamp"
508
+ mkdir -p "$CODEX_STAMP"
509
+
510
+ (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_STAMP" >/dev/null 2>&1)
511
+
512
+ # Assert: .flow-agents/install.json exists with runtime=codex
513
+ if node - "$CODEX_STAMP/.flow-agents/install.json" << 'NODE'
514
+ const fs = require("node:fs");
515
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
516
+ if (!record.version) throw new Error("install.json missing version");
517
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
518
+ if (record.runtime !== "codex") throw new Error("install.json wrong runtime: " + record.runtime + " (expected codex)");
519
+ const d = new Date(record.installedAt);
520
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
521
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
522
+ NODE
523
+ then
524
+ _pass "codex first-install: .flow-agents/install.json written with version+runtime=codex+installedAt"
525
+ else
526
+ _fail "codex first-install: .flow-agents/install.json missing or invalid or wrong runtime"
527
+ fi
528
+
529
+ echo ""
530
+
531
+ # ─── Codex: Scenario C3: Idempotent re-run ───────────────────────────────────
532
+ echo "--- Codex Scenario C3: Idempotent re-run ---"
533
+
534
+ CODEX_IDEM="$TMPDIR_EVAL/codex-idem"
535
+ mkdir -p "$CODEX_IDEM"
536
+
537
+ (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_IDEM" >/dev/null 2>&1)
538
+
539
+ CODEX_HOOKS_BEFORE=$(node -e "
540
+ const s = JSON.parse(require('fs').readFileSync('$CODEX_IDEM/.codex/hooks.json','utf8'));
541
+ const hooks = s.hooks || {};
542
+ let count = 0;
543
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
544
+ console.log(count);
545
+ " 2>/dev/null || echo "0")
546
+
547
+ (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_IDEM" >/dev/null 2>&1)
548
+
549
+ CODEX_HOOKS_AFTER=$(node -e "
550
+ const s = JSON.parse(require('fs').readFileSync('$CODEX_IDEM/.codex/hooks.json','utf8'));
551
+ const hooks = s.hooks || {};
552
+ let count = 0;
553
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
554
+ console.log(count);
555
+ " 2>/dev/null || echo "0")
556
+
557
+ if [[ "$CODEX_HOOKS_BEFORE" == "$CODEX_HOOKS_AFTER" && -n "$CODEX_HOOKS_BEFORE" && "$CODEX_HOOKS_BEFORE" != "0" ]]; then
558
+ _pass "codex idempotent: re-install did not grow hooks (before=$CODEX_HOOKS_BEFORE after=$CODEX_HOOKS_AFTER)"
559
+ else
560
+ _fail "codex idempotent: hook count changed (before=$CODEX_HOOKS_BEFORE after=$CODEX_HOOKS_AFTER)"
561
+ fi
562
+
563
+ echo ""
564
+
565
+ # ─── Codex: Scenario C4: Manual proof ───────────────────────────────────────
566
+ echo "--- Codex Scenario C4: Manual proof — user Stop hook survives, FA added, idempotent ---"
567
+
568
+ CODEX_PROOF="$TMPDIR_EVAL/codex-proof"
569
+ mkdir -p "$CODEX_PROOF/.codex"
570
+
571
+ cat > "$CODEX_PROOF/.codex/hooks.json" << 'JSON'
572
+ {
573
+ "hooks": {
574
+ "Stop": [
575
+ {
576
+ "hooks": [
577
+ {
578
+ "type": "command",
579
+ "command": "echo user-codex-hook",
580
+ "timeout": 5
581
+ }
582
+ ]
583
+ }
584
+ ]
585
+ }
586
+ }
587
+ JSON
588
+
589
+ echo "BEFORE codex install:"
590
+ node -e "
591
+ const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
592
+ console.log(' Stop hook count:', (s.hooks?.Stop || []).length);
593
+ console.log(' User hook present:', (s.hooks?.Stop || []).some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-codex-hook'))));
594
+ "
595
+
596
+ (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_PROOF" >/dev/null 2>&1)
597
+
598
+ echo "AFTER first codex install:"
599
+ node -e "
600
+ const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
601
+ const stopGroups = s.hooks?.Stop || [];
602
+ console.log(' Stop hook count:', stopGroups.length);
603
+ console.log(' User hook present:', stopGroups.some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-codex-hook'))));
604
+ console.log(' FA goal-fit hook present:', stopGroups.some(g => (g.hooks||[]).some(h => String(h.statusMessage||'').includes('Running Flow Agents hook policy'))));
605
+ "
606
+
607
+ (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_PROOF" >/dev/null 2>&1)
608
+
609
+ echo "AFTER second codex install (idempotence check):"
610
+ CODEX_PROOF_HOOKS=$(node -e "
611
+ const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
612
+ const hooks = s.hooks || {};
613
+ let count = 0;
614
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
615
+ console.log(count);
616
+ " 2>/dev/null || echo "err")
617
+ echo " Total hook groups: $CODEX_PROOF_HOOKS"
618
+
619
+ CODEX_PROOF_FIRST=$(node -e "
620
+ const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
621
+ const hooks = s.hooks || {};
622
+ let count = 0;
623
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
624
+ console.log(count);
625
+ " 2>/dev/null || echo "err")
626
+
627
+ if [[ "$CODEX_PROOF_FIRST" == "$CODEX_PROOF_HOOKS" ]]; then
628
+ _pass "codex manual proof: second install is idempotent (hook count stable at $CODEX_PROOF_FIRST)"
629
+ else
630
+ _fail "codex manual proof: hook count changed from first to second install"
631
+ fi
632
+
633
+ # Assert user hook survived second install
634
+ if node - "$CODEX_PROOF/.codex/hooks.json" << 'NODE'
635
+ const fs = require("node:fs");
636
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
637
+ const stopGroups = (s.hooks || {}).Stop || [];
638
+ const hasUser = stopGroups.some(
639
+ (g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-codex-hook"))
640
+ );
641
+ if (!hasUser) throw new Error("User codex hook not found after second install");
642
+ const hasFA = stopGroups.some(
643
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Running Flow Agents hook policy"))
644
+ );
645
+ if (!hasFA) throw new Error("FA goal-fit hook missing from codex Stop after second install");
646
+ console.log("ok");
647
+ NODE
648
+ then
649
+ _pass "codex manual proof: user Stop hook + FA hooks both present after second install"
650
+ else
651
+ _fail "codex manual proof: user Stop hook or FA hooks missing after second install"
652
+ fi
653
+
654
+
655
+
656
+ # ─── opencode: Scenario OC1: User keys survive + $schema present + no empty hooks ─
657
+ echo "=== Install Merge-Aware Tests (opencode) ==="
658
+ echo ""
659
+ echo "--- opencode Scenario OC1: User keys survive + \$schema present + no spurious empty hooks ---"
660
+
661
+ OPENCODE_SEEDED="$TMPDIR_EVAL/opencode-seeded"
662
+ mkdir -p "$OPENCODE_SEEDED"
663
+
664
+ # Seed opencode.json with user keys (model + plugin)
665
+ cat > "$OPENCODE_SEEDED/opencode.json" << 'JSON'
666
+ {
667
+ "model": "x",
668
+ "plugin": ["user-thing"]
669
+ }
670
+ JSON
671
+
672
+ (cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_SEEDED" >/dev/null 2>&1)
673
+
674
+ # Assert: user 'model' key survived
675
+ if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
676
+ const fs = require("node:fs");
677
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
678
+ if (s.model !== "x") throw new Error("user key 'model' was clobbered: " + JSON.stringify(s));
679
+ console.log("ok");
680
+ NODE
681
+ then
682
+ _pass "opencode seeded: user key 'model' survived install"
683
+ else
684
+ _fail "opencode seeded: user key 'model' was clobbered"
685
+ fi
686
+
687
+ # Assert: $schema present
688
+ if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
689
+ const fs = require("node:fs");
690
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
691
+ if (s["$schema"] !== "https://opencode.ai/config.json") throw new Error("\$schema missing or wrong: " + JSON.stringify(s));
692
+ console.log("ok");
693
+ NODE
694
+ then
695
+ _pass "opencode seeded: \$schema present after install"
696
+ else
697
+ _fail "opencode seeded: \$schema missing after install"
698
+ fi
699
+
700
+ # Assert: user 'plugin' array survived
701
+ if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
702
+ const fs = require("node:fs");
703
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
704
+ if (!Array.isArray(s.plugin) || s.plugin[0] !== "user-thing") throw new Error("user key 'plugin' was clobbered: " + JSON.stringify(s));
705
+ console.log("ok");
706
+ NODE
707
+ then
708
+ _pass "opencode seeded: user key 'plugin' survived install"
709
+ else
710
+ _fail "opencode seeded: user key 'plugin' was clobbered"
711
+ fi
712
+
713
+ # Assert: no spurious empty hooks key
714
+ if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
715
+ const fs = require("node:fs");
716
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
717
+ if ("hooks" in s) throw new Error("spurious empty 'hooks' key found: " + JSON.stringify(s));
718
+ console.log("ok");
719
+ NODE
720
+ then
721
+ _pass "opencode seeded: no spurious empty 'hooks' key injected"
722
+ else
723
+ _fail "opencode seeded: spurious empty 'hooks' key was injected"
724
+ fi
725
+
726
+ # Assert: idempotent (install again, same result)
727
+ (cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_SEEDED" >/dev/null 2>&1)
728
+ if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
729
+ const fs = require("node:fs");
730
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
731
+ if (s.model !== "x") throw new Error("user key clobbered on re-install");
732
+ if (s["$schema"] !== "https://opencode.ai/config.json") throw new Error("\$schema missing after re-install");
733
+ if ("hooks" in s) throw new Error("spurious hooks key on re-install: " + JSON.stringify(s));
734
+ console.log("ok");
735
+ NODE
736
+ then
737
+ _pass "opencode seeded: second install is idempotent (user keys + \$schema stable, no hooks)"
738
+ else
739
+ _fail "opencode seeded: second install changed the result"
740
+ fi
741
+
742
+ echo ""
743
+
744
+ # ─── opencode: Scenario OC2: Manual proof ───────────────────────────────────────
745
+ echo "--- opencode Scenario OC2: Manual proof — seed opencode.json with user keys ---"
746
+
747
+ OPENCODE_PROOF="$TMPDIR_EVAL/opencode-proof"
748
+ mkdir -p "$OPENCODE_PROOF"
749
+
750
+ cat > "$OPENCODE_PROOF/opencode.json" << 'JSON'
751
+ {"model":"x","plugin":["user-thing"]}
752
+ JSON
753
+
754
+ echo "BEFORE opencode install:"
755
+ node -e "const s=JSON.parse(require('fs').readFileSync('$OPENCODE_PROOF/opencode.json','utf8')); console.log(' opencode.json:', JSON.stringify(s));"
756
+
757
+ (cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_PROOF" >/dev/null 2>&1)
758
+
759
+ echo "AFTER opencode install:"
760
+ node -e "
761
+ const s=JSON.parse(require('fs').readFileSync('$OPENCODE_PROOF/opencode.json','utf8'));
762
+ console.log(' opencode.json:', JSON.stringify(s));
763
+ console.log(' user key model:', s.model);
764
+ console.log(' \$schema:', s['\$schema']);
765
+ console.log(' has hooks key:', 'hooks' in s);
766
+ "
767
+
768
+ echo ""
769
+
770
+ # ─── Version Stamp Tests (opencode, pi, kiro) ─────────────────────────────────
771
+ echo "=== Version Stamp Tests (opencode / pi / kiro / base) ==="
772
+ echo ""
773
+
774
+ echo "--- VS1: opencode install writes .flow-agents/install.json with runtime=opencode ---"
775
+
776
+ OC_STAMP="$TMPDIR_EVAL/opencode-stamp"
777
+ mkdir -p "$OC_STAMP"
778
+
779
+ (cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OC_STAMP" >/dev/null 2>&1)
780
+
781
+ if node - "$OC_STAMP/.flow-agents/install.json" << 'NODE'
782
+ const fs = require("node:fs");
783
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
784
+ if (!record.version) throw new Error("install.json missing version");
785
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
786
+ if (record.runtime !== "opencode") throw new Error("wrong runtime: " + record.runtime + " (expected opencode)");
787
+ const d = new Date(record.installedAt);
788
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
789
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
790
+ NODE
791
+ then
792
+ _pass "opencode install: .flow-agents/install.json written with runtime=opencode"
793
+ else
794
+ _fail "opencode install: .flow-agents/install.json missing or wrong runtime"
795
+ fi
796
+
797
+ echo ""
798
+ echo "--- VS2: pi install writes .flow-agents/install.json with runtime=pi ---"
799
+
800
+ PI_STAMP="$TMPDIR_EVAL/pi-stamp"
801
+ mkdir -p "$PI_STAMP"
802
+
803
+ (cd "$ROOT_DIR/dist/pi" && bash install.sh "$PI_STAMP" >/dev/null 2>&1)
804
+
805
+ if node - "$PI_STAMP/.flow-agents/install.json" << 'NODE'
806
+ const fs = require("node:fs");
807
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
808
+ if (!record.version) throw new Error("install.json missing version");
809
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
810
+ if (record.runtime !== "pi") throw new Error("wrong runtime: " + record.runtime + " (expected pi)");
811
+ const d = new Date(record.installedAt);
812
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
813
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
814
+ NODE
815
+ then
816
+ _pass "pi install: .flow-agents/install.json written with runtime=pi"
817
+ else
818
+ _fail "pi install: .flow-agents/install.json missing or wrong runtime"
819
+ fi
820
+
821
+ echo ""
822
+ echo "--- VS3: kiro install writes .flow-agents/install.json with runtime=kiro ---"
823
+
824
+ KIRO_STAMP="$TMPDIR_EVAL/kiro-stamp"
825
+ mkdir -p "$KIRO_STAMP"
826
+
827
+ (cd "$ROOT_DIR/dist/kiro" && bash install.sh "$KIRO_STAMP" >/dev/null 2>&1)
828
+
829
+ if node - "$KIRO_STAMP/.flow-agents/install.json" << 'NODE'
830
+ const fs = require("node:fs");
831
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
832
+ if (!record.version) throw new Error("install.json missing version");
833
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
834
+ if (record.runtime !== "kiro") throw new Error("wrong runtime: " + record.runtime + " (expected kiro)");
835
+ const d = new Date(record.installedAt);
836
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
837
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
838
+ NODE
839
+ then
840
+ _pass "kiro install: .flow-agents/install.json written with runtime=kiro"
841
+ else
842
+ _fail "kiro install: .flow-agents/install.json missing or wrong runtime"
843
+ fi
844
+
845
+ echo ""
846
+ echo "--- VS4: base install writes .flow-agents/install.json with runtime=base ---"
847
+
848
+ BASE_STAMP="$TMPDIR_EVAL/base-stamp"
849
+ mkdir -p "$BASE_STAMP"
850
+
851
+ (cd "$ROOT_DIR/dist/base" && bash install.sh "$BASE_STAMP" >/dev/null 2>&1)
852
+
853
+ if node - "$BASE_STAMP/.flow-agents/install.json" << 'NODE'
854
+ const fs = require("node:fs");
855
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
856
+ if (!record.version) throw new Error("install.json missing version");
857
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
858
+ if (record.runtime !== "base") throw new Error("wrong runtime: " + record.runtime + " (expected base)");
859
+ const d = new Date(record.installedAt);
860
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
861
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
862
+ NODE
863
+ then
864
+ _pass "base install: .flow-agents/install.json written with runtime=base"
865
+ else
866
+ _fail "base install: .flow-agents/install.json missing or wrong runtime"
867
+ fi
868
+
869
+
870
+
871
+ # ─── codex-home: CH1: merge — user Stop hook survives install-codex-home ─────
872
+ echo "=== Install Merge-Aware Tests (codex-home) ==="
873
+ echo ""
874
+ echo "--- CH1: codex-home merge — seed user Stop hook → install → user hook survives + FA hooks present ---"
875
+
876
+ CH1_DEST="$TMPDIR_EVAL/codex-home-ch1"
877
+ mkdir -p "$CH1_DEST"
878
+
879
+ # Seed a user Stop hook in the codex-home hooks.json (at root, where it lives after flatten)
880
+ cat > "$CH1_DEST/hooks.json" << 'JSON'
881
+ {
882
+ "hooks": {
883
+ "Stop": [
884
+ {
885
+ "hooks": [
886
+ {
887
+ "type": "command",
888
+ "command": "echo ch1-user-stop-hook",
889
+ "timeout": 5
890
+ }
891
+ ]
892
+ }
893
+ ]
894
+ }
895
+ }
896
+ JSON
897
+
898
+ # Run install-codex-home.sh pointing to the isolated dest
899
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH1_DEST" >/dev/null 2>&1
900
+
901
+ # Assert: FA telemetry hooks present in $DEST/hooks.json
902
+ if node - "$CH1_DEST/hooks.json" << 'NODE'
903
+ const fs = require("node:fs");
904
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
905
+ const hooks = s.hooks || {};
906
+ const hasFA = Object.values(hooks).flat().some(
907
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
908
+ );
909
+ if (!hasFA) throw new Error("FA telemetry hooks not found in codex-home hooks.json");
910
+ console.log("ok");
911
+ NODE
912
+ then
913
+ _pass "CH1: FA telemetry hooks present after install-codex-home"
914
+ else
915
+ _fail "CH1: FA telemetry hooks missing after install-codex-home"
916
+ fi
917
+
918
+ # Assert: user non-FA Stop hook survived
919
+ if node - "$CH1_DEST/hooks.json" << 'NODE'
920
+ const fs = require("node:fs");
921
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
922
+ const stopGroups = (s.hooks || {}).Stop || [];
923
+ const hasUser = stopGroups.some(
924
+ (g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo ch1-user-stop-hook"))
925
+ );
926
+ if (!hasUser) throw new Error("User Stop hook not found in codex-home after install: " + JSON.stringify(stopGroups));
927
+ console.log("ok");
928
+ NODE
929
+ then
930
+ _pass "CH1: user Stop hook survived install-codex-home (merge, not overwrite)"
931
+ else
932
+ _fail "CH1: user Stop hook was overwritten by install-codex-home"
933
+ fi
934
+
935
+ echo ""
936
+
937
+ # ─── codex-home: CH2: stamp — install.json runtime=codex + version + installedAt ─
938
+ echo "--- CH2: codex-home stamp — install.json runtime=codex + version + installedAt ---"
939
+
940
+ CH2_DEST="$TMPDIR_EVAL/codex-home-ch2"
941
+ mkdir -p "$CH2_DEST"
942
+
943
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH2_DEST" >/dev/null 2>&1
944
+
945
+ if node - "$CH2_DEST/.flow-agents/install.json" << 'NODE'
946
+ const fs = require("node:fs");
947
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
948
+ if (!record.version) throw new Error("install.json missing version");
949
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
950
+ if (record.runtime !== "codex") throw new Error("wrong runtime: " + record.runtime + " (expected codex)");
951
+ const d = new Date(record.installedAt);
952
+ if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
953
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
954
+ NODE
955
+ then
956
+ _pass "CH2: codex-home install.json written with runtime=codex + version + installedAt"
957
+ else
958
+ _fail "CH2: codex-home install.json missing or wrong"
959
+ fi
960
+
961
+ echo ""
962
+
963
+ # ─── codex-home: CH3: idempotent — stable hook count on re-run ───────────────
964
+ echo "--- CH3: codex-home idempotent — stable hook count on re-run ---"
965
+
966
+ CH3_DEST="$TMPDIR_EVAL/codex-home-ch3"
967
+ mkdir -p "$CH3_DEST"
968
+
969
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH3_DEST" >/dev/null 2>&1
970
+
971
+ CH3_BEFORE=$(node -e "
972
+ const s = JSON.parse(require('fs').readFileSync('$CH3_DEST/hooks.json','utf8'));
973
+ const hooks = s.hooks || {};
974
+ let count = 0;
975
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
976
+ console.log(count);
977
+ " 2>/dev/null || echo "0")
978
+
979
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH3_DEST" >/dev/null 2>&1
980
+
981
+ CH3_AFTER=$(node -e "
982
+ const s = JSON.parse(require('fs').readFileSync('$CH3_DEST/hooks.json','utf8'));
983
+ const hooks = s.hooks || {};
984
+ let count = 0;
985
+ for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
986
+ console.log(count);
987
+ " 2>/dev/null || echo "0")
988
+
989
+ if [[ "$CH3_BEFORE" == "$CH3_AFTER" && -n "$CH3_BEFORE" && "$CH3_BEFORE" != "0" ]]; then
990
+ _pass "CH3: codex-home hook count stable on re-run (before=$CH3_BEFORE after=$CH3_AFTER)"
991
+ else
992
+ _fail "CH3: codex-home hook count changed on re-run (before=$CH3_BEFORE after=$CH3_AFTER)"
993
+ fi
994
+
995
+ echo ""
996
+
997
+ # ─── opencode --global: OG1: seed user key → install --global → key survives + $schema + stamp ─
998
+ echo "=== Install Merge-Aware Tests (--global runtimes) ==="
999
+ echo ""
1000
+ echo "--- OG1: opencode --global — seed user key → user key survives + \$schema + no spurious hooks + stamp ---"
1001
+
1002
+ OG1_CONFIG_DIR="$TMPDIR_EVAL/opencode-global-og1"
1003
+ mkdir -p "$OG1_CONFIG_DIR"
1004
+ OG1_CONFIG_FILE="$OG1_CONFIG_DIR/opencode.json"
1005
+
1006
+ # Seed the global opencode.json with a user key
1007
+ cat > "$OG1_CONFIG_FILE" << 'JSON'
1008
+ {
1009
+ "model": "og1-user-model",
1010
+ "myUserKey": "og1-preserved"
1011
+ }
1012
+ JSON
1013
+
1014
+ # Run init --global --runtime opencode, using env override for path isolation
1015
+ FLOW_AGENTS_USER_OPENCODE_CONFIG="$OG1_CONFIG_FILE" node "$ROOT_DIR/build/src/cli.js" init --runtime opencode --global --yes >/dev/null 2>&1 || true
1016
+
1017
+ # Assert: user key survived
1018
+ if node - "$OG1_CONFIG_FILE" << 'NODE'
1019
+ const fs = require("node:fs");
1020
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1021
+ if (s.model !== "og1-user-model") throw new Error("user key 'model' clobbered: " + JSON.stringify(s));
1022
+ if (s.myUserKey !== "og1-preserved") throw new Error("user key 'myUserKey' clobbered: " + JSON.stringify(s));
1023
+ console.log("ok");
1024
+ NODE
1025
+ then
1026
+ _pass "OG1: opencode --global: user keys survived merge"
1027
+ else
1028
+ _fail "OG1: opencode --global: user keys were clobbered"
1029
+ fi
1030
+
1031
+ # Assert: $schema present
1032
+ if node - "$OG1_CONFIG_FILE" << 'NODE'
1033
+ const fs = require("node:fs");
1034
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1035
+ if (s["$schema"] !== "https://opencode.ai/config.json") throw new Error("\$schema missing or wrong: " + JSON.stringify(s));
1036
+ console.log("ok");
1037
+ NODE
1038
+ then
1039
+ _pass "OG1: opencode --global: \$schema present after merge"
1040
+ else
1041
+ _fail "OG1: opencode --global: \$schema missing after merge"
1042
+ fi
1043
+
1044
+ # Assert: no spurious empty hooks key
1045
+ if node - "$OG1_CONFIG_FILE" << 'NODE'
1046
+ const fs = require("node:fs");
1047
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1048
+ if ("hooks" in s) throw new Error("spurious empty hooks key found: " + JSON.stringify(s));
1049
+ console.log("ok");
1050
+ NODE
1051
+ then
1052
+ _pass "OG1: opencode --global: no spurious empty hooks key"
1053
+ else
1054
+ _fail "OG1: opencode --global: spurious hooks key was injected"
1055
+ fi
1056
+
1057
+ # Assert: version stamp written
1058
+ if node - "$OG1_CONFIG_DIR/.flow-agents/install.json" << 'NODE'
1059
+ const fs = require("node:fs");
1060
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1061
+ if (!record.version) throw new Error("install.json missing version");
1062
+ if (!record.installedAt) throw new Error("install.json missing installedAt");
1063
+ if (record.runtime !== "opencode") throw new Error("wrong runtime: " + record.runtime);
1064
+ if (record.global !== true) throw new Error("global flag not set in stamp");
1065
+ console.log("ok: version=" + record.version);
1066
+ NODE
1067
+ then
1068
+ _pass "OG1: opencode --global: version stamp written (runtime=opencode, global=true)"
1069
+ else
1070
+ _fail "OG1: opencode --global: version stamp missing or wrong"
1071
+ fi
1072
+
1073
+ echo ""
1074
+
1075
+ # ─── codex --global: CG1: FA hooks + stamp present ───────────────────────────
1076
+ echo "--- CG1: codex --global routes to codex-home — FA hooks + stamp present ---"
1077
+
1078
+ CG1_DEST="$TMPDIR_EVAL/codex-global-cg1"
1079
+ mkdir -p "$CG1_DEST"
1080
+
1081
+ # Run init --global --runtime codex with dest override (sandbox isolation)
1082
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" node "$ROOT_DIR/build/src/cli.js" init --runtime codex --global --dest "$CG1_DEST" --yes >/dev/null 2>&1 || true
1083
+
1084
+ # Assert: FA hooks present in $DEST/hooks.json
1085
+ if node - "$CG1_DEST/hooks.json" << 'NODE'
1086
+ const fs = require("node:fs");
1087
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1088
+ const hooks = s.hooks || {};
1089
+ const hasFA = Object.values(hooks).flat().some(
1090
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
1091
+ );
1092
+ if (!hasFA) throw new Error("FA hooks not found in codex-global hooks.json");
1093
+ console.log("ok");
1094
+ NODE
1095
+ then
1096
+ _pass "CG1: codex --global: FA hooks present in codex-home hooks.json"
1097
+ else
1098
+ _fail "CG1: codex --global: FA hooks missing from codex-home hooks.json"
1099
+ fi
1100
+
1101
+ # Assert: version stamp written
1102
+ if node - "$CG1_DEST/.flow-agents/install.json" << 'NODE'
1103
+ const fs = require("node:fs");
1104
+ const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1105
+ if (!record.version) throw new Error("install.json missing version");
1106
+ if (record.runtime !== "codex") throw new Error("wrong runtime: " + record.runtime);
1107
+ console.log("ok: version=" + record.version + " runtime=" + record.runtime);
1108
+ NODE
1109
+ then
1110
+ _pass "CG1: codex --global: version stamp written (runtime=codex)"
1111
+ else
1112
+ _fail "CG1: codex --global: version stamp missing or wrong"
1113
+ fi
1114
+
1115
+ echo ""
1116
+
1117
+ # ─── codex --global: CG2: fresh install clean ────────────────────────────────
1118
+ echo "--- CG2: codex --global fresh install — clean codex-home ---"
1119
+
1120
+ CG2_DEST="$TMPDIR_EVAL/codex-global-cg2"
1121
+ mkdir -p "$CG2_DEST"
1122
+
1123
+ # Fresh install (no prior hooks.json)
1124
+ CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" node "$ROOT_DIR/build/src/cli.js" init --runtime codex --global --dest "$CG2_DEST" --yes >/dev/null 2>&1 || true
1125
+
1126
+ # Assert: hooks.json exists and has FA hooks
1127
+ if [[ -f "$CG2_DEST/hooks.json" ]] && node - "$CG2_DEST/hooks.json" << 'NODE'
1128
+ const fs = require("node:fs");
1129
+ const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
1130
+ const hooks = s.hooks || {};
1131
+ if (Object.keys(hooks).length === 0) throw new Error("No hooks in fresh codex-global install");
1132
+ const hasFA = Object.values(hooks).flat().some(
1133
+ (g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
1134
+ );
1135
+ if (!hasFA) throw new Error("FA hooks not found in fresh codex-global hooks.json");
1136
+ console.log("ok");
1137
+ NODE
1138
+ then
1139
+ _pass "CG2: codex --global fresh install: hooks.json present with FA hooks"
1140
+ else
1141
+ _fail "CG2: codex --global fresh install: hooks.json missing or FA hooks absent"
1142
+ fi
1143
+
1144
+ echo ""
1145
+
1146
+ # ─── pi --global: PG1: warns NOT_VERIFIED + falls back to workspace default ──
1147
+ echo "--- PG1: pi --global warns NOT_VERIFIED + falls back to workspace default ---"
1148
+
1149
+ PG1_DEST="$TMPDIR_EVAL/pi-global-pg1"
1150
+ mkdir -p "$PG1_DEST"
1151
+
1152
+ # Capture stderr to check for the NOT_VERIFIED warning
1153
+ PG1_STDERR=$(node "$ROOT_DIR/build/src/cli.js" init --runtime pi --global --dest "$PG1_DEST" --yes 2>&1 >/dev/null || true)
1154
+
1155
+ # Assert: stderr contains NOT_VERIFIED warn
1156
+ if echo "$PG1_STDERR" | grep -q "NOT_VERIFIED"; then
1157
+ _pass "PG1: pi --global: stderr contains NOT_VERIFIED warning"
1158
+ else
1159
+ _fail "PG1: pi --global: NOT_VERIFIED warning not found in stderr (got: $PG1_STDERR)"
1160
+ fi
1161
+
1162
+ # Assert: install still ran (bundle files present at dest)
1163
+ if [[ -d "$PG1_DEST" ]] && [[ -f "$PG1_DEST/.flow-agents/install.json" ]] || [[ -d "$PG1_DEST" ]]; then
1164
+ _pass "PG1: pi --global: dest directory exists (fell back to workspace default install)"
1165
+ else
1166
+ _fail "PG1: pi --global: dest directory missing (fallback install did not run)"
1167
+ fi
1168
+
1169
+ echo ""
1170
+
1171
+ echo ""
1172
+ echo "==========================="
1173
+ total=$((pass + fail))
1174
+ echo "Results: ${pass}/${total} passed, ${fail} failed"
1175
+ [[ "$fail" -gt 0 ]] && exit 1
1176
+ exit 0