@kontourai/flow-agents 3.3.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 (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 +41 -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 +54 -1
  34. package/build/src/lib/flow-resolver.js +112 -5
  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 +72 -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 +117 -4
  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
@@ -5,10 +5,11 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseArgs, flagBool, flagString } from "../lib/args.js";
8
- import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
8
+ import { assertPathContained, assertPathsDisjoint, atomicWriteJson, copyDirAtomic, ensureSafeDirectory, isoNow, readJson, walkFiles } from "../lib/fs.js";
9
9
  import { assertKitRepository, deriveKitTargets, parseKitDependencies } from "../flow-kit/validate.js";
10
10
  import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
11
11
  import { defaultCodexHome } from "../lib/local-artifact-root.js";
12
+ import { root } from "../tools/common.js";
12
13
  const REGISTRY_REL = path.join("kits", "local", "installed-kits.json");
13
14
  const REPOSITORIES_REL = path.join("kits", "local", "repositories");
14
15
  function registryPath(dest) { return path.join(dest, REGISTRY_REL); }
@@ -17,6 +18,23 @@ function resolveDest(flags) {
17
18
  const explicit = flagString(flags, "dest");
18
19
  return path.resolve(explicit ?? defaultCodexHome());
19
20
  }
21
+ function resolveCatalogKitSource(source) {
22
+ if (!/^[a-z][a-z0-9-]*$/.test(source))
23
+ return null;
24
+ const repoCatalogPath = path.join(root, "kits", "catalog.json");
25
+ if (!fs.existsSync(repoCatalogPath))
26
+ return null;
27
+ const catalog = readJson(repoCatalogPath);
28
+ const entry = Array.isArray(catalog.kits)
29
+ ? catalog.kits.find((item) => typeof item === "object" && item !== null && item.id === source)
30
+ : undefined;
31
+ if (!entry || typeof entry !== "object")
32
+ return null;
33
+ const rel = entry.path;
34
+ if (typeof rel !== "string")
35
+ return null;
36
+ return path.resolve(path.dirname(repoCatalogPath), "..", rel);
37
+ }
20
38
  function loadRegistry(dest) {
21
39
  const file = registryPath(dest);
22
40
  if (!fs.existsSync(file))
@@ -96,7 +114,7 @@ async function install(argv) {
96
114
  if (isGitUrl) {
97
115
  return await installGitSource(source, argv);
98
116
  }
99
- return await installLocalSource(path.resolve(source), argv);
117
+ return await installLocalSource(resolveCatalogKitSource(source) ?? path.resolve(source), argv);
100
118
  }
101
119
  async function installLocalSource(source, argv) {
102
120
  const args = parseArgs(argv);
@@ -112,13 +130,26 @@ async function installLocalSource(source, argv) {
112
130
  return 1;
113
131
  }
114
132
  const kitId = String(manifest.id);
133
+ const target = installedPath(dest, kitId);
134
+ try {
135
+ assertPathsDisjoint(source, target);
136
+ ensureSafeDirectory(dest, dest);
137
+ assertPathContained(dest, target);
138
+ ensureSafeDirectory(dest, path.dirname(target));
139
+ const registryFile = registryPath(dest);
140
+ ensureSafeDirectory(dest, path.dirname(registryFile));
141
+ if (fs.existsSync(registryFile) && fs.lstatSync(registryFile).isSymbolicLink()) {
142
+ throw new Error(`refusing to read or replace symlink: ${registryFile}`);
143
+ }
144
+ }
145
+ catch (error) {
146
+ console.error(`install: unsafe source or destination: ${error.message}`);
147
+ return 1;
148
+ }
115
149
  warnUninstalledDependencies(manifest, path.join(source, "kit.json"), dest);
116
150
  const hash = contentHash(source);
117
151
  const registry = loadRegistry(dest);
118
152
  const existing = registry.kits.find((entry) => entry.id === kitId);
119
- const target = installedPath(dest, kitId);
120
- fs.mkdirSync(dest, { recursive: true });
121
- assertPathContained(dest, target);
122
153
  const sourceText = source;
123
154
  if (existing && existing.source !== sourceText && !flagBool(args.flags, "update")) {
124
155
  console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
@@ -128,7 +159,7 @@ async function installLocalSource(source, argv) {
128
159
  console.log(`kit '${kitId}' is already installed from ${sourceText}`);
129
160
  return 0;
130
161
  }
131
- copyDir(source, target);
162
+ copyDirAtomic(dest, source, target);
132
163
  const entry = {
133
164
  id: kitId,
134
165
  source: sourceText,
@@ -140,7 +171,7 @@ async function installLocalSource(source, argv) {
140
171
  if (typeof manifest.version === "string" && manifest.version)
141
172
  entry.version = manifest.version;
142
173
  registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
143
- writeJson(registryPath(dest), registry);
174
+ atomicWriteJson(dest, registryPath(dest), registry);
144
175
  console.log(`${existing ? "updated" : "installed"} local kit '${kitId}' at ${target}`);
145
176
  return 0;
146
177
  }
@@ -195,8 +226,20 @@ async function installGitSource(rawUrl, argv) {
195
226
  const registry = loadRegistry(dest);
196
227
  const existing = registry.kits.find((entry) => entry.id === kitId);
197
228
  const target = installedPath(dest, kitId);
198
- fs.mkdirSync(dest, { recursive: true });
199
- assertPathContained(dest, target);
229
+ try {
230
+ ensureSafeDirectory(dest, dest);
231
+ assertPathContained(dest, target);
232
+ ensureSafeDirectory(dest, path.dirname(target));
233
+ const registryFile = registryPath(dest);
234
+ ensureSafeDirectory(dest, path.dirname(registryFile));
235
+ if (fs.existsSync(registryFile) && fs.lstatSync(registryFile).isSymbolicLink()) {
236
+ throw new Error(`refusing to read or replace symlink: ${registryFile}`);
237
+ }
238
+ }
239
+ catch (error) {
240
+ console.error(`install: unsafe destination: ${error.message}`);
241
+ return 1;
242
+ }
200
243
  const sourceText = repoUrl + (ref ? `#${ref}` : "");
201
244
  if (existing && existing.source !== sourceText && !update) {
202
245
  console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
@@ -206,7 +249,7 @@ async function installGitSource(rawUrl, argv) {
206
249
  console.log(`kit '${kitId}' is already installed from ${sourceText}`);
207
250
  return 0;
208
251
  }
209
- copyDir(tmpBase, target);
252
+ copyDirAtomic(dest, tmpBase, target);
210
253
  const entry = {
211
254
  id: kitId,
212
255
  source: sourceText,
@@ -218,7 +261,7 @@ async function installGitSource(rawUrl, argv) {
218
261
  if (typeof manifest.version === "string" && manifest.version)
219
262
  entry.version = manifest.version;
220
263
  registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
221
- writeJson(registryPath(dest), registry);
264
+ atomicWriteJson(dest, registryPath(dest), registry);
222
265
  console.log(`${existing ? "updated" : "installed"} git kit '${kitId}' from ${sourceText} at ${target}`);
223
266
  return 0;
224
267
  }
@@ -313,7 +356,7 @@ async function inspect(argv) {
313
356
  return 1;
314
357
  }
315
358
  // Pass the real kitDir so @kontourai/flow can validate flow file existence for K0.
316
- const result = await deriveKitTargets(manifest, kitDir);
359
+ const result = await deriveKitTargets(manifest, kitDir, root);
317
360
  console.log(JSON.stringify(result, null, 2));
318
361
  return result.conformance.k0 ? 0 : 1;
319
362
  }
@@ -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
  const FLOW_ARTIFACT_PATTERN = /(?<path>\.kontourai\/flow-agents\/[^\s`'")]+)/g;
@@ -11,6 +12,12 @@ const DAY_MS = 24 * 60 * 60 * 1000;
11
12
  function loadJson(file) {
12
13
  return file === "-" ? JSON.parse(fs.readFileSync(0, "utf8")) : JSON.parse(fs.readFileSync(file, "utf8"));
13
14
  }
15
+ function asRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
17
+ }
18
+ function arrayValue(value) {
19
+ return Array.isArray(value) ? value : [];
20
+ }
14
21
  function names(values) {
15
22
  return Array.isArray(values) ? values.flatMap((item) => typeof item === "object" && item !== null && typeof item.name === "string" ? [item.name] : []) : [];
16
23
  }
@@ -18,7 +25,33 @@ function statusKey(value) {
18
25
  if (typeof value !== "string")
19
26
  return null;
20
27
  const key = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
21
- 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" }[key] ?? key;
28
+ 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" }[key] ?? key;
29
+ }
30
+ function normalizedText(value) {
31
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
32
+ }
33
+ function repoKey(owner, repo, number) {
34
+ const issueNumber = Number(number);
35
+ return typeof owner === "string" && typeof repo === "string" && Number.isFinite(issueNumber)
36
+ ? `${owner}/${repo}#${issueNumber}`
37
+ : null;
38
+ }
39
+ function configuredRepos(settings) {
40
+ const workProvider = settings.work_item_provider;
41
+ const boardProvider = (settings.board_provider ?? {});
42
+ const workRepo = workProvider.repo;
43
+ const board = (boardProvider.board ?? {});
44
+ const defaultOwner = stringValue(board.owner) ?? stringValue(boardProvider.repo?.owner) ?? workRepo.owner;
45
+ const workspace = asRecord(settings.workspace);
46
+ const repos = arrayValue(workspace?.repos).flatMap((repo) => {
47
+ if (typeof repo === "string" && repo.trim())
48
+ return [{ owner: defaultOwner, name: repo.trim() }];
49
+ const record = asRecord(repo);
50
+ const owner = stringValue(record?.owner) ?? defaultOwner;
51
+ const name = stringValue(record?.name);
52
+ return owner && name ? [{ owner, name }] : [];
53
+ });
54
+ return repos.length ? repos : [{ owner: workRepo.owner, name: workRepo.name }];
22
55
  }
23
56
  function projectStatus(issue) {
24
57
  for (const item of (Array.isArray(issue.projectItems) ? issue.projectItems : [])) {
@@ -393,6 +426,304 @@ function normalize(issue, settings) {
393
426
  created_at: issue.createdAt,
394
427
  };
395
428
  }
429
+ function fieldValueByName(item, fieldName) {
430
+ const wanted = fieldName.toLowerCase();
431
+ const direct = item[fieldName] ?? item[fieldName.toLowerCase()];
432
+ if (typeof direct === "string")
433
+ return direct;
434
+ const fieldValues = Array.isArray(item.fieldValues) ? item.fieldValues : arrayValue(asRecord(item.fieldValues)?.nodes);
435
+ for (const value of fieldValues) {
436
+ const record = asRecord(value);
437
+ if (!record)
438
+ continue;
439
+ const field = asRecord(record.field);
440
+ const name = normalizedText(field?.name);
441
+ if (name !== wanted)
442
+ continue;
443
+ return stringValue(record.name) ?? stringValue(record.text) ?? stringValue(record.value) ?? stringValue(record.optionName);
444
+ }
445
+ return undefined;
446
+ }
447
+ function normalizeBoardIssue(rawIssue, fallbackOwner, fallbackRepo) {
448
+ const issue = asRecord(rawIssue);
449
+ if (!issue)
450
+ return null;
451
+ const number = Number(issue.number);
452
+ if (!Number.isFinite(number))
453
+ return null;
454
+ const repository = asRecord(issue.repository);
455
+ const owner = stringValue(issue.owner) ?? stringValue(asRecord(repository?.owner)?.login) ?? fallbackOwner;
456
+ const repo = stringValue(issue.repo) ?? stringValue(issue.repositoryName) ?? stringValue(repository?.name) ?? fallbackRepo;
457
+ return {
458
+ id: stringValue(issue.id),
459
+ node_id: stringValue(issue.node_id) ?? stringValue(issue.id),
460
+ owner,
461
+ repo,
462
+ number,
463
+ title: stringValue(issue.title) ?? `#${number}`,
464
+ body: stringValue(issue.body) ?? "",
465
+ state: stringValue(issue.state) ?? "OPEN",
466
+ labels: names(issue.labels).concat(arrayValue(asRecord(issue.labels)?.nodes).flatMap((label) => stringValue(asRecord(label)?.name) ?? [])),
467
+ url: stringValue(issue.url),
468
+ updatedAt: stringValue(issue.updatedAt),
469
+ createdAt: stringValue(issue.createdAt),
470
+ };
471
+ }
472
+ function normalizeBoardItem(rawItem, index, fallbackOwner, fallbackRepo) {
473
+ const item = asRecord(rawItem);
474
+ if (!item)
475
+ return null;
476
+ const content = item.content ?? item.issue;
477
+ const issue = normalizeBoardIssue(content, fallbackOwner, fallbackRepo);
478
+ if (!issue)
479
+ return null;
480
+ const rawPosition = item.position ?? item.databaseId ?? item.index;
481
+ const position = Number.isFinite(Number(rawPosition)) ? Number(rawPosition) : index;
482
+ return {
483
+ id: stringValue(item.id),
484
+ position,
485
+ status: fieldValueByName(item, "Status") ?? stringValue(item.status),
486
+ priority: fieldValueByName(item, "Priority") ?? stringValue(item.priority),
487
+ issue,
488
+ };
489
+ }
490
+ function boardItemsFromDoc(doc, settings) {
491
+ const workProvider = settings.work_item_provider;
492
+ const repo = workProvider.repo;
493
+ const record = asRecord(doc);
494
+ const rawItems = Array.isArray(doc)
495
+ ? doc
496
+ : arrayValue(record?.project_items).length ? arrayValue(record?.project_items)
497
+ : arrayValue(record?.projectItems).length ? arrayValue(record?.projectItems)
498
+ : arrayValue(record?.items).length ? arrayValue(record?.items)
499
+ : arrayValue(asRecord(asRecord(asRecord(record?.data)?.organization)?.projectV2)?.items).length
500
+ ? arrayValue(asRecord(asRecord(asRecord(record?.data)?.organization)?.projectV2)?.items)
501
+ : [];
502
+ const nodes = rawItems.flatMap((item) => asRecord(item)?.nodes && Array.isArray(asRecord(item)?.nodes) ? arrayValue(asRecord(item)?.nodes) : [item]);
503
+ return nodes.flatMap((item, index) => normalizeBoardItem(item, index, repo.owner, repo.name) ?? []);
504
+ }
505
+ function openIssuesFromDoc(doc, settings) {
506
+ const repos = configuredRepos(settings);
507
+ const fallback = repos.length === 1 ? repos[0] : undefined;
508
+ const record = asRecord(doc);
509
+ const rawOpenIssues = record?.open_issues;
510
+ const rawIssues = arrayValue(rawOpenIssues).length ? arrayValue(rawOpenIssues)
511
+ : rawOpenIssues && typeof rawOpenIssues === "object" && !Array.isArray(rawOpenIssues)
512
+ ? Object.entries(rawOpenIssues).flatMap(([repoName, issues]) => arrayValue(issues).map((issue) => ({ issue, repoName })))
513
+ : arrayValue(record?.issues).length ? arrayValue(record?.issues)
514
+ : arrayValue(asRecord(asRecord(asRecord(record?.data)?.repository)?.issues)?.nodes);
515
+ return rawIssues.flatMap((issue) => {
516
+ const wrapped = asRecord(issue);
517
+ const rawIssue = wrapped && "issue" in wrapped ? wrapped.issue : issue;
518
+ const repoName = wrapped && typeof wrapped.repoName === "string" ? wrapped.repoName : undefined;
519
+ const repoFallback = repoName ? repos.find((repo) => repo.name === repoName) : fallback;
520
+ const normalized = normalizeBoardIssue(rawIssue, repoFallback?.owner, repoName ?? repoFallback?.name);
521
+ return normalized && (normalized.state ?? "OPEN").toUpperCase() === "OPEN" ? [normalized] : [];
522
+ });
523
+ }
524
+ function boardCandidate(item, settings) {
525
+ const workProvider = settings.work_item_provider;
526
+ const boardProvider = (settings.board_provider ?? {});
527
+ const issue = item.issue;
528
+ const owner = issue.owner ?? (workProvider.repo.owner);
529
+ const repo = issue.repo ?? (workProvider.repo.name);
530
+ const body = issue.body ?? "";
531
+ const metadata = parseWorkItemMetadata(body);
532
+ const rawStatus = item.status ?? "Todo";
533
+ return {
534
+ id: `github:${owner}/${repo}#${issue.number}`,
535
+ title: issue.title,
536
+ body,
537
+ status: statusKey(rawStatus) ?? rawStatus.toLowerCase(),
538
+ labels: issue.labels,
539
+ priority: item.priority,
540
+ ...sourceRevisionFields(metadata, owner, repo),
541
+ blockers: mergeBlockers(normalizeStructuredBlockers(metadata, owner, repo), blockers(body, owner, repo)),
542
+ related_links: extractRefs(body, owner, repo),
543
+ 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 ?? [] },
544
+ board_membership: {
545
+ role: "BoardProvider",
546
+ kind: boardProvider.kind,
547
+ board: boardProvider.board,
548
+ project_item_id: item.id,
549
+ position: item.position,
550
+ status: rawStatus,
551
+ priority: item.priority,
552
+ capabilities: boardProvider.capabilities ?? [],
553
+ },
554
+ artifact_refs: Array.from(body.matchAll(FLOW_ARTIFACT_PATTERN), (m) => (m.groups?.path ?? "").replace(/[.,]+$/, "")),
555
+ updated_at: issue.updatedAt,
556
+ created_at: issue.createdAt,
557
+ };
558
+ }
559
+ function priorityRank(priority) {
560
+ const key = String(priority ?? "").trim().toUpperCase();
561
+ return { P0: 0, P1: 1, P2: 2 }[key] ?? 99;
562
+ }
563
+ function boardResult(settings, boardItems, openIssues) {
564
+ const selection = (settings.selection ?? {});
565
+ const filters = (selection.filters ?? {});
566
+ const readyStatuses = new Set((filters.ready_statuses ?? ["ready"]).map((status) => status.toLowerCase()));
567
+ const workProvider = settings.work_item_provider;
568
+ const repo = workProvider.repo;
569
+ const candidates = boardItems.map((item) => ({ item, candidate: boardCandidate(item, settings) }));
570
+ const readyQueue = candidates
571
+ .filter(({ item }) => readyStatuses.has(String(statusKey(item.status) ?? item.status ?? "").toLowerCase()))
572
+ .sort((left, right) => priorityRank(left.item.priority) - priorityRank(right.item.priority) || left.item.position - right.item.position)
573
+ .map(({ candidate }) => candidate);
574
+ const boardIssueKeys = new Set(boardItems.flatMap((item) => {
575
+ const issue = item.issue;
576
+ const key = repoKey(issue?.owner ?? repo.owner, issue?.repo ?? repo.name, issue?.number);
577
+ return key ? [key] : [];
578
+ }));
579
+ const intakeGaps = openIssues
580
+ .filter((issue) => !boardIssueKeys.has(repoKey(issue.owner ?? repo.owner, issue.repo ?? repo.name, issue.number) ?? ""))
581
+ .map((issue) => ({
582
+ id: `github:${issue.owner ?? repo.owner}/${issue.repo ?? repo.name}#${issue.number}`,
583
+ title: issue.title,
584
+ status: statusKey(issue.state) ?? "todo",
585
+ labels: issue.labels,
586
+ 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 ?? [] },
587
+ }));
588
+ const warnings = readyQueue.length === 0
589
+ ? [{ 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." }]
590
+ : [];
591
+ return { ready_queue: readyQueue, intake_gaps: intakeGaps, warnings };
592
+ }
593
+ const PROJECT_BOARD_QUERY = `
594
+ query($org: String!, $projectNumber: Int!, $repoOwner: String!, $repoName: String!, $itemCursor: String, $issueCursor: String) {
595
+ organization(login: $org) {
596
+ projectV2(number: $projectNumber) {
597
+ items(first: 100, after: $itemCursor) {
598
+ pageInfo { hasNextPage endCursor }
599
+ nodes {
600
+ id
601
+ fieldValues(first: 30) {
602
+ nodes {
603
+ ... on ProjectV2ItemFieldSingleSelectValue { name field { ... on ProjectV2FieldCommon { name } } }
604
+ ... on ProjectV2ItemFieldTextValue { text field { ... on ProjectV2FieldCommon { name } } }
605
+ }
606
+ }
607
+ content {
608
+ ... on Issue {
609
+ id
610
+ number
611
+ title
612
+ body
613
+ state
614
+ url
615
+ updatedAt
616
+ createdAt
617
+ repository { name owner { login } }
618
+ labels(first: 50) { nodes { name } }
619
+ }
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+ repository(owner: $repoOwner, name: $repoName) {
626
+ issues(first: 100, after: $issueCursor, states: OPEN) {
627
+ pageInfo { hasNextPage endCursor }
628
+ nodes {
629
+ id
630
+ number
631
+ title
632
+ body
633
+ state
634
+ url
635
+ updatedAt
636
+ createdAt
637
+ repository { name owner { login } }
638
+ labels(first: 50) { nodes { name } }
639
+ }
640
+ }
641
+ }
642
+ }`;
643
+ const REPO_ISSUES_QUERY = `
644
+ query($repoOwner: String!, $repoName: String!, $issueCursor: String) {
645
+ repository(owner: $repoOwner, name: $repoName) {
646
+ issues(first: 100, after: $issueCursor, states: OPEN) {
647
+ pageInfo { hasNextPage endCursor }
648
+ nodes {
649
+ id
650
+ number
651
+ title
652
+ body
653
+ state
654
+ url
655
+ updatedAt
656
+ createdAt
657
+ repository { name owner { login } }
658
+ labels(first: 50) { nodes { name } }
659
+ }
660
+ }
661
+ }
662
+ }`;
663
+ function ghGraphql(query, variables) {
664
+ const args = ["api", "graphql", "-f", `query=${query}`];
665
+ for (const [key, value] of Object.entries(variables)) {
666
+ if (value !== undefined && value !== "")
667
+ args.push("-F", `${key}=${value}`);
668
+ }
669
+ return JSON.parse(execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }));
670
+ }
671
+ function liveBoardDoc(settings) {
672
+ const workProvider = settings.work_item_provider;
673
+ const boardProvider = settings.board_provider;
674
+ const workRepo = workProvider.repo;
675
+ const repos = configuredRepos(settings);
676
+ const board = boardProvider.board;
677
+ const org = stringValue(board.owner) ?? stringValue(boardProvider.repo?.owner) ?? workRepo.owner;
678
+ const projectNumber = Number(board.number);
679
+ if (!org || !Number.isFinite(projectNumber))
680
+ throw new Error("BoardProvider must configure board.owner or repo.owner and board.number for live reads");
681
+ const items = [];
682
+ const issues = [];
683
+ let itemCursor = "";
684
+ let issueCursor = "";
685
+ let fetchItems = true;
686
+ let fetchIssues = true;
687
+ while (fetchItems || fetchIssues) {
688
+ const page = ghGraphql(PROJECT_BOARD_QUERY, {
689
+ org,
690
+ projectNumber,
691
+ repoOwner: repos[0]?.owner ?? workRepo.owner,
692
+ repoName: repos[0]?.name ?? workRepo.name,
693
+ itemCursor,
694
+ issueCursor,
695
+ });
696
+ const projectItems = asRecord(asRecord(asRecord(page.data)?.organization)?.projectV2)?.items;
697
+ const repoIssues = asRecord(asRecord(page.data)?.repository)?.issues;
698
+ if (fetchItems)
699
+ items.push(...arrayValue(projectItems?.nodes));
700
+ if (fetchIssues)
701
+ issues.push(...arrayValue(repoIssues?.nodes));
702
+ const itemPageInfo = asRecord(projectItems?.pageInfo);
703
+ const issuePageInfo = asRecord(repoIssues?.pageInfo);
704
+ fetchItems = Boolean(itemPageInfo?.hasNextPage);
705
+ fetchIssues = Boolean(issuePageInfo?.hasNextPage);
706
+ itemCursor = stringValue(itemPageInfo?.endCursor) ?? "";
707
+ issueCursor = stringValue(issuePageInfo?.endCursor) ?? "";
708
+ }
709
+ for (const repo of repos.slice(1)) {
710
+ let repoIssueCursor = "";
711
+ let fetchRepoIssues = true;
712
+ while (fetchRepoIssues) {
713
+ const page = ghGraphql(REPO_ISSUES_QUERY, {
714
+ repoOwner: repo.owner,
715
+ repoName: repo.name,
716
+ issueCursor: repoIssueCursor,
717
+ });
718
+ const repoIssues = asRecord(asRecord(page.data)?.repository)?.issues;
719
+ issues.push(...arrayValue(repoIssues?.nodes));
720
+ const issuePageInfo = asRecord(repoIssues?.pageInfo);
721
+ fetchRepoIssues = Boolean(issuePageInfo?.hasNextPage);
722
+ repoIssueCursor = stringValue(issuePageInfo?.endCursor) ?? "";
723
+ }
724
+ }
725
+ return { items, issues };
726
+ }
396
727
  function classify(item, settings, resolved) {
397
728
  const selection = (settings.selection ?? {});
398
729
  const filters = (selection.filters ?? {});
@@ -434,16 +765,21 @@ function resolveSettings(doc) {
434
765
  return doc;
435
766
  if (Array.isArray(doc.projects) && doc.projects.length)
436
767
  return doc.projects[0];
437
- if (typeof doc.defaults === "object" && doc.defaults !== null)
438
- return doc.defaults;
768
+ if (typeof doc.defaults === "object" && doc.defaults !== null) {
769
+ const settings = structuredClone(doc.defaults);
770
+ if (typeof doc.workspace === "object" && doc.workspace !== null)
771
+ settings.workspace = doc.workspace;
772
+ return settings;
773
+ }
439
774
  throw new Error("settings JSON must be an effective settings object or backlog-provider-settings document");
440
775
  }
441
776
  export function main(argv = process.argv.slice(2)) {
442
777
  const args = parseArgs(argv);
443
778
  const settingsJson = flagString(args.flags, "settings-json");
444
779
  const issuesJson = flagString(args.flags, "issues-json");
445
- if (!settingsJson || !issuesJson)
446
- throw new Error("--settings-json and --issues-json are required");
780
+ const itemsJson = flagString(args.flags, "items-json");
781
+ if (!settingsJson)
782
+ throw new Error("--settings-json is required");
447
783
  const now = new Date(flagString(args.flags, "now") ?? Date.now());
448
784
  if (Number.isNaN(now.getTime()))
449
785
  throw new Error("--now must be a valid date/time when provided");
@@ -455,6 +791,11 @@ export function main(argv = process.argv.slice(2)) {
455
791
  now,
456
792
  };
457
793
  const settings = resolveSettings(loadJson(settingsJson));
794
+ if (itemsJson || !issuesJson) {
795
+ const doc = itemsJson ? loadJson(itemsJson) : liveBoardDoc(settings);
796
+ console.log(JSON.stringify(boardResult(settings, boardItemsFromDoc(doc, settings), openIssuesFromDoc(doc, settings)), null, 2));
797
+ return 0;
798
+ }
458
799
  const issuesDoc = loadJson(issuesJson);
459
800
  const issues = Array.isArray(issuesDoc) ? issuesDoc : (issuesDoc.items ?? [issuesDoc]);
460
801
  const resolved = Object.fromEntries(flagList(args.flags, "resolved-ref").map((item) => item.split("=", 2)));
@@ -0,0 +1 @@
1
+ export declare function main(argv?: string[]): Promise<number>;