@kontourai/flow-agents 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -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/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -0,0 +1,213 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { FLOW_RUN_EVIDENCE_MANIFEST_PATH, runDir } from "@kontourai/flow";
8
+ import {
9
+ startBuilderFlowSession,
10
+ syncBuilderFlowSession,
11
+ } from "../../build/src/builder-flow-runtime.js";
12
+
13
+ const SUBJECT = "local:work-item/runtime-projection";
14
+ const NOW = "2026-07-09T20:00:00.000Z";
15
+
16
+ function makeSession(slug = "runtime-projection") {
17
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-builder-runtime-"));
18
+ const artifactRoot = path.join(projectRoot, ".kontourai", "flow-agents");
19
+ const sessionDir = path.join(artifactRoot, slug);
20
+ writeJson(path.join(sessionDir, "state.json"), {
21
+ schema_version: "1.0",
22
+ task_slug: slug,
23
+ status: "planned",
24
+ phase: "planning",
25
+ updated_at: NOW,
26
+ work_item_refs: [SUBJECT],
27
+ next_action: { status: "continue", summary: "Start Builder." },
28
+ });
29
+ writeJson(path.join(artifactRoot, "current.json"), {
30
+ active_slug: slug,
31
+ artifact_dir: `.kontourai/flow-agents/${slug}`,
32
+ updated_at: NOW,
33
+ });
34
+ return { projectRoot, artifactRoot, sessionDir, slug };
35
+ }
36
+
37
+ function writeJson(file, value) {
38
+ fs.mkdirSync(path.dirname(file), { recursive: true });
39
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
40
+ }
41
+
42
+ function readJson(file) {
43
+ return JSON.parse(fs.readFileSync(file, "utf8"));
44
+ }
45
+
46
+ function bundleClaim({ expectation, claimType, subjectType, status = "pass", routeReason, subject = SUBJECT }) {
47
+ const claimId = `claim.${expectation}`;
48
+ return {
49
+ claim: {
50
+ id: claimId,
51
+ subjectType,
52
+ subjectId: `runtime-projection/gate-claim-${expectation}`,
53
+ claimType,
54
+ fieldOrBehavior: `${expectation} fixture`,
55
+ value: status,
56
+ metadata: {
57
+ workflow_subject_ref: subject,
58
+ gate_claim: {
59
+ expectation_id: expectation,
60
+ claim_type: claimType,
61
+ subject_type: subjectType,
62
+ ...(routeReason ? { route_reason: routeReason } : {}),
63
+ },
64
+ },
65
+ createdAt: NOW,
66
+ updatedAt: NOW,
67
+ },
68
+ evidence: {
69
+ id: `evidence.${expectation}`,
70
+ claimId,
71
+ evidenceType: "human_attestation",
72
+ method: "attestation",
73
+ sourceRef: "src/cli/builder-flow-runtime.test.mjs",
74
+ excerptOrSummary: `${expectation} fixture`,
75
+ observedAt: NOW,
76
+ collectedBy: "flow-agents-test",
77
+ },
78
+ event: {
79
+ id: `event.${expectation}`,
80
+ claimId,
81
+ status: status === "pass" ? "verified" : "disputed",
82
+ actor: "flow-agents-test",
83
+ method: "attestation",
84
+ evidenceIds: [`evidence.${expectation}`],
85
+ createdAt: NOW,
86
+ verifiedAt: NOW,
87
+ },
88
+ };
89
+ }
90
+
91
+ function writeBundle(sessionDir, entries) {
92
+ writeJson(path.join(sessionDir, "trust.bundle"), {
93
+ schemaVersion: 5,
94
+ source: "flow-agents-builder-runtime-test",
95
+ claims: entries.map((entry) => entry.claim),
96
+ evidence: entries.map((entry) => entry.evidence),
97
+ policies: [],
98
+ events: entries.map((entry) => entry.event),
99
+ });
100
+ }
101
+
102
+ async function writeAndSync(session, entries) {
103
+ writeBundle(session.sessionDir, entries);
104
+ return syncBuilderFlowSession({ sessionDir: session.sessionDir });
105
+ }
106
+
107
+ test("small-model client can start and advance from projected actions without choosing Flow steps", async () => {
108
+ const session = makeSession();
109
+ const started = await startBuilderFlowSession({ sessionDir: session.sessionDir });
110
+
111
+ assert.equal(started.run.state.current_step, "pull-work");
112
+ assert.deepEqual(started.projection.next_action.skills, ["pull-work"]);
113
+ assert.deepEqual(started.projection.next_action.operations, []);
114
+ assert.equal(started.projection.next_action.command, `flow-agents builder-run sync --session-dir .kontourai/flow-agents/${session.slug}`);
115
+ assert.ok(fs.existsSync(runDir(session.slug, session.projectRoot)));
116
+ assert.ok(!fs.existsSync(path.join(session.projectRoot, ".flow", "runs")), "retired runtime path must not be created");
117
+
118
+ const advanced = await writeAndSync(session, [bundleClaim({
119
+ expectation: "selected-work",
120
+ claimType: "builder.pull-work.selected",
121
+ subjectType: "work-item",
122
+ })]);
123
+
124
+ assert.equal(advanced.attached, true);
125
+ assert.equal(advanced.run.state.current_step, "design-probe");
126
+ assert.deepEqual(advanced.projection.next_action.skills, ["pickup-probe"]);
127
+ assert.equal(readJson(path.join(session.artifactRoot, "current.json")).active_step_id, "design-probe");
128
+
129
+ const duplicate = await syncBuilderFlowSession({ sessionDir: session.sessionDir });
130
+ assert.equal(duplicate.attached, false);
131
+ assert.equal(duplicate.run.manifest.evidence.length, advanced.run.manifest.evidence.length);
132
+ });
133
+
134
+ test("wrong workflow subject is rejected before canonical Flow mutation", async () => {
135
+ const session = makeSession("wrong-subject");
136
+ await startBuilderFlowSession({ sessionDir: session.sessionDir });
137
+ const beforeState = readJson(path.join(runDir(session.slug, session.projectRoot), "state.json"));
138
+ const beforeManifest = readJson(path.join(runDir(session.slug, session.projectRoot), FLOW_RUN_EVIDENCE_MANIFEST_PATH));
139
+ writeBundle(session.sessionDir, [bundleClaim({
140
+ expectation: "selected-work",
141
+ claimType: "builder.pull-work.selected",
142
+ subjectType: "work-item",
143
+ subject: "local:work-item/other",
144
+ })]);
145
+
146
+ await assert.rejects(
147
+ () => syncBuilderFlowSession({ sessionDir: session.sessionDir }),
148
+ /workflow_subject_ref.*persisted run subject/,
149
+ );
150
+ assert.deepEqual(readJson(path.join(runDir(session.slug, session.projectRoot), "state.json")), beforeState);
151
+ assert.deepEqual(readJson(path.join(runDir(session.slug, session.projectRoot), FLOW_RUN_EVIDENCE_MANIFEST_PATH)), beforeManifest);
152
+ });
153
+
154
+ test("failed verification projects Flow-owned route-back attempt and budget", async () => {
155
+ const session = makeSession("route-back-projection");
156
+ await startBuilderFlowSession({ sessionDir: session.sessionDir });
157
+ await writeAndSync(session, [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })]);
158
+ await writeAndSync(session, [
159
+ bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
160
+ bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
161
+ ]);
162
+ await writeAndSync(session, [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })]);
163
+ const verify = await writeAndSync(session, [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })]);
164
+ assert.equal(verify.run.state.current_step, "verify");
165
+
166
+ const routed = await writeAndSync(session, [bundleClaim({
167
+ expectation: "tests-evidence",
168
+ claimType: "builder.verify.tests",
169
+ subjectType: "flow-step",
170
+ status: "fail",
171
+ routeReason: "implementation_defect",
172
+ })]);
173
+
174
+ assert.equal(routed.run.state.current_step, "execute");
175
+ assert.equal(routed.projection.flow_run.route_back_attempt, 1);
176
+ assert.equal(routed.projection.flow_run.route_back_max_attempts, 3);
177
+ assert.match(routed.projection.next_action.summary, /Route-back history: attempt 1\/3 returned to `execute` for `implementation_defect`/);
178
+ assert.deepEqual(routed.projection.next_action.skills, ["execute-plan"]);
179
+ });
180
+
181
+ test("verified sidecar claims drive the composed publish and learning prefix to completion", async () => {
182
+ const session = makeSession("composed-completion");
183
+ await startBuilderFlowSession({ sessionDir: session.sessionDir });
184
+ const steps = [
185
+ [bundleClaim({ expectation: "selected-work", claimType: "builder.pull-work.selected", subjectType: "work-item" })],
186
+ [
187
+ bundleClaim({ expectation: "pickup-probe-readiness", claimType: "builder.design-probe.pickup-readiness", subjectType: "work-item" }),
188
+ bundleClaim({ expectation: "probe-decisions-or-accepted-gaps", claimType: "builder.design-probe.decisions", subjectType: "decision" }),
189
+ ],
190
+ [bundleClaim({ expectation: "implementation-plan", claimType: "builder.plan.implementation", subjectType: "artifact" })],
191
+ [bundleClaim({ expectation: "implementation-scope", claimType: "builder.execute.scope", subjectType: "change" })],
192
+ [bundleClaim({ expectation: "tests-evidence", claimType: "builder.verify.tests", subjectType: "flow-step" })],
193
+ [bundleClaim({ expectation: "merge-readiness", claimType: "builder.merge-ready.readiness", subjectType: "change" })],
194
+ ];
195
+ for (const entries of steps) await writeAndSync(session, entries);
196
+
197
+ const prOpen = readJson(path.join(session.sessionDir, "state.json"));
198
+ assert.equal(prOpen.flow_run.current_step, "pr-open");
199
+ assert.deepEqual(prOpen.next_action.skills, []);
200
+ assert.deepEqual(prOpen.next_action.operations, ["publish-change"]);
201
+
202
+ await writeAndSync(session, [bundleClaim({ expectation: "pull-request-opened", claimType: "builder.pr-open.pull-request", subjectType: "pull-request" })]);
203
+ await writeAndSync(session, [bundleClaim({ expectation: "ci-merge-readiness", claimType: "builder.merge-ready-ci.readiness", subjectType: "pull-request" })]);
204
+ const completed = await writeAndSync(session, [
205
+ bundleClaim({ expectation: "decision-evidence", claimType: "builder.learn.decisions", subjectType: "decision" }),
206
+ bundleClaim({ expectation: "learning-evidence", claimType: "builder.learn.evidence", subjectType: "release" }),
207
+ ]);
208
+
209
+ assert.equal(completed.run.state.current_step, "learn", JSON.stringify(completed.run.state.gate_outcomes, null, 2));
210
+ assert.equal(completed.run.state.status, "completed");
211
+ assert.equal(completed.projection.status, "delivered");
212
+ assert.deepEqual(completed.projection.next_action, { status: "done", summary: "Canonical Flow run is complete." });
213
+ });
@@ -0,0 +1,28 @@
1
+ import { flagString, parseArgs } from "../lib/args.js";
2
+ import { startBuilderFlowSession, syncBuilderFlowSession } from "../builder-flow-runtime.js";
3
+
4
+ export async function main(argv: string[]): Promise<number> {
5
+ const parsed = parseArgs(argv);
6
+ const action = parsed.positionals[0];
7
+ const sessionDir = flagString(parsed.flags, "session-dir");
8
+ if (!sessionDir) {
9
+ console.error("builder-run requires --session-dir .kontourai/flow-agents/<slug>");
10
+ return 64;
11
+ }
12
+ if (action !== "start" && action !== "sync") {
13
+ console.error("Usage: flow-agents builder-run <start|sync> --session-dir <path>");
14
+ return 64;
15
+ }
16
+ const result = action === "start"
17
+ ? await startBuilderFlowSession({ sessionDir })
18
+ : await syncBuilderFlowSession({ sessionDir });
19
+ console.log(JSON.stringify({
20
+ run_id: result.run.runId,
21
+ definition_id: result.run.definitionId,
22
+ current_step: result.run.state.current_step,
23
+ status: result.run.state.status,
24
+ attached: result.attached,
25
+ next_action: result.projection.next_action,
26
+ }));
27
+ return 0;
28
+ }
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { codexAgentRoutingErrors } from "../../build/src/tools/codex-agent-routing.js";
4
+
5
+ const agents = ["tool-planner", "tool-worker"];
6
+ const valid = {
7
+ codex: { allowed_agent_models: ["gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra"] },
8
+ codex_model_map: { default: "gpt-5.5" },
9
+ codex_reasoning_map: { default: "medium" },
10
+ codex_agent_map: {
11
+ "tool-planner": { model: "gpt-5.6-sol", reasoning_effort: "high" },
12
+ "tool-worker": { model: "gpt-5.6-terra", reasoning_effort: "high" },
13
+ },
14
+ };
15
+
16
+ test("accepts complete role-specific Codex routing", () => {
17
+ assert.deepEqual(codexAgentRoutingErrors(valid, agents), []);
18
+ });
19
+
20
+ test("rejects unknown agents, unsupported fields, models, and reasoning efforts", () => {
21
+ const malformed = structuredClone(valid);
22
+ malformed.codex_agent_map["tool-missing"] = {
23
+ model: "gpt-unknown",
24
+ reasoning_effort: "extreme",
25
+ extra: true,
26
+ };
27
+ const errors = codexAgentRoutingErrors(malformed, agents).join("\n");
28
+ assert.match(errors, /agent 'tool-missing' does not exist/);
29
+ assert.match(errors, /must contain exactly model and reasoning_effort/);
30
+ assert.match(errors, /uses unsupported model 'gpt-unknown'/);
31
+ assert.match(errors, /reasoning_effort is invalid: 'extreme'/);
32
+ });
33
+
34
+ test("rejects partial overrides and invalid family fallbacks", () => {
35
+ const malformed = structuredClone(valid);
36
+ malformed.codex_agent_map["tool-planner"] = { model: "gpt-5.6-sol" };
37
+ malformed.codex_model_map.default = "gpt-unknown";
38
+ malformed.codex_reasoning_map.default = "extreme";
39
+ const errors = codexAgentRoutingErrors(malformed, agents).join("\n");
40
+ assert.match(errors, /tool-planner must contain exactly model and reasoning_effort/);
41
+ assert.match(errors, /tool-planner.reasoning_effort is invalid: 'undefined'/);
42
+ assert.match(errors, /codex_model_map.default uses unsupported model 'gpt-unknown'/);
43
+ assert.match(errors, /codex_reasoning_map.default uses invalid reasoning effort 'extreme'/);
44
+ });
@@ -0,0 +1,207 @@
1
+ // Unit tests for scripts/hooks/lib/codex-exit-code.js (#470 iteration 2).
2
+ //
3
+ // Loaded via createRequire (mirrors src/cli/public-api.test.mjs:35-36) since
4
+ // the module under test is a plain CJS shared hook library, not part of the
5
+ // TS build. Fixtures live under os.tmpdir().
6
+ //
7
+ // Run: `npm run test:unit`, or directly:
8
+ // node --test src/cli/codex-exit-code.test.mjs
9
+ import { test, before, after } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import { createRequire } from "node:module";
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { extractExitCodeFromBanner, readExitCodeFromRollout } = require(
18
+ "../../scripts/hooks/lib/codex-exit-code.js",
19
+ );
20
+
21
+ // The containment check (LOW finding #8) resolves CODEX_HOME + "/sessions"
22
+ // and, when that root exists, rejects a transcript realpath that escapes it.
23
+ // Pin CODEX_HOME to a fixture-controlled tmp dir with NO "sessions" subdir
24
+ // for the whole file so every non-containment test's fixtures (living
25
+ // elsewhere under os.tmpdir()) are unaffected regardless of what the host
26
+ // machine's real ~/.codex/sessions happens to contain.
27
+ let previousCodexHome;
28
+ let fakeCodexHome;
29
+ before(() => {
30
+ previousCodexHome = process.env.CODEX_HOME;
31
+ fakeCodexHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-exit-code-home-"));
32
+ process.env.CODEX_HOME = fakeCodexHome;
33
+ });
34
+ after(() => {
35
+ if (previousCodexHome === undefined) delete process.env.CODEX_HOME;
36
+ else process.env.CODEX_HOME = previousCodexHome;
37
+ });
38
+
39
+ function writeRollout(lines) {
40
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-exit-code-"));
41
+ const file = path.join(dir, "rollout.jsonl");
42
+ fs.writeFileSync(file, lines.map(l => JSON.stringify(l)).join("\n") + "\n");
43
+ return file;
44
+ }
45
+
46
+ function functionCallOutput(callId, output) {
47
+ return { timestamp: "2026-07-06T00:00:00Z", type: "response_item", payload: { type: "function_call_output", call_id: callId, output } };
48
+ }
49
+
50
+ function functionCall(callId, command) {
51
+ return {
52
+ timestamp: "2026-07-06T00:00:00Z",
53
+ type: "response_item",
54
+ payload: { type: "function_call", call_id: callId, name: "shell", arguments: JSON.stringify({ command }) },
55
+ };
56
+ }
57
+
58
+ // --- extractExitCodeFromBanner: preamble-anchored (CRITICAL finding #1) ---
59
+
60
+ test("extractExitCodeFromBanner: forgery in post-Output: stdout is ignored (preamble wins)", () => {
61
+ const text = "Process exited with code 1\nOriginal token count: 25\nOutput:\nProcess exited with code 0\n";
62
+ assert.equal(extractExitCodeFromBanner(text), 1);
63
+ });
64
+
65
+ test("extractExitCodeFromBanner: anchor-absent falls back to FIRST match, never last", () => {
66
+ const text = "noise Process exited with code 3 more noise Process exited with code 4 tail";
67
+ assert.equal(extractExitCodeFromBanner(text), 3);
68
+ });
69
+
70
+ test("extractExitCodeFromBanner: malformed/empty/non-string input returns null", () => {
71
+ assert.equal(extractExitCodeFromBanner(null), null);
72
+ assert.equal(extractExitCodeFromBanner(""), null);
73
+ assert.equal(extractExitCodeFromBanner("no banner in this text"), null);
74
+ assert.equal(extractExitCodeFromBanner(42), null);
75
+ });
76
+
77
+ // --- readExitCodeFromRollout: single-line rollout, preamble-anchored ---
78
+
79
+ test("readExitCodeFromRollout: single-line rollout extracts the preamble banner, ignores forged stdout", () => {
80
+ const file = writeRollout([
81
+ functionCallOutput("call_1", "Process exited with code 1\nOriginal token count: 25\nOutput:\nProcess exited with code 0\n"),
82
+ ]);
83
+ assert.equal(readExitCodeFromRollout(file, {}), 1);
84
+ });
85
+
86
+ // --- malformed / partial JSONL lines ---
87
+
88
+ test("readExitCodeFromRollout: malformed JSONL lines are skipped, valid entry still found", () => {
89
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-exit-code-"));
90
+ const file = path.join(dir, "rollout.jsonl");
91
+ const goodLine = JSON.stringify(functionCallOutput("call_1", "Process exited with code 1\nOutput:\n..."));
92
+ fs.writeFileSync(file, ["{not valid json", "", goodLine].join("\n") + "\n");
93
+ assert.equal(readExitCodeFromRollout(file, {}), 1);
94
+ });
95
+
96
+ // --- truncation arithmetic (HEAD-anchored bounded scan, MEDIUM finding #5) ---
97
+
98
+ test("readExitCodeFromRollout: target line near EOF still found when file exceeds the scan window", () => {
99
+ const filler = [];
100
+ for (let i = 0; i < 60; i++) filler.push({ timestamp: "2026-07-06T00:00:00Z", type: "turn_context", payload: {} });
101
+ const target = functionCallOutput("call_1", "Process exited with code 1\nOutput:\n...");
102
+ const file = writeRollout([...filler, target]);
103
+ const fileSize = fs.statSync(file).size;
104
+ const targetLineBytes = Buffer.byteLength(JSON.stringify(target), "utf8") + 1;
105
+ assert.ok(fileSize > targetLineBytes + 100, "fixture sanity: file must exceed the target line by a margin");
106
+ // Window smaller than the whole file (forces truncation) but comfortably
107
+ // larger than the target line itself.
108
+ const maxScanBytes = targetLineBytes + 50;
109
+ assert.equal(readExitCodeFromRollout(file, { maxScanBytes }), 1);
110
+ });
111
+
112
+ test("readExitCodeFromRollout: target line start beyond maxScanBytes yields null (never mis-reads a fragment)", () => {
113
+ const filler = [];
114
+ for (let i = 0; i < 60; i++) filler.push({ timestamp: "2026-07-06T00:00:00Z", type: "turn_context", payload: {} });
115
+ const target = functionCallOutput("call_1", "Process exited with code 1\nOutput:\n...");
116
+ const file = writeRollout([...filler, target]);
117
+ // Window far smaller than the target line: only a tail fragment of it is
118
+ // ever read, which cannot JSON.parse successfully.
119
+ assert.equal(readExitCodeFromRollout(file, { maxScanBytes: 20 }), null);
120
+ });
121
+
122
+ // --- call_id correlation (Decision B #1, HIGH finding #4) ---
123
+
124
+ test("readExitCodeFromRollout: call_id match wins over the newer entry", () => {
125
+ const file = writeRollout([
126
+ functionCallOutput("call_a", "Process exited with code 2\nOutput:\n..."),
127
+ functionCallOutput("call_b", "Process exited with code 5\nOutput:\n..."), // newest, but not correlated
128
+ ]);
129
+ assert.equal(readExitCodeFromRollout(file, { callId: "call_a" }), 2);
130
+ assert.equal(readExitCodeFromRollout(file, { callId: "call_b" }), 5);
131
+ });
132
+
133
+ // --- command cross-check correlation (Decision B #2, HIGH finding #4) ---
134
+
135
+ test("readExitCodeFromRollout: command cross-check mismatch declines to null", () => {
136
+ const file = writeRollout([
137
+ functionCall("call_1", "npm run lint"),
138
+ functionCallOutput("call_1", "Process exited with code 1\nOutput:\n..."),
139
+ ]);
140
+ assert.equal(readExitCodeFromRollout(file, { command: "npm test" }), null);
141
+ });
142
+
143
+ test("readExitCodeFromRollout: command cross-check match uses the correlated entry", () => {
144
+ const file = writeRollout([
145
+ functionCall("call_1", "npm run lint"),
146
+ functionCallOutput("call_1", "Process exited with code 1\nOutput:\n..."),
147
+ ]);
148
+ assert.equal(readExitCodeFromRollout(file, { command: "npm run lint" }), 1);
149
+ });
150
+
151
+ test("readExitCodeFromRollout: no resolvable pairing falls back to the newest banner (single-call case)", () => {
152
+ const file = writeRollout([
153
+ functionCallOutput("call_1", "Process exited with code 1\nOutput:\n..."),
154
+ ]);
155
+ // No function_call entry exists at all, so no pairing can be resolved even
156
+ // though a `command` is supplied — must not spuriously decline.
157
+ assert.equal(readExitCodeFromRollout(file, { command: "npm test" }), 1);
158
+ });
159
+
160
+ // --- flooding (MEDIUM finding #5): >64KB stdout after the banner never masks it, never null ---
161
+
162
+ test("readExitCodeFromRollout: flooded stdout after the banner still extracts it, never null, never the wrong code", () => {
163
+ const flood = "x".repeat(200 * 1024); // 200KB, well beyond the default 64KB head window
164
+ const output = `Process exited with code 1\nOriginal token count: 25\nOutput:\n${flood}`;
165
+ const file = writeRollout([functionCallOutput("call_1", output)]);
166
+ const result = readExitCodeFromRollout(file, {}); // default maxLineHeadBytes (64KB)
167
+ assert.equal(result, 1);
168
+ assert.notEqual(result, 0);
169
+ assert.notEqual(result, null);
170
+ });
171
+
172
+ // --- missing / unreadable path ---
173
+
174
+ test("readExitCodeFromRollout: missing path returns null", () => {
175
+ assert.equal(readExitCodeFromRollout(path.join(os.tmpdir(), "codex-exit-code-does-not-exist", "rollout.jsonl"), {}), null);
176
+ });
177
+
178
+ test("readExitCodeFromRollout: non-string/empty path returns null", () => {
179
+ assert.equal(readExitCodeFromRollout("", {}), null);
180
+ assert.equal(readExitCodeFromRollout(null, {}), null);
181
+ assert.equal(readExitCodeFromRollout(undefined, {}), null);
182
+ });
183
+
184
+ // --- containment (LOW finding #8) ---
185
+
186
+ test("readExitCodeFromRollout: a transcript path escaping a resolvable codex sessions root is rejected", () => {
187
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "codex-exit-code-home2-"));
188
+ const sessionsRoot = path.join(home, "sessions");
189
+ fs.mkdirSync(sessionsRoot, { recursive: true });
190
+ const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-exit-code-outside-"));
191
+ const outsideFile = path.join(outside, "rollout.jsonl");
192
+ fs.writeFileSync(outsideFile, JSON.stringify(functionCallOutput("call_1", "Process exited with code 1\nOutput:\n...")) + "\n");
193
+
194
+ const previous = process.env.CODEX_HOME;
195
+ process.env.CODEX_HOME = home; // sessionsRoot now resolves, so containment is enforced
196
+ try {
197
+ assert.equal(readExitCodeFromRollout(outsideFile, {}), null);
198
+
199
+ // Control: the same file, contained under the sessions root, still extracts.
200
+ const insideFile = path.join(sessionsRoot, "rollout.jsonl");
201
+ fs.copyFileSync(outsideFile, insideFile);
202
+ assert.equal(readExitCodeFromRollout(insideFile, {}), 1);
203
+ } finally {
204
+ if (previous === undefined) delete process.env.CODEX_HOME;
205
+ else process.env.CODEX_HOME = previous;
206
+ }
207
+ });