@kontourai/flow-agents 3.3.0 → 3.4.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 (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env bash
2
+ # Proves first-step workflow entry and provider-neutral local work-item anchoring (#438).
3
+
4
+ set -uo pipefail
5
+
6
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
7
+ source "$ROOT/evals/lib/node.sh"
8
+
9
+ TMP="$(mktemp -d)"
10
+ errors=0
11
+ trap 'rm -rf "$TMP"' EXIT
12
+
13
+ pass() { printf ' PASS %s\n' "$1"; }
14
+ fail() { printf ' FAIL %s\n' "$1"; errors=$((errors + 1)); }
15
+
16
+ WRITER="workflow-sidecar"
17
+
18
+ echo "=== Builder workflow entry enforcement ==="
19
+
20
+ REFUSED_ROOT="$TMP/refused/.kontourai/flow-agents"
21
+ if flow_agents_node "$WRITER" ensure-session \
22
+ --artifact-root "$REFUSED_ROOT" \
23
+ --task-slug refused-plan \
24
+ --title "Refused plan entry" \
25
+ --summary "Must enter through the declared prefix." \
26
+ --flow-id builder.build \
27
+ --step-id plan >"$TMP/refused.out" 2>&1; then
28
+ fail "fresh later-step entry is rejected"
29
+ elif [[ ! -e "$REFUSED_ROOT" ]] && grep -q 'must start at first step "pull-work"' "$TMP/refused.out"; then
30
+ pass "fresh later-step entry is rejected before artifact-root creation"
31
+ else
32
+ fail "later-step refusal wrote files or returned the wrong diagnostic: $(cat "$TMP/refused.out")"
33
+ fi
34
+
35
+ AD_HOC_ROOT="$TMP/ad-hoc/.kontourai/flow-agents"
36
+ if flow_agents_node "$WRITER" ensure-session \
37
+ --artifact-root "$AD_HOC_ROOT" \
38
+ --task-slug refused-ad-hoc \
39
+ --title "Refused ad-hoc entry" \
40
+ --summary "A reason is not workflow authority." \
41
+ --flow-id builder.build \
42
+ --step-id plan \
43
+ --ad-hoc-reason "skip the prefix" >"$TMP/ad-hoc.out" 2>&1; then
44
+ fail "ad-hoc reason cannot authorize later-step entry"
45
+ elif [[ ! -e "$AD_HOC_ROOT" ]] && grep -q 'cannot authorize workflow entry' "$TMP/ad-hoc.out"; then
46
+ pass "ad-hoc reason cannot authorize later-step entry or write artifacts"
47
+ else
48
+ fail "ad-hoc refusal wrote files or returned the wrong diagnostic: $(cat "$TMP/ad-hoc.out")"
49
+ fi
50
+
51
+ LOCAL_ROOT="$TMP/local/.kontourai/flow-agents"
52
+ if flow_agents_node "$WRITER" ensure-session \
53
+ --artifact-root "$LOCAL_ROOT" \
54
+ --task-slug local-request \
55
+ --title "Local request" \
56
+ --summary "Providerless work still needs an anchor." \
57
+ --flow-id builder.build \
58
+ --timestamp "2026-07-10T00:00:00Z" >"$TMP/local.out" 2>&1; then
59
+ if node - "$LOCAL_ROOT" <<'NODE'
60
+ const fs = require('node:fs');
61
+ const path = require('node:path');
62
+ const root = process.argv[2];
63
+ const current = JSON.parse(fs.readFileSync(path.join(root, 'current.json'), 'utf8'));
64
+ const state = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'state.json'), 'utf8'));
65
+ const workItem = JSON.parse(fs.readFileSync(path.join(root, 'local-request', 'work-item.json'), 'utf8'));
66
+ if (current.active_flow_id !== 'builder.build' || current.active_step_id !== 'pull-work') process.exit(1);
67
+ if (state.status !== 'new' || state.phase !== 'pickup') process.exit(1);
68
+ if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['local:local-request'])) process.exit(1);
69
+ if (workItem.id !== 'local-request' || workItem.title !== 'Local request') process.exit(1);
70
+ if (workItem.source_provider?.kind !== 'local' || workItem.source_provider?.path !== 'work-item.json') process.exit(1);
71
+ if (!state.next_action?.summary?.includes('builder-run start') || !state.next_action?.summary?.includes('`pull-work`')) process.exit(1);
72
+ NODE
73
+ then
74
+ pass "providerless request creates a local Work Item and starts at pull-work"
75
+ else
76
+ fail "local Work Item or first-step state is invalid"
77
+ fi
78
+ else
79
+ fail "providerless Builder entry failed: $(cat "$TMP/local.out")"
80
+ fi
81
+
82
+ LOCAL_SESSION="$LOCAL_ROOT/local-request"
83
+ if flow_agents_node builder-run start --session-dir "$LOCAL_SESSION" >"$TMP/builder-start.out" 2>&1 \
84
+ && node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
85
+ const fs = require('node:fs');
86
+ const path = require('node:path');
87
+ const project = process.argv[2];
88
+ const session = process.argv[3];
89
+ const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
90
+ const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
91
+ if (flowState.current_step !== 'pull-work' || flowState.subject !== 'local:local-request') process.exit(1);
92
+ if (sidecar.flow_run?.current_step !== 'pull-work') process.exit(1);
93
+ if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pull-work'])) process.exit(1);
94
+ if (!sidecar.next_action?.command?.includes('builder-run sync')) process.exit(1);
95
+ NODE
96
+ then
97
+ pass "small-model entry command creates canonical Flow run and projects pull-work action"
98
+ else
99
+ fail "canonical Builder run did not start or project correctly: $(cat "$TMP/builder-start.out")"
100
+ fi
101
+
102
+ if flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
103
+ --expectation selected-work \
104
+ --status pass \
105
+ --summary "Selected local:local-request with scope and acceptance context." \
106
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Provider-neutral local Work Item."}' \
107
+ >"$TMP/selected-work.out" 2>&1 \
108
+ && node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
109
+ const fs = require('node:fs');
110
+ const path = require('node:path');
111
+ const project = process.argv[2];
112
+ const session = process.argv[3];
113
+ const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
114
+ const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
115
+ if (flowState.current_step !== 'design-probe') process.exit(1);
116
+ if (sidecar.flow_run?.current_step !== 'design-probe') process.exit(1);
117
+ if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['pickup-probe'])) process.exit(1);
118
+ NODE
119
+ then
120
+ pass "gate-claim write synchronizes Flow and projects the next skill"
121
+ else
122
+ fail "selected-work claim did not advance the canonical run: $(cat "$TMP/selected-work.out")"
123
+ fi
124
+
125
+ if FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=100000 \
126
+ node "$ROOT/scripts/hooks/stop-goal-fit.js" >"$TMP/stop.out" 2>"$TMP/stop.err" <<JSON
127
+ {"hook_event_name":"Stop","cwd":"$TMP/local"}
128
+ JSON
129
+ then
130
+ fail "active pre-execution Flow run should block Stop"
131
+ else
132
+ stop_status=$?
133
+ if [[ "$stop_status" -eq 2 ]] \
134
+ && grep -q 'required skills: pickup-probe' "$TMP/stop.err" \
135
+ && grep -q 'builder-run sync' "$TMP/stop.err" \
136
+ && grep -q 'release skipped for active Flow run' "$TMP/stop.err" \
137
+ && node - "$LOCAL_SESSION/state.json" <<'NODE'
138
+ const fs = require('node:fs');
139
+ const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
140
+ if (state.flow_run?.status !== 'active' || state.flow_run?.current_step !== 'design-probe') process.exit(1);
141
+ NODE
142
+ then
143
+ pass "active Flow run blocks Stop, preserves liveness, and exposes executable guidance"
144
+ else
145
+ fail "active Flow Stop enforcement or guidance was incomplete (exit $stop_status): $(cat "$TMP/stop.err")"
146
+ fi
147
+ fi
148
+
149
+ if flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
150
+ --expectation pickup-probe-readiness \
151
+ --status pass \
152
+ --summary "Pickup Probe confirmed scope and planning readiness." \
153
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Probe fixture evidence."}' >/dev/null 2>&1 \
154
+ && flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
155
+ --expectation probe-decisions-or-accepted-gaps \
156
+ --status pass \
157
+ --summary "Probe decisions and accepted gaps are recorded." \
158
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Probe decision fixture evidence."}' >/dev/null 2>&1 \
159
+ && flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
160
+ --expectation implementation-plan \
161
+ --status pass \
162
+ --summary "Implementation plan records files, sequence, and evidence." \
163
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Plan fixture evidence."}' >/dev/null 2>&1 \
164
+ && flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
165
+ --expectation implementation-scope \
166
+ --status pass \
167
+ --summary "Implementation scope and changed files are recorded." \
168
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Execution fixture evidence."}' >/dev/null 2>&1 \
169
+ && flow_agents_node "$WRITER" record-gate-claim "$LOCAL_SESSION" \
170
+ --expectation tests-evidence \
171
+ --status fail \
172
+ --route-reason implementation_defect \
173
+ --summary "Verification found an implementation defect." \
174
+ --evidence-ref-json '{"kind":"artifact","file":".kontourai/flow-agents/local-request/work-item.json","summary":"Failed verification fixture evidence."}' >"$TMP/route-back.out" 2>&1 \
175
+ && node - "$TMP/local" "$LOCAL_SESSION" <<'NODE'
176
+ const fs = require('node:fs');
177
+ const path = require('node:path');
178
+ const project = process.argv[2];
179
+ const session = process.argv[3];
180
+ const flowState = JSON.parse(fs.readFileSync(path.join(project, '.kontourai', 'flow', 'runs', 'local-request', 'state.json'), 'utf8'));
181
+ const sidecar = JSON.parse(fs.readFileSync(path.join(session, 'state.json'), 'utf8'));
182
+ if (flowState.current_step !== 'execute') process.exit(1);
183
+ if (sidecar.flow_run?.route_back_attempt !== 1 || sidecar.flow_run?.route_back_max_attempts !== 3) process.exit(1);
184
+ if (JSON.stringify(sidecar.next_action?.skills) !== JSON.stringify(['execute-plan'])) process.exit(1);
185
+ if (!sidecar.next_action?.summary?.includes('Route-back history: attempt 1/3')) process.exit(1);
186
+ NODE
187
+ then
188
+ pass "sidecar failure classifier drives Flow route-back and projects its attempt budget"
189
+ else
190
+ fail "route-back classifier did not produce the canonical execute retry: $(cat "$TMP/route-back.out" 2>/dev/null)"
191
+ fi
192
+
193
+ PROVIDER_ROOT="$TMP/provider/.kontourai/flow-agents"
194
+ if flow_agents_node "$WRITER" ensure-session \
195
+ --artifact-root "$PROVIDER_ROOT" \
196
+ --work-item "kontourai/flow-agents#438" \
197
+ --title "Provider work item" \
198
+ --summary "Keep the provider reference." \
199
+ --flow-id builder.build \
200
+ --timestamp "2026-07-10T00:01:00Z" >/dev/null 2>&1 \
201
+ && node - "$PROVIDER_ROOT/kontourai-flow-agents-438/state.json" <<'NODE'
202
+ const fs = require('node:fs');
203
+ const state = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
204
+ if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['kontourai/flow-agents#438'])) process.exit(1);
205
+ NODE
206
+ then
207
+ pass "provider-backed request preserves its neutral work-item ref"
208
+ else
209
+ fail "provider-backed work-item ref was not persisted"
210
+ fi
211
+
212
+ # A direct primitive remains usable without claiming Builder prefix completion.
213
+ STANDALONE_ROOT="$TMP/standalone/.kontourai/flow-agents"
214
+ if flow_agents_node "$WRITER" ensure-session \
215
+ --artifact-root "$STANDALONE_ROOT" \
216
+ --task-slug standalone-plan \
217
+ --title "Standalone plan" \
218
+ --summary "Direct primitive session." \
219
+ --timestamp "2026-07-10T00:02:00Z" >/dev/null 2>&1 \
220
+ && node - "$STANDALONE_ROOT" <<'NODE'
221
+ const fs = require('node:fs');
222
+ const path = require('node:path');
223
+ const root = process.argv[2];
224
+ const current = JSON.parse(fs.readFileSync(path.join(root, 'current.json'), 'utf8'));
225
+ const state = JSON.parse(fs.readFileSync(path.join(root, 'standalone-plan', 'state.json'), 'utf8'));
226
+ if ('active_flow_id' in current || 'active_step_id' in current) process.exit(1);
227
+ if (state.status !== 'planned' || state.phase !== 'planning') process.exit(1);
228
+ if (JSON.stringify(state.work_item_refs) !== JSON.stringify(['local:standalone-plan'])) process.exit(1);
229
+ NODE
230
+ then
231
+ pass "standalone primitive session remains available without a Builder stamp"
232
+ else
233
+ fail "standalone primitive session was incorrectly stamped as Builder"
234
+ fi
235
+
236
+ if [[ "$errors" -gt 0 ]]; then
237
+ printf 'test_builder_entry_enforcement: %d failure(s)\n' "$errors" >&2
238
+ exit 1
239
+ fi
240
+
241
+ echo "test_builder_entry_enforcement: all checks passed"
@@ -41,21 +41,29 @@ trap cleanup EXIT
41
41
 
42
42
  # ─── Helper: set active_step_id for a step.
43
43
  # For steps in the phase_map, use advance-state.
44
- # For design-probe (no phase mapping), use ensure-session --step-id.
44
+ # For design-probe (no phase mapping), mutate only the test fixture's current pointer.
45
45
  # ──────────────────────────────────────────────────────────────────
46
46
  set_active_step() {
47
47
  local aroot="$1" slug="$2" step="$3"
48
48
  case "$step" in
49
49
  design-probe)
50
- # design-probe has no lifecycle phase in the phase_map set via ensure-session --step-id
51
- flow_agents_node "workflow-sidecar" ensure-session \
52
- --artifact-root "$aroot" \
53
- --task-slug "$slug" \
54
- --title "Producer test: $step" \
55
- --summary "Test gate-claim producer at $step." \
56
- --flow-id builder.build \
57
- --step-id design-probe \
58
- --timestamp "2026-06-26T00:00:30Z" >/dev/null 2>&1
50
+ # The production entry surface cannot select a later step. This producer-focused
51
+ # fixture sets the pointer directly because design-probe has no legacy phase_map key.
52
+ node - "$aroot" <<'NODE'
53
+ const fs = require('node:fs');
54
+ const path = require('node:path');
55
+ const root = process.argv[2];
56
+ const files = [path.join(root, 'current.json')];
57
+ const actorRoot = path.join(root, 'current');
58
+ if (fs.existsSync(actorRoot)) {
59
+ files.push(...fs.readdirSync(actorRoot).filter((name) => name.endsWith('.json')).map((name) => path.join(actorRoot, name)));
60
+ }
61
+ for (const file of files) {
62
+ const current = JSON.parse(fs.readFileSync(file, 'utf8'));
63
+ current.active_step_id = 'design-probe';
64
+ fs.writeFileSync(file, `${JSON.stringify(current, null, 2)}\n`);
65
+ }
66
+ NODE
59
67
  ;;
60
68
  pull-work)
61
69
  flow_agents_node "workflow-sidecar" advance-state "$aroot/$slug" \
@@ -86,6 +86,20 @@ else
86
86
  _fail "Codex install with Kontour hosted Console telemetry config failed"
87
87
  fi
88
88
 
89
+ CODEX_HOSTED_CONSOLE_NO_TENANT_DEST="$TMPDIR_EVAL/codex-hosted-console-no-tenant-workspace"
90
+ CODEX_HOSTED_CONSOLE_NO_TENANT_STDERR="$TMPDIR_EVAL/codex-hosted-console-no-tenant-stderr.txt"
91
+ if (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_HOSTED_CONSOLE_NO_TENANT_DEST" --telemetry-sink kontour-hosted-console --console-token-file "$CONSOLE_TOKEN_FILE" >/dev/null 2>"$CODEX_HOSTED_CONSOLE_NO_TENANT_STDERR"); then
92
+ _pass "Codex install with hosted Console sink and no --console-tenant still succeeds"
93
+ else
94
+ _fail "Codex install with hosted Console sink and no --console-tenant unexpectedly failed"
95
+ fi
96
+
97
+ if rg -q 'warning: install-console-config.sh: a Console telemetry sink was selected with no --console-tenant' "$CODEX_HOSTED_CONSOLE_NO_TENANT_STDERR"; then
98
+ _pass "Untenanted hosted Console sink install prints DX warning to stderr"
99
+ else
100
+ _fail "Untenanted hosted Console sink install did not print DX warning to stderr"
101
+ fi
102
+
89
103
  if (cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_USER_HOSTED_CONSOLE_DEST" --telemetry-sink user-hosted-console --console-url https://console.example.test --console-token-file "$CONSOLE_TOKEN_FILE" >/dev/null); then
90
104
  _pass "Codex install with user-hosted Console telemetry config succeeded"
91
105
  else
@@ -116,6 +130,137 @@ else
116
130
  _fail "flow-agents init headless Codex install failed"
117
131
  fi
118
132
 
133
+ echo ""
134
+ echo "--- Guided Console-Connect Wizard (G2/G3): headless regression + summary/verify ---"
135
+
136
+ # New (G3): a plain local-files headless install still prints the structured
137
+ # post-install summary block (regardless of the guided wizard, which only
138
+ # fires for interactive installs) -- the summary/verify tail is shared by
139
+ # both install paths.
140
+ BASE_SUMMARY_LOCAL_DEST="$TMPDIR_EVAL/base-summary-local-workspace"
141
+ BASE_SUMMARY_LOCAL_STDOUT="$TMPDIR_EVAL/base-summary-local-stdout.txt"
142
+ if node "$ROOT_DIR/build/src/cli.js" init --dest "$BASE_SUMMARY_LOCAL_DEST" --telemetry-sink local-files --yes >"$BASE_SUMMARY_LOCAL_STDOUT" 2>&1; then
143
+ _pass "flow-agents init headless local-files install succeeded (G3 summary case)"
144
+ else
145
+ _fail "flow-agents init headless local-files install failed (G3 summary case)"
146
+ fi
147
+
148
+ if rg -q 'Console:' "$BASE_SUMMARY_LOCAL_STDOUT" && rg -q 'local-only' "$BASE_SUMMARY_LOCAL_STDOUT" && rg -F -q "$BASE_SUMMARY_LOCAL_DEST" "$BASE_SUMMARY_LOCAL_STDOUT"; then
149
+ _pass "flow-agents init prints G3 post-install summary block for a local-files headless install"
150
+ else
151
+ _fail "flow-agents init did not print G3 post-install summary block for a local-files headless install"
152
+ fi
153
+
154
+ # New (G2/G3): a kontour-hosted-console headless install auto-verifies via
155
+ # telemetry-doctor's buildReport in-process, stays within a bounded wall-clock
156
+ # time (never hangs), never prints the raw console token, and prints a
157
+ # console status line -- verified or unverified is acceptable in a sandboxed
158
+ # CI network, as long as the check happened honestly.
159
+ BASE_SUMMARY_HOSTED_DEST="$TMPDIR_EVAL/base-summary-hosted-workspace"
160
+ BASE_SUMMARY_HOSTED_STDOUT="$TMPDIR_EVAL/base-summary-hosted-stdout.txt"
161
+ SECONDS=0
162
+ if node "$ROOT_DIR/build/src/cli.js" init --dest "$BASE_SUMMARY_HOSTED_DEST" --telemetry-sink kontour-hosted-console --console-tenant tenant-x --console-token-file "$CONSOLE_TOKEN_FILE" --yes >"$BASE_SUMMARY_HOSTED_STDOUT" 2>&1; then
163
+ _pass "flow-agents init headless kontour-hosted-console install succeeded (G2/G3 case)"
164
+ else
165
+ _fail "flow-agents init headless kontour-hosted-console install failed (G2/G3 case)"
166
+ fi
167
+ HOSTED_SUMMARY_ELAPSED_SECONDS=$SECONDS
168
+
169
+ if [[ "$HOSTED_SUMMARY_ELAPSED_SECONDS" -le 20 ]]; then
170
+ _pass "flow-agents init auto-verify (G2) completed within a bounded wall-clock time (${HOSTED_SUMMARY_ELAPSED_SECONDS}s, never hangs)"
171
+ else
172
+ _fail "flow-agents init auto-verify (G2) took too long (${HOSTED_SUMMARY_ELAPSED_SECONDS}s) -- may be hanging"
173
+ fi
174
+
175
+ if rg -q 'Console:' "$BASE_SUMMARY_HOSTED_STDOUT" && ! rg -F -q 'test-token' "$BASE_SUMMARY_HOSTED_STDOUT"; then
176
+ _pass "flow-agents init prints a G2/G3 console status line and never prints the raw console token"
177
+ else
178
+ _fail "flow-agents init did not print a console status line, or leaked the raw console token"
179
+ fi
180
+
181
+ # New (regression, RESOLVED OWNER DECISION): the self-hosted blank-URL guard
182
+ # (fallback to local-files with a warning) applies to the INTERACTIVE wizard
183
+ # ONLY. The headless (--yes/--headless) path keeps today's unchanged
184
+ # behavior -- install-console-config.sh's existing die-on-blank for
185
+ # user-hosted-console with no --console-url/--console-endpoint. This asserts
186
+ # the exact same non-zero exit + die message current main() (pre-PR2)
187
+ # already produces, proving headless is byte-for-byte untouched. (The
188
+ # interactive fallback itself is proven by the console-connect-options.test.mjs
189
+ # unit tests on resolveSelfHostedUrlOrFallback + runConsoleConnectWizard.)
190
+ BASE_SELF_HOSTED_NO_URL_DEST="$TMPDIR_EVAL/base-self-hosted-no-url-workspace"
191
+ BASE_SELF_HOSTED_NO_URL_STDERR="$TMPDIR_EVAL/base-self-hosted-no-url-stderr.txt"
192
+ set +e
193
+ node "$ROOT_DIR/build/src/cli.js" init --dest "$BASE_SELF_HOSTED_NO_URL_DEST" --telemetry-sink user-hosted-console --yes >/dev/null 2>"$BASE_SELF_HOSTED_NO_URL_STDERR"
194
+ BASE_SELF_HOSTED_NO_URL_EXIT=$?
195
+ set -e
196
+
197
+ if [[ "$BASE_SELF_HOSTED_NO_URL_EXIT" -ne 0 ]] && rg -q 'install-console-config.sh: user-hosted-console requires --console-url or --console-endpoint' "$BASE_SELF_HOSTED_NO_URL_STDERR"; then
198
+ _pass "flow-agents init headless user-hosted-console with no --console-url still dies exactly as before (self-hosted guard is interactive-only)"
199
+ else
200
+ _fail "flow-agents init headless user-hosted-console with no --console-url behavior changed (expected unchanged die-on-blank)"
201
+ fi
202
+
203
+ # New (review fix FIX 1): a self-hosted/BYO Console (non-local https, not the
204
+ # known hosted host) headless install stays honest -- connected-unverified,
205
+ # exit 0, reachability.checked stays false because --allow-network was not
206
+ # passed -- but the summary now includes an actionable next-step hint instead
207
+ # of leaving the operator with a bare "not checked". Never for local-only.
208
+ BASE_SUMMARY_SELFHOSTED_DEST="$TMPDIR_EVAL/base-summary-selfhosted-workspace"
209
+ BASE_SUMMARY_SELFHOSTED_STDOUT="$TMPDIR_EVAL/base-summary-selfhosted-stdout.txt"
210
+ if node "$ROOT_DIR/build/src/cli.js" init --dest "$BASE_SUMMARY_SELFHOSTED_DEST" --telemetry-sink user-hosted-console --console-url https://console.example.test --console-tenant tenant-selfhosted --console-token-file "$CONSOLE_TOKEN_FILE" --yes >"$BASE_SUMMARY_SELFHOSTED_STDOUT" 2>&1; then
211
+ _pass "flow-agents init headless self-hosted/BYO Console install succeeds (unverified is honest, not a failure)"
212
+ else
213
+ _fail "flow-agents init headless self-hosted/BYO Console install unexpectedly failed"
214
+ fi
215
+
216
+ if rg -q 'connected, unverified' "$BASE_SUMMARY_SELFHOSTED_STDOUT" \
217
+ && rg -F -q 'flow-agents telemetry-doctor --allow-network' "$BASE_SUMMARY_SELFHOSTED_STDOUT" \
218
+ && ! rg -F -q 'test-token' "$BASE_SUMMARY_SELFHOSTED_STDOUT"; then
219
+ _pass "flow-agents init summary discloses WHY self-hosted reachability is unverified with an actionable --allow-network hint, and never prints the raw token"
220
+ else
221
+ _fail "flow-agents init summary did not disclose the self-hosted not-checked reason with the --allow-network hint"
222
+ fi
223
+
224
+ if rg -F -q 'flow-agents telemetry-doctor --allow-network' "$BASE_SUMMARY_LOCAL_STDOUT"; then
225
+ _fail "flow-agents init printed the self-hosted --allow-network hint for a local-files-only install (should never appear there)"
226
+ else
227
+ _pass "flow-agents init does not print the self-hosted --allow-network hint for a local-files-only install"
228
+ fi
229
+
230
+ # New (review fix FIX 4, security): pin the validate-before-persist ordering
231
+ # invariant. The TS-side wizard validators (console-telemetry-validate.ts) are
232
+ # soft (validate-and-warn, never block); the ONLY reason a malformed value can
233
+ # never land in a persisted telemetry.conf is that install-console-config.sh's
234
+ # bash validators (validate_tenant et al.) run BEFORE any config key is
235
+ # written and `die` (non-zero exit) on failure. A tenant value containing a
236
+ # semicolon fails validate_tenant's charset (^[A-Za-z0-9._:-]+$ --
237
+ # scripts/telemetry/install-console-config.sh); assert the headless install
238
+ # dies non-zero via that exact bash validator, and that the hostile value
239
+ # never appears in the persisted telemetry.conf (which already exists at this
240
+ # point from the rsync'd template, since install-console-config.sh only
241
+ # validates -- and writes nothing -- before dying).
242
+ BASE_HOSTILE_TENANT_DEST="$TMPDIR_EVAL/base-hostile-tenant-workspace"
243
+ BASE_HOSTILE_TENANT_STDERR="$TMPDIR_EVAL/base-hostile-tenant-stderr.txt"
244
+ HOSTILE_TENANT='tenant;rm-rf-evidence'
245
+ set +e
246
+ node "$ROOT_DIR/build/src/cli.js" init --dest "$BASE_HOSTILE_TENANT_DEST" --telemetry-sink user-hosted-console --console-url https://console.example.test --console-tenant "$HOSTILE_TENANT" --yes >/dev/null 2>"$BASE_HOSTILE_TENANT_STDERR"
247
+ BASE_HOSTILE_TENANT_EXIT=$?
248
+ set -e
249
+
250
+ if [[ "$BASE_HOSTILE_TENANT_EXIT" -ne 0 ]] \
251
+ && rg -q 'install-console-config.sh: --console-tenant contains unsupported characters' "$BASE_HOSTILE_TENANT_STDERR"; then
252
+ _pass "flow-agents init headless install with an illegal-character Console tenant dies via the bash validate_tenant gate (non-zero exit)"
253
+ else
254
+ _fail "flow-agents init headless install with an illegal-character Console tenant did not die via bash validate_tenant as expected"
255
+ fi
256
+
257
+ if [[ ! -f "$BASE_HOSTILE_TENANT_DEST/scripts/telemetry/telemetry.conf" ]] \
258
+ || ! rg -F -q "$HOSTILE_TENANT" "$BASE_HOSTILE_TENANT_DEST/scripts/telemetry/telemetry.conf"; then
259
+ _pass "hostile Console tenant value never lands in telemetry.conf (validate-before-persist ordering invariant holds)"
260
+ else
261
+ _fail "hostile Console tenant value was persisted into telemetry.conf despite failing bash validate_tenant"
262
+ fi
263
+
119
264
  if (cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_DEST" >/dev/null); then
120
265
  _pass "opencode install succeeded"
121
266
  else
@@ -708,6 +853,33 @@ else
708
853
  _fail "Codex full install is missing base agents"
709
854
  fi
710
855
 
856
+ if node - "$CODEX_AGENTS_DIR" <<'NODE'
857
+ const fs = require("node:fs");
858
+ const path = require("node:path");
859
+ function stringAssignment(text, key, name) {
860
+ const matches = [...text.matchAll(new RegExp(`^${key}\\s*=\\s*"([^"]*)"\\s*$`, "gm"))];
861
+ if (matches.length !== 1) throw new Error(`${name}: expected exactly one ${key} assignment, found ${matches.length}`);
862
+ return matches[0][1];
863
+ }
864
+ const expected = {
865
+ "tool-planner.toml": ["gpt-5.6-sol", "high"],
866
+ "tool-worker.toml": ["gpt-5.6-terra", "high"],
867
+ "tool-code-reviewer.toml": ["gpt-5.6-sol", "high"],
868
+ "tool-security-reviewer.toml": ["gpt-5.6-sol", "high"],
869
+ "tool-verifier.toml": ["gpt-5.6-sol", "high"],
870
+ };
871
+ for (const [name, [model, effort]] of Object.entries(expected)) {
872
+ const text = fs.readFileSync(path.join(process.argv[2], name), "utf8");
873
+ if (stringAssignment(text, "model", name) !== model) throw new Error(`${name}: expected model ${model}`);
874
+ if (stringAssignment(text, "model_reasoning_effort", name) !== effort) throw new Error(`${name}: expected reasoning ${effort}`);
875
+ }
876
+ NODE
877
+ then
878
+ _pass "Codex full install preserves Builder specialist 5.6 role routing"
879
+ else
880
+ _fail "Codex full install lost Builder specialist role routing"
881
+ fi
882
+
711
883
  if [[ -d "$CODEX_FULL_DEST/.codex/skills/plan-work" && -d "$CODEX_FULL_DEST/.codex/skills/deliver" && -d "$CODEX_FULL_DEST/.codex/skills/agentic-engineering" ]]; then
712
884
  _pass "Codex full install ships kit-skills and standalone skills together"
713
885
  else
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # test_console_tenant_isolation.sh — multi-tenant install proof (console epic fa#410 item #5).
3
+ #
4
+ # Closes the gap that `test_telemetry.sh` §7 (fake-curl capture) and
5
+ # `test_liveness_console_relay.sh` (real HTTP stub) leave open: both already prove
6
+ # single-tenant header injection, but neither proves that TWO DISTINCT tenants, each installed
7
+ # through the REAL config-writer path (`install-console-config.sh` -> `telemetry.conf` ->
8
+ # `config.sh` -> `transport.sh`), get correctly separated `x-console-tenant-id` /
9
+ # `Authorization: Bearer` headers on the wire with no cross-tenant leakage.
10
+ #
11
+ # IMPORTANT SCOPE BOUNDARY: this eval proves the CLIENT-SIDE WIRE CONTRACT only — that a
12
+ # correctly configured tenant-a install and a correctly configured tenant-b install each send
13
+ # their own (and never each other's) tenant/token headers to the network layer. It does NOT
14
+ # prove server-side Console tenant enforcement (that a token cannot be replayed against another
15
+ # tenant's data, that a misconfigured/malicious client can't spoof a tenant header, etc.) — that
16
+ # is Console-repo scope (console #98-#100 tenant hardening). Do not read a green run here as
17
+ # proof of end-to-end multi-tenant security.
18
+ #
19
+ # Deterministic (a local stub HTTP server, no network/model spend, self-cleaning). The Console
20
+ # POST fires in a detached/backgrounded subshell (transport.sh's console_post_json), so the
21
+ # stub-received assertions poll for the captured file rather than assuming synchronous delivery
22
+ # (mirrors test_liveness_console_relay.sh's wait_for_post).
23
+ # Usage: bash evals/integration/test_console_tenant_isolation.sh
24
+
25
+ set -uo pipefail
26
+
27
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
28
+ source "$ROOT/evals/lib/node.sh"
29
+
30
+ TMPDIR_EVAL="$(mktemp -d)"
31
+ trap 'rm -rf "$TMPDIR_EVAL"; [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null' EXIT
32
+ PORT=38796
33
+ RECV="$TMPDIR_EVAL/recv.jsonl"
34
+
35
+ errors=0
36
+ pass() { echo " [PASS] $1"; }
37
+ fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
38
+
39
+ # Stub Console endpoint: append each received {method, headers, body} as one JSON line, shared
40
+ # across both tenant phases so the isolation assertions can positively confirm no cross-tenant
41
+ # header leaked into the OTHER tenant's captured request (not just that each phase's own header
42
+ # is correct in isolation).
43
+ start_stub() {
44
+ node -e '
45
+ const http=require("http"),fs=require("fs");
46
+ const out=process.argv[1];
47
+ const s=http.createServer((req,res)=>{let b="";req.on("data",d=>b+=d);req.on("end",()=>{
48
+ fs.appendFileSync(out, JSON.stringify({method:req.method,headers:req.headers,body:b})+"\n");
49
+ res.writeHead(200); res.end("ok");
50
+ });});
51
+ s.listen(Number(process.argv[2]),"127.0.0.1");
52
+ setTimeout(()=>process.exit(0),30000);
53
+ ' "$RECV" "$PORT" >/dev/null 2>&1 &
54
+ STUB_PID=$!
55
+ # wait for bind
56
+ for _ in $(seq 1 20); do
57
+ if node -e 'const n=require("net");const s=n.connect(Number(process.argv[1]),"127.0.0.1");s.on("connect",()=>{s.end();process.exit(0)});s.on("error",()=>process.exit(1))' "$PORT" 2>/dev/null; then return 0; fi
58
+ sleep 0.2
59
+ done
60
+ return 1
61
+ }
62
+ stop_stub() { [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null; STUB_PID=""; }
63
+
64
+ # Poll the stub's received file for at least N lines (the POST is detached/async).
65
+ wait_for_lines() {
66
+ local n="$1" lines
67
+ for _ in $(seq 1 25); do
68
+ lines="$(wc -l <"$RECV" 2>/dev/null | tr -d ' ')"
69
+ [[ -n "$lines" && "$lines" -ge "$n" ]] && return 0
70
+ sleep 0.2
71
+ done
72
+ return 1
73
+ }
74
+
75
+ ENDPOINT="http://127.0.0.1:${PORT}/records"
76
+
77
+ # Install a tenant's real telemetry.conf via the REAL config writer, then emit one synthetic
78
+ # event through the REAL runtime path (config.sh's conf-key-to-env mapping, exercised by
79
+ # sourcing it, followed by transport.sh's console_telemetry_emit) — not a re-implementation of
80
+ # either.
81
+ install_and_emit() {
82
+ local tenant="$1" token_value="$2"
83
+ local work="$TMPDIR_EVAL/$tenant"
84
+ mkdir -p "$work"
85
+ local conf="$work/telemetry.conf"
86
+ local token_file="$work/token"
87
+ printf '%s' "$token_value" >"$token_file"
88
+
89
+ bash "$ROOT/scripts/telemetry/install-console-config.sh" "$conf" \
90
+ --console-endpoint "$ENDPOINT" \
91
+ --console-token-file "$token_file" \
92
+ --console-tenant "$tenant" >/dev/null
93
+
94
+ (
95
+ export TELEMETRY_CONFIG_FILE="$conf"
96
+ export TELEMETRY_DATA_DIR="$work/data"
97
+ export TELEMETRY_SESSION_DIR="$work/data/sessions"
98
+ unset TELEMETRY_DIR
99
+ source "$ROOT/scripts/telemetry/lib/config.sh"
100
+ source "$ROOT/scripts/telemetry/lib/transport.sh"
101
+ console_telemetry_emit '{"type":"test","note":"tenant-isolation-eval"}'
102
+ )
103
+ }
104
+
105
+ # Assert the Nth captured request (1-indexed) carries this tenant's own headers and never the
106
+ # other tenant's tenant-id/token — the actual "no cross-tenant leakage" proof.
107
+ check_capture() {
108
+ local line_no="$1" label="$2" own_tenant="$3" own_token="$4" other_tenant="$5" other_token="$6"
109
+ node -e '
110
+ const fs = require("fs");
111
+ const [recvPath, lineNoStr, ownTenant, ownToken, otherTenant, otherToken] = process.argv.slice(1);
112
+ const lineNo = Number(lineNoStr);
113
+ const lines = fs.readFileSync(recvPath, "utf8").trim().split("\n");
114
+ if (lines.length < lineNo) { console.log("BAD missing-captured-request"); process.exit(0); }
115
+ const r = JSON.parse(lines[lineNo - 1]);
116
+ const h = r.headers || {};
117
+ const raw = JSON.stringify(r);
118
+ const checks = [
119
+ ["method-post", r.method === "POST"],
120
+ ["own-tenant-header", h["x-console-tenant-id"] === ownTenant],
121
+ ["own-auth-header", h["authorization"] === ("Bearer " + ownToken)],
122
+ ["no-other-tenant-id-in-header", h["x-console-tenant-id"] !== otherTenant],
123
+ ["no-other-token-in-header", h["authorization"] !== ("Bearer " + otherToken)],
124
+ ["no-other-tenant-id-anywhere", !raw.includes(otherTenant)],
125
+ ["no-other-token-anywhere", !raw.includes(otherToken)],
126
+ ];
127
+ for (const [name, ok] of checks) console.log((ok ? "OK " : "BAD ") + name);
128
+ ' "$RECV" "$line_no" "$own_tenant" "$own_token" "$other_tenant" "$other_token" >"$TMPDIR_EVAL/checks-$line_no.txt" 2>&1
129
+ while IFS= read -r line; do
130
+ [[ "$line" == OK* ]] && pass "$label: ${line#OK }" || fail "$label: ${line#BAD } — $(cat "$TMPDIR_EVAL/checks-$line_no.txt")"
131
+ done < <(grep -E '^(OK|BAD) ' "$TMPDIR_EVAL/checks-$line_no.txt")
132
+ }
133
+
134
+ echo "=== console tenant isolation (multi-tenant install proof, fa#410 item #5) ==="
135
+
136
+ : >"$RECV"
137
+ start_stub || fail "stub server did not start"
138
+
139
+ TOKEN_A="tok-a-alnum-111"
140
+ TOKEN_B="tok-b-alnum-222"
141
+
142
+ # ─── Phase 1 (tenant-a): install + emit, assert its own headers ────────────────────────────────
143
+ echo "--- 1. tenant-a: real install-console-config.sh writes telemetry.conf; emit carries tenant-a headers ---"
144
+ install_and_emit "tenant-a" "$TOKEN_A"
145
+ if wait_for_lines 1; then pass "tenant-a event POSTed to the stub"; else fail "tenant-a event did NOT POST within timeout"; fi
146
+ check_capture 1 "tenant-a" "tenant-a" "$TOKEN_A" "tenant-b" "$TOKEN_B"
147
+
148
+ # ─── Phase 2 (tenant-b): fresh conf + distinct token, assert its own headers ───────────────────
149
+ echo "--- 2. tenant-b: fresh conf + distinct token; emit carries tenant-b headers ---"
150
+ install_and_emit "tenant-b" "$TOKEN_B"
151
+ if wait_for_lines 2; then pass "tenant-b event POSTed to the stub"; else fail "tenant-b event did NOT POST within timeout"; fi
152
+ check_capture 2 "tenant-b" "tenant-b" "$TOKEN_B" "tenant-a" "$TOKEN_A"
153
+
154
+ # ─── Isolation: re-check phase 1's record after phase 2 ran, to catch any late cross-write ─────
155
+ echo "--- 3. isolation: tenant-a's captured request still never carries tenant-b's values (and vice versa, above) ---"
156
+ check_capture 1 "tenant-a (post phase-2 recheck)" "tenant-a" "$TOKEN_A" "tenant-b" "$TOKEN_B"
157
+
158
+ stop_stub
159
+
160
+ echo ""
161
+ if [[ "$errors" -eq 0 ]]; then
162
+ echo "test_console_tenant_isolation: all checks passed."
163
+ exit 0
164
+ else
165
+ echo "test_console_tenant_isolation: $errors check(s) failed."
166
+ exit 1
167
+ fi
@@ -81,7 +81,10 @@ if [[ $? -eq 0 ]]; then _pass "(a) trust-reconcile exits 0 (resolved session con
81
81
  echo ""
82
82
  echo "=== (b) HEADLINE --flow-id builder.build/verify: mixed evidence + critique → reconcile 0 ==="
83
83
  B_AROOT="$TMP/b/aroot"; B_SLUG="flowid-converge"; B_DIR="$B_AROOT/$B_SLUG"
84
- seed "$B_AROOT" "$B_SLUG" --flow-id builder.build --step-id verify
84
+ seed "$B_AROOT" "$B_SLUG" --flow-id builder.build
85
+ flow_agents_node "$WRITER" advance-state "$B_DIR" --status in_progress --phase verification \
86
+ --summary "Testing at verify." --next-action "Record evidence." \
87
+ --flow-definition builder.build --timestamp "2026-07-01T00:00:30Z" >/dev/null 2>&1
85
88
  flow_agents_node "$WRITER" record-evidence "$B_DIR" --verdict pass \
86
89
  --check-json '{"id":"k-diff","kind":"diff","status":"pass","summary":"diff excerpt"}' \
87
90
  --check-json '{"id":"k-policy","kind":"policy","status":"pass","summary":"policy rule"}' \