@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
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { execFileSync } from "node:child_process";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import { parseArgs, flagList, flagString } from "../lib/args.js";
4
5
 
@@ -27,10 +28,46 @@ type FreshnessContext = {
27
28
  scopeIntersections: string[];
28
29
  };
29
30
 
31
+ type BoardIssue = {
32
+ id?: string;
33
+ node_id?: string;
34
+ owner?: string;
35
+ repo?: string;
36
+ number: number;
37
+ title: string;
38
+ body?: string;
39
+ state?: string;
40
+ labels: string[];
41
+ url?: string;
42
+ updatedAt?: string;
43
+ createdAt?: string;
44
+ };
45
+
46
+ type BoardItem = {
47
+ id?: string;
48
+ position: number;
49
+ status?: string;
50
+ priority?: string;
51
+ issue: BoardIssue | null;
52
+ };
53
+
54
+ type RepoRef = {
55
+ owner: string;
56
+ name: string;
57
+ };
58
+
30
59
  function loadJson(file: string): unknown {
31
60
  return file === "-" ? JSON.parse(fs.readFileSync(0, "utf8")) : JSON.parse(fs.readFileSync(file, "utf8"));
32
61
  }
33
62
 
63
+ function asRecord(value: unknown): Record<string, unknown> | null {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
65
+ }
66
+
67
+ function arrayValue(value: unknown): unknown[] {
68
+ return Array.isArray(value) ? value : [];
69
+ }
70
+
34
71
  function names(values: unknown): string[] {
35
72
  return Array.isArray(values) ? values.flatMap((item) => typeof item === "object" && item !== null && typeof (item as Record<string, unknown>).name === "string" ? [(item as Record<string, string>).name] : []) : [];
36
73
  }
@@ -38,7 +75,35 @@ function names(values: unknown): string[] {
38
75
  function statusKey(value: unknown): string | null {
39
76
  if (typeof value !== "string") return null;
40
77
  const key = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
41
- return ({ todo: "todo", to_do: "todo", ready: "ready", doing: "in_progress", in_progress: "in_progress", blocked: "blocked", review: "review", verification: "verification", done: "done", closed: "done" } as Record<string, string>)[key] ?? key;
78
+ return ({ open: "todo", todo: "todo", to_do: "todo", ready: "ready", doing: "in_progress", in_progress: "in_progress", blocked: "blocked", review: "review", verification: "verification", done: "done", closed: "done" } as Record<string, string>)[key] ?? key;
79
+ }
80
+
81
+ function normalizedText(value: unknown): string | null {
82
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
83
+ }
84
+
85
+ function repoKey(owner: unknown, repo: unknown, number: unknown): string | null {
86
+ const issueNumber = Number(number);
87
+ return typeof owner === "string" && typeof repo === "string" && Number.isFinite(issueNumber)
88
+ ? `${owner}/${repo}#${issueNumber}`
89
+ : null;
90
+ }
91
+
92
+ function configuredRepos(settings: Record<string, unknown>): RepoRef[] {
93
+ const workProvider = settings.work_item_provider as Record<string, unknown>;
94
+ const boardProvider = (settings.board_provider ?? {}) as Record<string, unknown>;
95
+ const workRepo = workProvider.repo as Record<string, string>;
96
+ const board = (boardProvider.board ?? {}) as Record<string, unknown>;
97
+ const defaultOwner = stringValue(board.owner) ?? stringValue((boardProvider.repo as Record<string, unknown> | undefined)?.owner) ?? workRepo.owner;
98
+ const workspace = asRecord(settings.workspace);
99
+ const repos = arrayValue(workspace?.repos).flatMap((repo) => {
100
+ if (typeof repo === "string" && repo.trim()) return [{ owner: defaultOwner, name: repo.trim() }];
101
+ const record = asRecord(repo);
102
+ const owner = stringValue(record?.owner) ?? defaultOwner;
103
+ const name = stringValue(record?.name);
104
+ return owner && name ? [{ owner, name }] : [];
105
+ });
106
+ return repos.length ? repos : [{ owner: workRepo.owner, name: workRepo.name }];
42
107
  }
43
108
 
44
109
  function projectStatus(issue: Record<string, unknown>): string | null {
@@ -412,6 +477,305 @@ function normalize(issue: Record<string, unknown>, settings: Record<string, unkn
412
477
  };
413
478
  }
414
479
 
480
+ function fieldValueByName(item: Record<string, unknown>, fieldName: string): string | undefined {
481
+ const wanted = fieldName.toLowerCase();
482
+ const direct = item[fieldName] ?? item[fieldName.toLowerCase()];
483
+ if (typeof direct === "string") return direct;
484
+ const fieldValues = Array.isArray(item.fieldValues) ? item.fieldValues : arrayValue(asRecord(item.fieldValues)?.nodes);
485
+ for (const value of fieldValues) {
486
+ const record = asRecord(value);
487
+ if (!record) continue;
488
+ const field = asRecord(record.field);
489
+ const name = normalizedText(field?.name);
490
+ if (name !== wanted) continue;
491
+ return stringValue(record.name) ?? stringValue(record.text) ?? stringValue(record.value) ?? stringValue(record.optionName);
492
+ }
493
+ return undefined;
494
+ }
495
+
496
+ function normalizeBoardIssue(rawIssue: unknown, fallbackOwner?: string, fallbackRepo?: string): BoardIssue | null {
497
+ const issue = asRecord(rawIssue);
498
+ if (!issue) return null;
499
+ const number = Number(issue.number);
500
+ if (!Number.isFinite(number)) return null;
501
+ const repository = asRecord(issue.repository);
502
+ const owner = stringValue(issue.owner) ?? stringValue(asRecord(repository?.owner)?.login) ?? fallbackOwner;
503
+ const repo = stringValue(issue.repo) ?? stringValue(issue.repositoryName) ?? stringValue(repository?.name) ?? fallbackRepo;
504
+ return {
505
+ id: stringValue(issue.id),
506
+ node_id: stringValue(issue.node_id) ?? stringValue(issue.id),
507
+ owner,
508
+ repo,
509
+ number,
510
+ title: stringValue(issue.title) ?? `#${number}`,
511
+ body: stringValue(issue.body) ?? "",
512
+ state: stringValue(issue.state) ?? "OPEN",
513
+ labels: names(issue.labels).concat(arrayValue(asRecord(issue.labels)?.nodes).flatMap((label) => stringValue(asRecord(label)?.name) ?? [])),
514
+ url: stringValue(issue.url),
515
+ updatedAt: stringValue(issue.updatedAt),
516
+ createdAt: stringValue(issue.createdAt),
517
+ };
518
+ }
519
+
520
+ function normalizeBoardItem(rawItem: unknown, index: number, fallbackOwner?: string, fallbackRepo?: string): BoardItem | null {
521
+ const item = asRecord(rawItem);
522
+ if (!item) return null;
523
+ const content = item.content ?? item.issue;
524
+ const issue = normalizeBoardIssue(content, fallbackOwner, fallbackRepo);
525
+ if (!issue) return null;
526
+ const rawPosition = item.position ?? item.databaseId ?? item.index;
527
+ const position = Number.isFinite(Number(rawPosition)) ? Number(rawPosition) : index;
528
+ return {
529
+ id: stringValue(item.id),
530
+ position,
531
+ status: fieldValueByName(item, "Status") ?? stringValue(item.status),
532
+ priority: fieldValueByName(item, "Priority") ?? stringValue(item.priority),
533
+ issue,
534
+ };
535
+ }
536
+
537
+ function boardItemsFromDoc(doc: unknown, settings: Record<string, unknown>): BoardItem[] {
538
+ const workProvider = settings.work_item_provider as Record<string, unknown>;
539
+ const repo = workProvider.repo as Record<string, string>;
540
+ const record = asRecord(doc);
541
+ const rawItems = Array.isArray(doc)
542
+ ? doc
543
+ : arrayValue(record?.project_items).length ? arrayValue(record?.project_items)
544
+ : arrayValue(record?.projectItems).length ? arrayValue(record?.projectItems)
545
+ : arrayValue(record?.items).length ? arrayValue(record?.items)
546
+ : arrayValue(asRecord(asRecord(asRecord(record?.data)?.organization)?.projectV2)?.items).length
547
+ ? arrayValue(asRecord(asRecord(asRecord(record?.data)?.organization)?.projectV2)?.items)
548
+ : [];
549
+ const nodes = rawItems.flatMap((item) => asRecord(item)?.nodes && Array.isArray(asRecord(item)?.nodes) ? arrayValue(asRecord(item)?.nodes) : [item]);
550
+ return nodes.flatMap((item, index) => normalizeBoardItem(item, index, repo.owner, repo.name) ?? []);
551
+ }
552
+
553
+ function openIssuesFromDoc(doc: unknown, settings: Record<string, unknown>): BoardIssue[] {
554
+ const repos = configuredRepos(settings);
555
+ const fallback = repos.length === 1 ? repos[0] : undefined;
556
+ const record = asRecord(doc);
557
+ const rawOpenIssues = record?.open_issues;
558
+ const rawIssues = arrayValue(rawOpenIssues).length ? arrayValue(rawOpenIssues)
559
+ : rawOpenIssues && typeof rawOpenIssues === "object" && !Array.isArray(rawOpenIssues)
560
+ ? Object.entries(rawOpenIssues as Record<string, unknown>).flatMap(([repoName, issues]) => arrayValue(issues).map((issue) => ({ issue, repoName })))
561
+ : arrayValue(record?.issues).length ? arrayValue(record?.issues)
562
+ : arrayValue(asRecord(asRecord(asRecord(record?.data)?.repository)?.issues)?.nodes);
563
+ return rawIssues.flatMap((issue) => {
564
+ const wrapped = asRecord(issue);
565
+ const rawIssue = wrapped && "issue" in wrapped ? wrapped.issue : issue;
566
+ const repoName = wrapped && typeof wrapped.repoName === "string" ? wrapped.repoName : undefined;
567
+ const repoFallback = repoName ? repos.find((repo) => repo.name === repoName) : fallback;
568
+ const normalized = normalizeBoardIssue(rawIssue, repoFallback?.owner, repoName ?? repoFallback?.name);
569
+ return normalized && (normalized.state ?? "OPEN").toUpperCase() === "OPEN" ? [normalized] : [];
570
+ });
571
+ }
572
+
573
+ function boardCandidate(item: BoardItem, settings: Record<string, unknown>): Record<string, unknown> {
574
+ const workProvider = settings.work_item_provider as Record<string, unknown>;
575
+ const boardProvider = (settings.board_provider ?? {}) as Record<string, unknown>;
576
+ const issue = item.issue as BoardIssue;
577
+ const owner = issue.owner ?? ((workProvider.repo as Record<string, string>).owner);
578
+ const repo = issue.repo ?? ((workProvider.repo as Record<string, string>).name);
579
+ const body = issue.body ?? "";
580
+ const metadata = parseWorkItemMetadata(body);
581
+ const rawStatus = item.status ?? "Todo";
582
+ return {
583
+ id: `github:${owner}/${repo}#${issue.number}`,
584
+ title: issue.title,
585
+ body,
586
+ status: statusKey(rawStatus) ?? rawStatus.toLowerCase(),
587
+ labels: issue.labels,
588
+ priority: item.priority,
589
+ ...sourceRevisionFields(metadata, owner, repo),
590
+ blockers: mergeBlockers(normalizeStructuredBlockers(metadata, owner, repo), blockers(body, owner, repo)),
591
+ related_links: extractRefs(body, owner, repo),
592
+ source_provider: { role: "WorkItemProvider", kind: workProvider.kind, owner, repo, number: issue.number, url: issue.url, state: issue.state, node_id: issue.node_id ?? issue.id, capabilities: workProvider.capabilities ?? [] },
593
+ board_membership: {
594
+ role: "BoardProvider",
595
+ kind: boardProvider.kind,
596
+ board: boardProvider.board,
597
+ project_item_id: item.id,
598
+ position: item.position,
599
+ status: rawStatus,
600
+ priority: item.priority,
601
+ capabilities: boardProvider.capabilities ?? [],
602
+ },
603
+ artifact_refs: Array.from(body.matchAll(FLOW_ARTIFACT_PATTERN), (m) => (m.groups?.path ?? "").replace(/[.,]+$/, "")),
604
+ updated_at: issue.updatedAt,
605
+ created_at: issue.createdAt,
606
+ };
607
+ }
608
+
609
+ function priorityRank(priority: unknown): number {
610
+ const key = String(priority ?? "").trim().toUpperCase();
611
+ return ({ P0: 0, P1: 1, P2: 2 } as Record<string, number>)[key] ?? 99;
612
+ }
613
+
614
+ function boardResult(settings: Record<string, unknown>, boardItems: BoardItem[], openIssues: BoardIssue[]): Record<string, unknown> {
615
+ const selection = (settings.selection ?? {}) as Record<string, unknown>;
616
+ const filters = (selection.filters ?? {}) as Record<string, unknown>;
617
+ const readyStatuses = new Set(((filters.ready_statuses as string[] | undefined) ?? ["ready"]).map((status) => status.toLowerCase()));
618
+ const workProvider = settings.work_item_provider as Record<string, unknown>;
619
+ const repo = workProvider.repo as Record<string, string>;
620
+ const candidates = boardItems.map((item) => ({ item, candidate: boardCandidate(item, settings) }));
621
+ const readyQueue = candidates
622
+ .filter(({ item }) => readyStatuses.has(String(statusKey(item.status) ?? item.status ?? "").toLowerCase()))
623
+ .sort((left, right) => priorityRank(left.item.priority) - priorityRank(right.item.priority) || left.item.position - right.item.position)
624
+ .map(({ candidate }) => candidate);
625
+ const boardIssueKeys = new Set(boardItems.flatMap((item) => {
626
+ const issue = item.issue;
627
+ const key = repoKey(issue?.owner ?? repo.owner, issue?.repo ?? repo.name, issue?.number);
628
+ return key ? [key] : [];
629
+ }));
630
+ const intakeGaps = openIssues
631
+ .filter((issue) => !boardIssueKeys.has(repoKey(issue.owner ?? repo.owner, issue.repo ?? repo.name, issue.number) ?? ""))
632
+ .map((issue) => ({
633
+ id: `github:${issue.owner ?? repo.owner}/${issue.repo ?? repo.name}#${issue.number}`,
634
+ title: issue.title,
635
+ status: statusKey(issue.state) ?? "todo",
636
+ labels: issue.labels,
637
+ source_provider: { role: "WorkItemProvider", kind: workProvider.kind, owner: issue.owner ?? repo.owner, repo: issue.repo ?? repo.name, number: issue.number, url: issue.url, state: issue.state, node_id: issue.node_id ?? issue.id, capabilities: workProvider.capabilities ?? [] },
638
+ }));
639
+ const warnings = readyQueue.length === 0
640
+ ? [{ code: "zero_ready_items", message: "Configured BoardProvider yielded zero ready items; treat this as a dead readiness source and do not silently fall back to WorkItemProvider issue listing." }]
641
+ : [];
642
+ return { ready_queue: readyQueue, intake_gaps: intakeGaps, warnings };
643
+ }
644
+
645
+ const PROJECT_BOARD_QUERY = `
646
+ query($org: String!, $projectNumber: Int!, $repoOwner: String!, $repoName: String!, $itemCursor: String, $issueCursor: String) {
647
+ organization(login: $org) {
648
+ projectV2(number: $projectNumber) {
649
+ items(first: 100, after: $itemCursor) {
650
+ pageInfo { hasNextPage endCursor }
651
+ nodes {
652
+ id
653
+ fieldValues(first: 30) {
654
+ nodes {
655
+ ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2FieldCommon { name } } }
656
+ ... on ProjectV2ItemFieldTextValue { text field { ... on ProjectV2FieldCommon { name } } }
657
+ }
658
+ }
659
+ content {
660
+ ... on Issue {
661
+ id
662
+ number
663
+ title
664
+ body
665
+ state
666
+ url
667
+ updatedAt
668
+ createdAt
669
+ repository { name owner { login } }
670
+ labels(first: 50) { nodes { name } }
671
+ }
672
+ }
673
+ }
674
+ }
675
+ }
676
+ }
677
+ repository(owner: $repoOwner, name: $repoName) {
678
+ issues(first: 100, after: $issueCursor, states: OPEN) {
679
+ pageInfo { hasNextPage endCursor }
680
+ nodes {
681
+ id
682
+ number
683
+ title
684
+ body
685
+ state
686
+ url
687
+ updatedAt
688
+ createdAt
689
+ repository { name owner { login } }
690
+ labels(first: 50) { nodes { name } }
691
+ }
692
+ }
693
+ }
694
+ }`;
695
+
696
+ const REPO_ISSUES_QUERY = `
697
+ query($repoOwner: String!, $repoName: String!, $issueCursor: String) {
698
+ repository(owner: $repoOwner, name: $repoName) {
699
+ issues(first: 100, after: $issueCursor, states: OPEN) {
700
+ pageInfo { hasNextPage endCursor }
701
+ nodes {
702
+ id
703
+ number
704
+ title
705
+ body
706
+ state
707
+ url
708
+ updatedAt
709
+ createdAt
710
+ repository { name owner { login } }
711
+ labels(first: 50) { nodes { name } }
712
+ }
713
+ }
714
+ }
715
+ }`;
716
+
717
+ function ghGraphql(query: string, variables: Record<string, string | number | undefined>): Record<string, unknown> {
718
+ const args = ["api", "graphql", "-f", `query=${query}`];
719
+ for (const [key, value] of Object.entries(variables)) {
720
+ if (value !== undefined && value !== "") args.push("-F", `${key}=${value}`);
721
+ }
722
+ return JSON.parse(execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })) as Record<string, unknown>;
723
+ }
724
+
725
+ function liveBoardDoc(settings: Record<string, unknown>): Record<string, unknown> {
726
+ const workProvider = settings.work_item_provider as Record<string, unknown>;
727
+ const boardProvider = settings.board_provider as Record<string, unknown>;
728
+ const workRepo = workProvider.repo as Record<string, string>;
729
+ const repos = configuredRepos(settings);
730
+ const board = boardProvider.board as Record<string, unknown>;
731
+ const org = stringValue(board.owner) ?? stringValue((boardProvider.repo as Record<string, unknown> | undefined)?.owner) ?? workRepo.owner;
732
+ const projectNumber = Number(board.number);
733
+ if (!org || !Number.isFinite(projectNumber)) throw new Error("BoardProvider must configure board.owner or repo.owner and board.number for live reads");
734
+ const items: unknown[] = [];
735
+ const issues: unknown[] = [];
736
+ let itemCursor = "";
737
+ let issueCursor = "";
738
+ let fetchItems = true;
739
+ let fetchIssues = true;
740
+ while (fetchItems || fetchIssues) {
741
+ const page = ghGraphql(PROJECT_BOARD_QUERY, {
742
+ org,
743
+ projectNumber,
744
+ repoOwner: repos[0]?.owner ?? workRepo.owner,
745
+ repoName: repos[0]?.name ?? workRepo.name,
746
+ itemCursor,
747
+ issueCursor,
748
+ });
749
+ const projectItems = asRecord(asRecord(asRecord(page.data)?.organization)?.projectV2)?.items as Record<string, unknown> | undefined;
750
+ const repoIssues = asRecord(asRecord(page.data)?.repository)?.issues as Record<string, unknown> | undefined;
751
+ if (fetchItems) items.push(...arrayValue(projectItems?.nodes));
752
+ if (fetchIssues) issues.push(...arrayValue(repoIssues?.nodes));
753
+ const itemPageInfo = asRecord(projectItems?.pageInfo);
754
+ const issuePageInfo = asRecord(repoIssues?.pageInfo);
755
+ fetchItems = Boolean(itemPageInfo?.hasNextPage);
756
+ fetchIssues = Boolean(issuePageInfo?.hasNextPage);
757
+ itemCursor = stringValue(itemPageInfo?.endCursor) ?? "";
758
+ issueCursor = stringValue(issuePageInfo?.endCursor) ?? "";
759
+ }
760
+ for (const repo of repos.slice(1)) {
761
+ let repoIssueCursor = "";
762
+ let fetchRepoIssues = true;
763
+ while (fetchRepoIssues) {
764
+ const page = ghGraphql(REPO_ISSUES_QUERY, {
765
+ repoOwner: repo.owner,
766
+ repoName: repo.name,
767
+ issueCursor: repoIssueCursor,
768
+ });
769
+ const repoIssues = asRecord(asRecord(page.data)?.repository)?.issues as Record<string, unknown> | undefined;
770
+ issues.push(...arrayValue(repoIssues?.nodes));
771
+ const issuePageInfo = asRecord(repoIssues?.pageInfo);
772
+ fetchRepoIssues = Boolean(issuePageInfo?.hasNextPage);
773
+ repoIssueCursor = stringValue(issuePageInfo?.endCursor) ?? "";
774
+ }
775
+ }
776
+ return { items, issues };
777
+ }
778
+
415
779
  function classify(item: Record<string, unknown>, settings: Record<string, unknown>, resolved: Record<string, string>): Record<string, unknown> {
416
780
  const selection = (settings.selection ?? {}) as Record<string, unknown>;
417
781
  const filters = (selection.filters ?? {}) as Record<string, unknown>;
@@ -448,7 +812,11 @@ function resolveSettings(doc: Record<string, unknown>): Record<string, unknown>
448
812
  if (typeof doc.settings === "object" && doc.settings !== null) return doc.settings as Record<string, unknown>;
449
813
  if (doc.work_item_provider) return doc;
450
814
  if (Array.isArray(doc.projects) && doc.projects.length) return doc.projects[0] as Record<string, unknown>;
451
- if (typeof doc.defaults === "object" && doc.defaults !== null) return doc.defaults as Record<string, unknown>;
815
+ if (typeof doc.defaults === "object" && doc.defaults !== null) {
816
+ const settings = structuredClone(doc.defaults) as Record<string, unknown>;
817
+ if (typeof doc.workspace === "object" && doc.workspace !== null) settings.workspace = doc.workspace;
818
+ return settings;
819
+ }
452
820
  throw new Error("settings JSON must be an effective settings object or backlog-provider-settings document");
453
821
  }
454
822
 
@@ -456,7 +824,8 @@ export function main(argv = process.argv.slice(2)): number {
456
824
  const args = parseArgs(argv);
457
825
  const settingsJson = flagString(args.flags, "settings-json");
458
826
  const issuesJson = flagString(args.flags, "issues-json");
459
- if (!settingsJson || !issuesJson) throw new Error("--settings-json and --issues-json are required");
827
+ const itemsJson = flagString(args.flags, "items-json");
828
+ if (!settingsJson) throw new Error("--settings-json is required");
460
829
  const now = new Date(flagString(args.flags, "now") ?? Date.now());
461
830
  if (Number.isNaN(now.getTime())) throw new Error("--now must be a valid date/time when provided");
462
831
  const freshnessInputs: FreshnessInputs = {
@@ -467,6 +836,11 @@ export function main(argv = process.argv.slice(2)): number {
467
836
  now,
468
837
  };
469
838
  const settings = resolveSettings(loadJson(settingsJson) as Record<string, unknown>);
839
+ if (itemsJson || !issuesJson) {
840
+ const doc = itemsJson ? loadJson(itemsJson) : liveBoardDoc(settings);
841
+ console.log(JSON.stringify(boardResult(settings, boardItemsFromDoc(doc, settings), openIssuesFromDoc(doc, settings)), null, 2));
842
+ return 0;
843
+ }
470
844
  const issuesDoc = loadJson(issuesJson);
471
845
  const issues = Array.isArray(issuesDoc) ? issuesDoc : ((issuesDoc as Record<string, unknown>).items as unknown[] ?? [issuesDoc]);
472
846
  const resolved = Object.fromEntries(flagList(args.flags, "resolved-ref").map((item) => item.split("=", 2) as [string, string]));
@@ -17,6 +17,7 @@ import {
17
17
  validateEvidenceRef,
18
18
  normalizeEvidenceRefs,
19
19
  normalizeCheck,
20
+ reduceCaptureLogByCommand,
20
21
  } from "../../build/src/cli/workflow-sidecar.js";
21
22
  import { buildClaimExplanation } from "../../build/src/cli/sidecar-claim-explain.js";
22
23
 
@@ -111,6 +112,69 @@ test("normalizeCheck: validates required fields, kind, and status", () => {
111
112
  assert.equal(ok.status, "pass");
112
113
  });
113
114
 
115
+ // ── reduceCaptureLogByCommand (#470 iteration 2, finding #2 — three-way capture fold) ────
116
+
117
+ test("reduceCaptureLogByCommand: a single ambiguous entry classifies as ambiguous (non-confirming), not pass", () => {
118
+ const out = reduceCaptureLogByCommand([{ command: "npm test", observedResult: "ambiguous", exitCode: null }]);
119
+ const entry = out.get("npm test");
120
+ assert.equal(entry.observedResult, "ambiguous");
121
+ assert.equal(entry.exitCode, null);
122
+ });
123
+
124
+ test("reduceCaptureLogByCommand: fail beats pass and ambiguous for the same command", () => {
125
+ const out = reduceCaptureLogByCommand([
126
+ { command: "npm test", observedResult: "pass", exitCode: 0 },
127
+ { command: "npm test", observedResult: "ambiguous", exitCode: null },
128
+ { command: "npm test", observedResult: "fail", exitCode: 1 },
129
+ ]);
130
+ const entry = out.get("npm test");
131
+ assert.equal(entry.observedResult, "fail");
132
+ assert.equal(entry.exitCode, 1);
133
+ });
134
+
135
+ test("reduceCaptureLogByCommand: fail beats pass and ambiguous regardless of entry order", () => {
136
+ const out = reduceCaptureLogByCommand([
137
+ { command: "npm test", observedResult: "fail", exitCode: 1 },
138
+ { command: "npm test", observedResult: "pass", exitCode: 0 },
139
+ ]);
140
+ assert.equal(out.get("npm test").observedResult, "fail");
141
+ });
142
+
143
+ test("reduceCaptureLogByCommand: pass beats ambiguous when there is no fail", () => {
144
+ const out = reduceCaptureLogByCommand([
145
+ { command: "npm test", observedResult: "ambiguous", exitCode: null },
146
+ { command: "npm test", observedResult: "pass", exitCode: 0 },
147
+ ]);
148
+ const entry = out.get("npm test");
149
+ assert.equal(entry.observedResult, "pass");
150
+ assert.equal(entry.exitCode, 0);
151
+ });
152
+
153
+ test("reduceCaptureLogByCommand: legacy entries (no observedResult) classify from exitCode alone", () => {
154
+ const out = reduceCaptureLogByCommand([
155
+ { command: "legacy-pass", exitCode: 0 },
156
+ { command: "legacy-fail", exitCode: 1 },
157
+ { command: "legacy-ambiguous", exitCode: null },
158
+ ]);
159
+ assert.equal(out.get("legacy-pass").observedResult, "pass");
160
+ assert.equal(out.get("legacy-fail").observedResult, "fail");
161
+ assert.equal(out.get("legacy-ambiguous").observedResult, "ambiguous");
162
+ });
163
+
164
+ test("reduceCaptureLogByCommand: legacy nonzero exit code classifies as fail (never coerced to pass)", () => {
165
+ const out = reduceCaptureLogByCommand([{ command: "some cmd", exitCode: 2 }]);
166
+ const entry = out.get("some cmd");
167
+ assert.equal(entry.observedResult, "fail");
168
+ assert.equal(entry.exitCode, 2);
169
+ });
170
+
171
+ test("reduceCaptureLogByCommand: grep/diff absence-ambiguous entry (observedResult:'ambiguous', exitCode:1) stays ambiguous, not fail", () => {
172
+ const out = reduceCaptureLogByCommand([{ command: "grep -q needle file", observedResult: "ambiguous", exitCode: 1 }]);
173
+ const entry = out.get("grep -q needle file");
174
+ assert.equal(entry.observedResult, "ambiguous");
175
+ assert.equal(entry.exitCode, 1);
176
+ });
177
+
114
178
  // ── buildGateInquiryRecords (pure orchestration, fake Surface injected) ───────
115
179
 
116
180
  test("buildGateInquiryRecords: resolves each claim through the injected Surface and tags calibration", () => {