@kontourai/flow-agents 2.3.0 → 3.0.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 (301) hide show
  1. package/.github/CODEOWNERS +16 -0
  2. package/.github/workflows/ci.yml +44 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/AGENTS.md +4 -4
  5. package/CHANGELOG.md +62 -0
  6. package/CONTEXT.md +35 -0
  7. package/README.md +9 -3
  8. package/agents/dev.json +1 -1
  9. package/agents/tool-code-reviewer.json +1 -1
  10. package/agents/tool-planner.json +3 -3
  11. package/agents/tool-verifier.json +3 -3
  12. package/build/src/cli/assignment-provider.d.ts +1 -0
  13. package/build/src/cli/assignment-provider.js +748 -0
  14. package/build/src/cli/console-learning-projection.js +3 -2
  15. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  16. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  17. package/build/src/cli/init.js +104 -21
  18. package/build/src/cli/kit.js +37 -6
  19. package/build/src/cli/pull-work-provider.js +1 -1
  20. package/build/src/cli/usage-feedback.js +3 -3
  21. package/build/src/cli/validate-hook-influence.js +1 -0
  22. package/build/src/cli/validate-workflow-artifacts.js +27 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  24. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  25. package/build/src/cli/workflow-sidecar.js +938 -168
  26. package/build/src/cli.js +6 -0
  27. package/build/src/flow-kit/validate.d.ts +23 -0
  28. package/build/src/flow-kit/validate.js +67 -1
  29. package/build/src/index.d.ts +1 -1
  30. package/build/src/index.js +1 -1
  31. package/build/src/lib/flow-resolver.d.ts +6 -4
  32. package/build/src/lib/flow-resolver.js +125 -58
  33. package/build/src/lib/local-artifact-root.d.ts +14 -2
  34. package/build/src/lib/local-artifact-root.js +22 -5
  35. package/build/src/lib/workflow-learning-projection.js +2 -2
  36. package/build/src/runtime-adapters.d.ts +12 -0
  37. package/build/src/runtime-adapters.js +56 -16
  38. package/build/src/tools/build-universal-bundles.js +49 -16
  39. package/build/src/tools/generate-context-map.js +10 -8
  40. package/build/src/tools/validate-source-tree.js +24 -1
  41. package/console.telemetry.json +22 -0
  42. package/context/contracts/artifact-contract.md +40 -7
  43. package/context/contracts/assignment-provider-contract.md +239 -0
  44. package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
  45. package/context/contracts/decision-registry-contract.md +265 -0
  46. package/context/contracts/delivery-contract.md +3 -1
  47. package/context/contracts/execution-contract.md +25 -0
  48. package/context/contracts/governance-adapter-contract.md +2 -0
  49. package/context/contracts/knowledge-store-contract.md +197 -0
  50. package/context/contracts/planning-contract.md +4 -1
  51. package/context/contracts/review-contract.md +7 -3
  52. package/context/contracts/sandbox-policy.md +2 -0
  53. package/context/contracts/standing-directives.md +13 -0
  54. package/context/contracts/verification-contract.md +11 -1
  55. package/context/contracts/work-item-contract.md +3 -1
  56. package/context/deferred/parallelization.md +1 -1
  57. package/context/gate-awareness.md +4 -4
  58. package/context/scripts/git-status.sh +2 -2
  59. package/context/scripts/hooks/config-protection.js +502 -2
  60. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  61. package/context/scripts/hooks/run-hook.js +14 -1
  62. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  63. package/context/scripts/hooks/workflow-steering.js +212 -6
  64. package/context/settings/assignment-provider-settings.json +33 -0
  65. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  66. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  67. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  68. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  69. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  70. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
  71. package/docs/agent-system-guidebook.md +3 -3
  72. package/docs/agent-usage-feedback-loop.md +6 -6
  73. package/docs/context-map.md +10 -7
  74. package/docs/decisions/decision-records.md +57 -0
  75. package/docs/decisions/index.md +17 -0
  76. package/docs/decisions/knowledge-store-provider.md +51 -0
  77. package/docs/decisions/model-routing.md +63 -0
  78. package/docs/decisions/promotion-gate.md +52 -0
  79. package/docs/decisions/standing-directives.md +66 -0
  80. package/docs/developer-architecture.md +1 -1
  81. package/docs/fixture-ownership.md +4 -0
  82. package/docs/flow-kit-repository-contract.md +5 -5
  83. package/docs/getting-started.md +12 -2
  84. package/docs/integrations/harness-install.md +2 -1
  85. package/docs/kit-authoring-guide.md +64 -5
  86. package/docs/knowledge-kit.md +5 -1
  87. package/docs/learnings/2026-07-improvement-program.md +203 -0
  88. package/docs/migrations.md +6 -5
  89. package/docs/north-star.md +2 -2
  90. package/docs/operating-layers.md +4 -3
  91. package/docs/repository-structure.md +5 -5
  92. package/docs/skills-map.md +8 -8
  93. package/docs/spec/runtime-hook-surface.md +19 -15
  94. package/docs/spikes/graph-provider-2026-07.md +155 -0
  95. package/docs/standards-register.md +7 -7
  96. package/docs/trust-anchor-adoption.md +105 -4
  97. package/docs/work-item-adapters.md +1 -1
  98. package/docs/workflow-artifact-lifecycle.md +72 -9
  99. package/docs/workflow-eval-strategy.md +3 -3
  100. package/docs/workflow-shared-contracts.md +3 -2
  101. package/docs/workflow-usage-guide.md +260 -21
  102. package/evals/acceptance/DEMO-false-completion.md +20 -11
  103. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  104. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  105. package/evals/acceptance/test_kiro_harness.sh +1 -1
  106. package/evals/ci/antigaming-suite.sh +5 -0
  107. package/evals/ci/run-baseline.sh +71 -1
  108. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  109. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  110. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  111. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  112. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  113. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  114. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  115. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  116. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  117. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  118. package/evals/fixtures/hook-influence/cases.json +32 -0
  119. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  120. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  121. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  122. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  123. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  124. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  125. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  126. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  127. package/evals/integration/test_actor_identity.sh +254 -0
  128. package/evals/integration/test_assignment_provider_github.sh +318 -0
  129. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  130. package/evals/integration/test_builder_step_producers.sh +33 -23
  131. package/evals/integration/test_bundle_install.sh +22 -16
  132. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  133. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  134. package/evals/integration/test_checkpoint_signing.sh +43 -0
  135. package/evals/integration/test_claim_lookup.sh +4 -4
  136. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  137. package/evals/integration/test_command_log_concurrency.sh +8 -8
  138. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  139. package/evals/integration/test_command_log_integrity.sh +16 -16
  140. package/evals/integration/test_console_learning_projection.sh +1 -1
  141. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  142. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  143. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  144. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  145. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  146. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  147. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  148. package/evals/integration/test_flow_kit_repository.sh +2 -0
  149. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  150. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  151. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  152. package/evals/integration/test_gate_lockdown.sh +97 -73
  153. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  154. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  155. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  156. package/evals/integration/test_goal_fit_hook.sh +32 -32
  157. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  158. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  159. package/evals/integration/test_hook_influence_cases.sh +1 -0
  160. package/evals/integration/test_install_merge.sh +262 -1
  161. package/evals/integration/test_kit_identity_trust.sh +6 -6
  162. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  163. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  164. package/evals/integration/test_liveness_verdict.sh +394 -0
  165. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  166. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  167. package/evals/integration/test_promote_gate.sh +178 -0
  168. package/evals/integration/test_publish_delivery.sh +23 -6
  169. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  170. package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
  171. package/evals/integration/test_pull_work_provider.sh +2 -2
  172. package/evals/integration/test_reconcile_soundness.sh +37 -13
  173. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  174. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  175. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  176. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  177. package/evals/integration/test_trust_reconcile.sh +11 -10
  178. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  179. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  180. package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
  181. package/evals/integration/test_usage_feedback_global.sh +5 -5
  182. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  183. package/evals/integration/test_verify_cli.sh +2 -2
  184. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  185. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  186. package/evals/integration/test_workflow_artifacts.sh +33 -0
  187. package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
  188. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  189. package/evals/lib/codex-provider.sh +1 -1
  190. package/evals/lib/node.sh +19 -1
  191. package/evals/run.sh +32 -0
  192. package/evals/static/test_decisions.sh +150 -0
  193. package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
  194. package/evals/static/test_knowledge_providers.sh +23 -0
  195. package/evals/static/test_package.sh +19 -10
  196. package/evals/static/test_universal_bundles.sh +48 -3
  197. package/evals/static/test_workflow_skills.sh +41 -8
  198. package/kits/builder/flows/build.flow.json +3 -74
  199. package/kits/builder/flows/publish-learn.flow.json +90 -0
  200. package/kits/builder/kit.json +11 -0
  201. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  202. package/kits/builder/skills/deliver/SKILL.md +32 -11
  203. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  204. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  205. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  206. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  207. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  208. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  209. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  210. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  211. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  212. package/kits/builder/skills/pull-work/SKILL.md +148 -5
  213. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  214. package/kits/builder/skills/review-work/SKILL.md +24 -1
  215. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  216. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  217. package/kits/catalog.json +6 -0
  218. package/kits/knowledge/kit.json +35 -0
  219. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  220. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  221. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  222. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  223. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  224. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  225. package/kits/knowledge/providers/conformance/suite.test.js +125 -0
  226. package/kits/knowledge/providers/git-repo/index.js +236 -0
  227. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  228. package/kits/knowledge/providers/health/index.js +153 -0
  229. package/kits/knowledge/providers/index.js +24 -0
  230. package/kits/knowledge/providers/lib/model.js +91 -0
  231. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  232. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  233. package/kits/knowledge/providers/work-item/index.js +204 -0
  234. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  235. package/kits/veritas-governance/docs/README.md +75 -0
  236. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  237. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  238. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  239. package/kits/veritas-governance/kit.json +16 -0
  240. package/package.json +11 -5
  241. package/packaging/README.md +2 -2
  242. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  243. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  244. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  245. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  246. package/packaging/manifest.json +9 -10
  247. package/schemas/assignment-provider-settings.schema.json +125 -0
  248. package/schemas/decision-record.schema.json +109 -0
  249. package/schemas/knowledge/edge.schema.json +54 -0
  250. package/schemas/knowledge/health-report.schema.json +45 -0
  251. package/schemas/knowledge/node.schema.json +49 -0
  252. package/schemas/knowledge/proposal.schema.json +53 -0
  253. package/schemas/workflow-evidence.schema.json +1 -1
  254. package/schemas/workflow-state.schema.json +5 -0
  255. package/scripts/README.md +8 -3
  256. package/scripts/check-content-boundary.cjs +1 -1
  257. package/scripts/check-decisions.cjs +356 -0
  258. package/scripts/ci/derive-claim-status.mjs +74 -0
  259. package/scripts/ci/trust-reconcile.js +957 -103
  260. package/scripts/git-status.sh +2 -2
  261. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  262. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  263. package/scripts/hooks/config-protection.js +102 -30
  264. package/scripts/hooks/evidence-capture.js +3 -3
  265. package/scripts/hooks/lib/actor-identity.js +368 -0
  266. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  267. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  268. package/scripts/hooks/lib/liveness-policy.js +127 -0
  269. package/scripts/hooks/lib/liveness-read.js +105 -12
  270. package/scripts/hooks/lib/liveness-write.js +46 -0
  271. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  272. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  273. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  274. package/scripts/hooks/stop-goal-fit.js +157 -26
  275. package/scripts/hooks/workflow-steering.js +77 -3
  276. package/scripts/install-codex-home.sh +138 -38
  277. package/scripts/statusline/flow-agents-statusline.js +2 -2
  278. package/src/cli/assignment-provider.ts +845 -0
  279. package/src/cli/console-learning-projection.ts +3 -2
  280. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  281. package/src/cli/init.ts +101 -21
  282. package/src/cli/kit.ts +37 -6
  283. package/src/cli/public-api.test.mjs +58 -6
  284. package/src/cli/pull-work-provider.ts +1 -1
  285. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  286. package/src/cli/usage-feedback.ts +3 -3
  287. package/src/cli/validate-hook-influence.ts +1 -0
  288. package/src/cli/validate-workflow-artifacts.ts +27 -7
  289. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  290. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  291. package/src/cli/workflow-sidecar.ts +933 -168
  292. package/src/cli.ts +6 -0
  293. package/src/flow-kit/validate.ts +74 -1
  294. package/src/index.ts +6 -2
  295. package/src/lib/flow-resolver.ts +123 -55
  296. package/src/lib/local-artifact-root.ts +24 -5
  297. package/src/lib/workflow-learning-projection.ts +2 -2
  298. package/src/runtime-adapters.ts +61 -15
  299. package/src/tools/build-universal-bundles.ts +49 -16
  300. package/src/tools/generate-context-map.ts +10 -8
  301. package/src/tools/validate-source-tree.ts +24 -1
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import * as path from "node:path";
4
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
5
+ import { defaultArtifactRootForRead } from "../lib/local-artifact-root.js";
5
6
  import { buildWorkflowLearningProjection, readWorkflowLearningSources } from "../lib/workflow-learning-projection.js";
6
7
 
7
8
  type Summary = {
@@ -24,7 +25,7 @@ function printHelp(): void {
24
25
  console.log("Build an inert Console learning projection from local workflow learning sidecars.");
25
26
  console.log("");
26
27
  console.log("Options:");
27
- console.log(" --artifact-root <path> Workflow artifact root to scan (default: .flow-agents)");
28
+ console.log(" --artifact-root <path> Workflow artifact root to scan (default: .kontourai/flow-agents)");
28
29
  console.log(" --kontour-root <path> Local Kontour root to write under (default: .kontour)");
29
30
  console.log(" --scope <id> Projection scope id (default: current directory name)");
30
31
  console.log(" --scope-kind <kind> Projection scope kind (default: repo)");
@@ -105,7 +106,7 @@ export function main(argv = process.argv.slice(2)): number {
105
106
  }
106
107
 
107
108
  try {
108
- const artifactRoot = path.resolve(flagString(flags, "artifact-root", ".flow-agents") ?? ".flow-agents");
109
+ const artifactRoot = path.resolve(flagString(flags, "artifact-root") ?? defaultArtifactRootForRead());
109
110
  const kontourRoot = path.resolve(flagString(flags, "kontour-root", ".kontour") ?? ".kontour");
110
111
  const producer = requireSafeSegment(flagString(flags, "producer", "flow-agents-learning") ?? "flow-agents-learning", "--producer");
111
112
  const scope = {
@@ -0,0 +1,112 @@
1
+ import * as child_process from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseArgs, flagString, flagBool } from "../lib/args.js";
7
+ import { readJson } from "../lib/fs.js";
8
+
9
+ // Mirrors src/cli/effective-backlog-settings.ts's loadSettings/currentRepo/merge/findProject/
10
+ // effective structure and its ask_user/configured result envelope exactly, pointed at the
11
+ // AssignmentProvider settings schema/example instead of the backlog equivalents (#290 Wave 2
12
+ // Task B). Deliberately NOT extracted into a shared helper with effective-backlog-settings.ts —
13
+ // see the #290 plan artifact's Unresolved Questions #3 for the recorded rationale (duplicating
14
+ // this file's small merge/repo-detection logic is lower-risk than refactoring a file outside
15
+ // this issue's declared scope).
16
+
17
+ const PROJECT_SETTINGS_RELATIVE_PATH = path.join("context", "settings", "assignment-provider-settings.json");
18
+
19
+ function loadSettings(file: string): Record<string, unknown> | null {
20
+ if (!fs.existsSync(file)) return null;
21
+ const data = readJson(file) as Record<string, unknown>;
22
+ if (data.schema_version !== "1.0") throw new Error(`${file}: unsupported schema_version ${String(data.schema_version)}`);
23
+ return data;
24
+ }
25
+
26
+ function repoFromText(text: string): { owner: string; name: string } | null {
27
+ const match = text.trim().match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
28
+ return match ? { owner: match[1], name: match[2] } : null;
29
+ }
30
+
31
+ function currentRepo(repoPath: string): { owner: string; name: string } | null {
32
+ try {
33
+ const out = child_process.execFileSync("git", ["-C", repoPath, "remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
34
+ const repo = repoFromText(out);
35
+ if (repo) return repo;
36
+ } catch {}
37
+ const packagePath = path.join(repoPath, "package.json");
38
+ if (fs.existsSync(packagePath)) {
39
+ const data = readJson(packagePath) as Record<string, unknown>;
40
+ const repository = data.repository;
41
+ const url = typeof repository === "object" && repository !== null ? (repository as Record<string, unknown>).url : repository;
42
+ if (typeof url === "string") return repoFromText(url);
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function merge(base: unknown, override: unknown): Record<string, unknown> | null {
48
+ if (!base && !override) return null;
49
+ if (!base) return structuredClone(override) as Record<string, unknown>;
50
+ if (!override) return structuredClone(base) as Record<string, unknown>;
51
+ const out = structuredClone(base) as Record<string, unknown>;
52
+ for (const [key, value] of Object.entries(override as Record<string, unknown>)) {
53
+ out[key] = typeof value === "object" && value !== null && typeof out[key] === "object" && out[key] !== null && !Array.isArray(value)
54
+ ? merge(out[key], value)
55
+ : structuredClone(value);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function findProject(settings: Record<string, unknown> | null, repo: { owner: string; name: string }): Record<string, unknown> | null {
61
+ const projects = settings?.projects;
62
+ if (!Array.isArray(projects)) return null;
63
+ return (projects.find((project) => {
64
+ const projectRepo = (((project as Record<string, unknown>).project as Record<string, unknown> | undefined)?.repo ?? {}) as Record<string, unknown>;
65
+ return projectRepo.owner === repo.owner && projectRepo.name === repo.name;
66
+ }) as Record<string, unknown> | undefined) ?? null;
67
+ }
68
+
69
+ function defaultProjectSettingsPath(): string {
70
+ let cursor = path.dirname(fileURLToPath(import.meta.url));
71
+ while (true) {
72
+ const candidate = path.join(cursor, PROJECT_SETTINGS_RELATIVE_PATH);
73
+ if (fs.existsSync(candidate)) return candidate;
74
+ const parent = path.dirname(cursor);
75
+ if (parent === cursor) return path.resolve(PROJECT_SETTINGS_RELATIVE_PATH);
76
+ cursor = parent;
77
+ }
78
+ }
79
+
80
+ function effective(repoPath: string, projectSettings: string, globalSettings: string): [Record<string, unknown>, number] {
81
+ const repo = currentRepo(repoPath);
82
+ const projectDoc = loadSettings(projectSettings);
83
+ const globalDoc = loadSettings(globalSettings);
84
+ if (!repo) return [{ status: "ask_user", reason: "could_not_identify_current_repo", message: "Ask the user which assignment AssignmentProvider to use for this workspace.", resolution: { project_settings_path: projectSettings, global_settings_path: globalSettings } }, 2];
85
+ const effectiveSettings = merge(merge(merge(globalDoc?.defaults, findProject(globalDoc, repo)), projectDoc?.defaults), findProject(projectDoc, repo));
86
+ if (!effectiveSettings) return [{ status: "ask_user", reason: "no_assignment_provider_settings", message: "Ask the user which assignment AssignmentProvider to use before claiming work.", current_repo: repo, resolution: { project_settings_path: projectSettings, global_settings_path: globalSettings, checked: ["project", "global"] } }, 2];
87
+ return [{ status: "configured", current_repo: repo, source: findProject(projectDoc, repo) || projectDoc?.defaults ? "project" : "global", precedence: ["project.projects match", "project.defaults", "global.projects match", "global.defaults"], settings: effectiveSettings }, 0];
88
+ }
89
+
90
+ export function main(argv = process.argv.slice(2)): number {
91
+ const args = parseArgs(argv);
92
+ try {
93
+ const [result, code] = effective(
94
+ path.resolve(flagString(args.flags, "repo-path", ".") ?? "."),
95
+ path.resolve(flagString(args.flags, "project-settings", defaultProjectSettingsPath()) ?? ""),
96
+ path.resolve(flagString(args.flags, "global-settings", path.join(os.homedir(), ".config", "flow-agents", "assignment-provider-settings.json")) ?? ""),
97
+ );
98
+ if (flagBool(args.flags, "json")) console.log(JSON.stringify(result, null, 2));
99
+ else console.log(`status: ${String(result.status)}`);
100
+ return code;
101
+ } catch (error) {
102
+ console.error(`error: ${(error as Error).message}`);
103
+ return 1;
104
+ }
105
+ }
106
+
107
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
108
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
109
+ // entry-point guard fires correctly when the module is loaded directly as a script.
110
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
111
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
112
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
package/src/cli/init.ts CHANGED
@@ -10,6 +10,7 @@ import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
10
10
  import { activateCodexLocal } from "../runtime-adapters.js";
11
11
  import { main as buildBundles } from "../tools/build-universal-bundles.js";
12
12
  import { root } from "../tools/common.js";
13
+ import { defaultCodexHome, durableInstallRecordPath } from "../lib/local-artifact-root.js";
13
14
 
14
15
  type Runtime = "base" | "codex" | "claude-code" | "kiro" | "opencode" | "pi";
15
16
  type TelemetrySink = "local-files" | "local-kontour-console" | "kontour-hosted-console" | "user-hosted-console" | "kontour-cloud" | "hosted-kontour-console";
@@ -101,8 +102,8 @@ Options:
101
102
  Honors FLOW_AGENTS_USER_CLAUDE_SETTINGS for test isolation.
102
103
  opencode: merges opencode.json into ~/.config/opencode/opencode.json
103
104
  (honors XDG_CONFIG_HOME; test isolation via FLOW_AGENTS_USER_OPENCODE_CONFIG).
104
- codex: runs install-codex-home.sh into ~/.flow-agents/codex
105
- (the isolated Codex HOME; hooks merged, not overwritten).
105
+ codex: runs install-codex-home.sh into CODEX_HOME or ~/.codex
106
+ (hooks merged, not overwritten).
106
107
  pi: NOT_VERIFIED (no documented global dir); warns and falls back to workspace default.
107
108
  --telemetry-sink local-files|local-kontour-console|kontour-hosted-console|user-hosted-console
108
109
  --console-url URL
@@ -193,9 +194,8 @@ function globalDest(runtime: Runtime): string {
193
194
  return path.join(process.env["XDG_CONFIG_HOME"] ?? path.join(os.homedir(), ".config"), "opencode");
194
195
  }
195
196
  if (runtime === "codex") {
196
- // codex --global routes to the isolated Codex HOME at ~/.flow-agents/codex.
197
- // This is the same path used by install-codex-home.sh; --dest overrides it for sandbox testing.
198
- return path.join(os.homedir(), ".flow-agents", "codex");
197
+ // codex --global routes to the standard Codex home. --dest remains an explicit override.
198
+ return defaultCodexHome();
199
199
  }
200
200
  if (runtime === "pi") {
201
201
  // pi has no documented global config dir.
@@ -286,6 +286,73 @@ function ensureBundle(runtime: Runtime): string {
286
286
  return bundle;
287
287
  }
288
288
 
289
+ // The bundle's hook commands resolve the flow-agents scripts directory via
290
+ // ${CLAUDE_PROJECT_DIR:-$(pwd)}. That is correct for a project-scoped install
291
+ // (installBundle rsyncs scripts/ alongside .claude/, so CLAUDE_PROJECT_DIR ==
292
+ // the install destination). It is NOT correct for a --global install: there
293
+ // is no per-destination copy of scripts/, and CLAUDE_PROJECT_DIR varies with
294
+ // whichever project happens to be open, so the hook resolves to a path that
295
+ // exists in at most one project (and never for most sessions). Global
296
+ // installs need an absolute, session-independent path instead.
297
+ const GLOBAL_INSTALL_PROJECT_DIR_PREFIX = /root="\$\{CLAUDE_PROJECT_DIR:-\$\(pwd\)\}";\s*/g;
298
+ const GLOBAL_INSTALL_PROJECT_DIR_VAR = /"\$root\//g;
299
+
300
+ function rewriteCommandForGlobalInstall(command: string, sourceRoot: string): string {
301
+ return command
302
+ .replace(GLOBAL_INSTALL_PROJECT_DIR_PREFIX, "")
303
+ .replace(GLOBAL_INSTALL_PROJECT_DIR_VAR, `"${sourceRoot}/`);
304
+ }
305
+
306
+ /** Recursively rewrite every `command` string found under `value` in place. */
307
+ function rewriteCommandsForGlobalInstall(value: unknown, sourceRoot: string): void {
308
+ if (Array.isArray(value)) {
309
+ for (const item of value) rewriteCommandsForGlobalInstall(item, sourceRoot);
310
+ return;
311
+ }
312
+ if (!value || typeof value !== "object") return;
313
+ const obj = value as Record<string, unknown>;
314
+ for (const key of Object.keys(obj)) {
315
+ if (key === "command" && typeof obj[key] === "string") {
316
+ obj[key] = rewriteCommandForGlobalInstall(obj[key] as string, sourceRoot);
317
+ continue;
318
+ }
319
+ rewriteCommandsForGlobalInstall(obj[key], sourceRoot);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Additively copy every file under srcDir into destDir, creating directories
325
+ * as needed and overwriting files whose content changed. Never deletes files
326
+ * in destDir that srcDir does not own — destDir may contain unrelated content
327
+ * (other kits, other tools) that this sync must not touch.
328
+ */
329
+ function copyDirMerge(srcDir: string, destDir: string): { added: number; updated: number } {
330
+ let added = 0;
331
+ let updated = 0;
332
+ if (!fs.existsSync(srcDir)) return { added, updated };
333
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
334
+ const srcPath = path.join(srcDir, entry.name);
335
+ const destPath = path.join(destDir, entry.name);
336
+ if (entry.isDirectory()) {
337
+ const nested = copyDirMerge(srcPath, destPath);
338
+ added += nested.added;
339
+ updated += nested.updated;
340
+ continue;
341
+ }
342
+ if (!entry.isFile()) continue;
343
+ const content = fs.readFileSync(srcPath);
344
+ if (fs.existsSync(destPath)) {
345
+ if (Buffer.compare(fs.readFileSync(destPath), content) === 0) continue;
346
+ updated += 1;
347
+ } else {
348
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
349
+ added += 1;
350
+ }
351
+ fs.writeFileSync(destPath, content);
352
+ }
353
+ return { added, updated };
354
+ }
355
+
289
356
  function installBundle(bundle: string, options: InitOptions): number {
290
357
  const args = ["install.sh", options.dest];
291
358
  for (const sink of options.telemetrySinks) args.push("--telemetry-sink", sink);
@@ -345,10 +412,13 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
345
412
  // targeting the global settings, so the collision they'd create is expected.
346
413
  if (!options.global) checkScopeCollision();
347
414
  }
348
- // --global for claude-code: merge only into the global/user-level settings dir.
349
- // This writes only the hook-wiring config (merge into ~/.claude/settings.json),
350
- // not the full workspace bundle. The global settings dir is the claude config root,
351
- // so the settings.json lives directly in dest (not dest/.claude/).
415
+ // --global for claude-code: merge hook-wiring into the global/user-level
416
+ // settings dir, plus additively sync skills/agents so a global install
417
+ // does not silently go stale as new Builder Kit skills ship. This does
418
+ // NOT rsync the full workspace bundle (no context/, powers/, prompts/,
419
+ // etc.) — only the parts a global Claude Code user-config needs. The
420
+ // global settings dir is the claude config root, so the settings.json
421
+ // lives directly in dest (not dest/.claude/).
352
422
  if (options.global && options.runtime === "claude-code") {
353
423
  const bundle = ensureBundle(options.runtime);
354
424
  // For --global, dest is ~/.claude/ (the global settings dir).
@@ -364,6 +434,10 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
364
434
  // Remove permissive defaults (not appropriate for global user settings).
365
435
  delete managed["permissions"];
366
436
  delete managed["skipDangerousModePermissionPrompt"];
437
+ // See rewriteCommandsForGlobalInstall: bundle hook commands assume
438
+ // CLAUDE_PROJECT_DIR points at this package; a global install must not
439
+ // depend on which project is currently open, so pin to an absolute path.
440
+ rewriteCommandsForGlobalInstall(managed, root);
367
441
  fs.mkdirSync(options.dest, { recursive: true });
368
442
  const destSettingsPath = path.join(options.dest, "settings.json");
369
443
  const installMergePath = path.join(root, "scripts", "install-merge.js");
@@ -377,16 +451,21 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
377
451
  const tmp = `${destSettingsPath}.tmp.${process.pid}`;
378
452
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
379
453
  fs.renameSync(tmp, destSettingsPath);
454
+ // Additive skills/agents sync: adds new files and updates changed ones,
455
+ // never deletes — dest may hold unrelated content from other kits/tools.
456
+ const skillsSync = copyDirMerge(path.join(bundle, ".claude", "skills"), path.join(options.dest, "skills"));
457
+ const agentsSync = copyDirMerge(path.join(bundle, ".claude", "agents"), path.join(options.dest, "agents"));
380
458
  // Write version stamp.
381
- const installRecordDir = path.join(options.dest, ".flow-agents");
459
+ const recordPath = durableInstallRecordPath(options.dest);
460
+ const installRecordDir = path.dirname(recordPath);
382
461
  fs.mkdirSync(installRecordDir, { recursive: true });
383
462
  const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
384
463
  const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code", global: true };
385
- const recordPath = path.join(installRecordDir, "install.json");
386
464
  const recordTmp = `${recordPath}.tmp.${process.pid}`;
387
465
  fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
388
466
  fs.renameSync(recordTmp, recordPath);
389
467
  console.log(`Flow Agents global hooks merged for claude-code in ${options.dest}`);
468
+ console.log(`Synced skills (+${skillsSync.added} new, ~${skillsSync.updated} updated) and agents (+${agentsSync.added} new, ~${agentsSync.updated} updated) in ${options.dest}`);
390
469
  return 0;
391
470
  }
392
471
  // --global for opencode: merge FA opencode.json into the global opencode config dir.
@@ -416,11 +495,11 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
416
495
  `, "utf8");
417
496
  fs.renameSync(tmp, destConfigPath);
418
497
  // Write version stamp.
419
- const installRecordDir = path.join(options.dest, ".flow-agents");
498
+ const recordPath = durableInstallRecordPath(options.dest);
499
+ const installRecordDir = path.dirname(recordPath);
420
500
  fs.mkdirSync(installRecordDir, { recursive: true });
421
501
  const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
422
502
  const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "opencode", global: true };
423
- const recordPath = path.join(installRecordDir, "install.json");
424
503
  const recordTmp = `${recordPath}.tmp.${process.pid}`;
425
504
  fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}
426
505
  `, "utf8");
@@ -428,9 +507,8 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
428
507
  console.log(`Flow Agents global config merged for opencode in ${options.dest}`);
429
508
  return 0;
430
509
  }
431
- // --global for codex: run install-codex-home.sh to install into the isolated Codex HOME.
432
- // The codex --global path is ~/.flow-agents/codex (the isolated home IS codex's global).
433
- // Pass through telemetry/console args and honor --dest override for sandbox testing.
510
+ // --global for codex: run install-codex-home.sh to install into the Codex home.
511
+ // Defaults to CODEX_HOME or ~/.codex. --dest remains an explicit override.
434
512
  if (options.global && options.runtime === "codex") {
435
513
  const codexHomeScript = path.join(root, "scripts", "install-codex-home.sh");
436
514
  if (!fs.existsSync(codexHomeScript)) {
@@ -448,7 +526,9 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
448
526
  console.error(`flow-agents init: unable to run install-codex-home.sh: ${result.error.message}`);
449
527
  return 1;
450
528
  }
451
- return result.status ?? 1;
529
+ const installed = result.status ?? 1;
530
+ if (installed !== 0) return installed;
531
+ return activateKits(options);
452
532
  }
453
533
  // --global for pi: NOT_VERIFIED (no documented global dir). Warn and fall through to workspace install.
454
534
  if (options.global && options.runtime === "pi") {
@@ -534,11 +614,11 @@ function dogfoodClaudeCode(bundleRoot: string, dest: string): void {
534
614
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
535
615
  fs.renameSync(tmp, destSettingsPath);
536
616
  // Write version stamp.
537
- const installRecordDir = path.join(dest, ".flow-agents");
617
+ const recordPath = durableInstallRecordPath(dest);
618
+ const installRecordDir = path.dirname(recordPath);
538
619
  fs.mkdirSync(installRecordDir, { recursive: true });
539
620
  const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
540
621
  const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "claude-code" };
541
- const recordPath = path.join(installRecordDir, "install.json");
542
622
  const recordTmp = `${recordPath}.tmp.${process.pid}`;
543
623
  fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
544
624
  fs.renameSync(recordTmp, recordPath);
@@ -572,11 +652,11 @@ function dogfoodCodex(bundleRoot: string, dest: string): void {
572
652
  fs.writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
573
653
  fs.renameSync(tmp, destHooksPath);
574
654
  // Write version stamp.
575
- const installRecordDir = path.join(dest, ".flow-agents");
655
+ const recordPath = durableInstallRecordPath(dest);
656
+ const installRecordDir = path.dirname(recordPath);
576
657
  fs.mkdirSync(installRecordDir, { recursive: true });
577
658
  const pkgJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")) as Record<string, string>;
578
659
  const record = { version: pkgJson["version"] ?? "0.0.0", installedAt: new Date().toISOString(), runtime: "codex" };
579
- const recordPath = path.join(installRecordDir, "install.json");
580
660
  const recordTmp = `${recordPath}.tmp.${process.pid}`;
581
661
  fs.writeFileSync(recordTmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
582
662
  fs.renameSync(recordTmp, recordPath);
package/src/cli/kit.ts CHANGED
@@ -6,14 +6,19 @@ import * as path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseArgs, flagBool, flagString } from "../lib/args.js";
8
8
  import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
9
- import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
9
+ import { assertKitRepository, deriveKitTargets, parseKitDependencies } from "../flow-kit/validate.js";
10
10
  import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
11
+ import { defaultCodexHome } from "../lib/local-artifact-root.js";
11
12
 
12
13
  const REGISTRY_REL = path.join("kits", "local", "installed-kits.json");
13
14
  const REPOSITORIES_REL = path.join("kits", "local", "repositories");
14
15
 
15
16
  function registryPath(dest: string): string { return path.join(dest, REGISTRY_REL); }
16
17
  function installedPath(dest: string, kitId: string): string { return path.join(dest, REPOSITORIES_REL, kitId); }
18
+ function resolveDest(flags: ReturnType<typeof parseArgs>["flags"]): string {
19
+ const explicit = flagString(flags, "dest");
20
+ return path.resolve(explicit ?? defaultCodexHome());
21
+ }
17
22
 
18
23
  function loadRegistry(dest: string): { schema_version: string; kits: Record<string, unknown>[] } {
19
24
  const file = registryPath(dest);
@@ -22,6 +27,28 @@ function loadRegistry(dest: string): { schema_version: string; kits: Record<stri
22
27
  return { schema_version: data.schema_version ?? "1.0", kits: Array.isArray(data.kits) ? data.kits.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null) : [] };
23
28
  }
24
29
 
30
+ /**
31
+ * Emit a non-blocking warning for each declared kit dependency that is not present
32
+ * in the destination's LOCAL registry.
33
+ *
34
+ * Scope limitation (v1, accepted): this check only sees the local installed-kits
35
+ * registry at `dest`, NOT the built-in Kit Catalog. A dependency satisfied solely by
36
+ * a built-in catalog kit will still warn here. Presence is enforced authoritatively
37
+ * (hard error) at activation time against the full discovered inventory
38
+ * (built-in catalog + local registry) — see src/runtime-adapters.ts.
39
+ */
40
+ function warnUninstalledDependencies(manifest: Record<string, unknown>, manifestPath: string, dest: string): void {
41
+ const { entries } = parseKitDependencies(manifest, manifestPath);
42
+ if (!entries.length) return;
43
+ const installed = new Set(loadRegistry(dest).kits.map((entry) => String(entry.id ?? "")));
44
+ const kitId = String(manifest.id);
45
+ for (const dep of entries) {
46
+ if (!installed.has(dep.kit_id)) {
47
+ console.log(`warning: kit '${kitId}' declares a dependency on '${dep.kit_id}'${dep.reason ? ` (${dep.reason})` : ""} which is not installed at ${dest}; install it first with 'flow-agents kit install <source>' or activation will fail`);
48
+ }
49
+ }
50
+ }
51
+
25
52
  function contentHash(root: string): string {
26
53
  const hash = crypto.createHash("sha256");
27
54
  for (const file of walkFiles(root)) {
@@ -80,7 +107,7 @@ async function install(argv: string[]): Promise<number> {
80
107
 
81
108
  async function installLocalSource(source: string, argv: string[]): Promise<number> {
82
109
  const args = parseArgs(argv);
83
- const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
110
+ const dest = resolveDest(args.flags);
84
111
  let manifest: Record<string, unknown>;
85
112
  try {
86
113
  manifest = await assertKitRepository(source);
@@ -90,10 +117,12 @@ async function installLocalSource(source: string, argv: string[]): Promise<numbe
90
117
  return 1;
91
118
  }
92
119
  const kitId = String(manifest.id);
120
+ warnUninstalledDependencies(manifest, path.join(source, "kit.json"), dest);
93
121
  const hash = contentHash(source);
94
122
  const registry = loadRegistry(dest);
95
123
  const existing = registry.kits.find((entry) => entry.id === kitId);
96
124
  const target = installedPath(dest, kitId);
125
+ fs.mkdirSync(dest, { recursive: true });
97
126
  assertPathContained(dest, target);
98
127
  const sourceText = source;
99
128
  if (existing && existing.source !== sourceText && !flagBool(args.flags, "update")) {
@@ -133,7 +162,7 @@ async function installGitSource(rawUrl: string, argv: string[]): Promise<number>
133
162
  }
134
163
  if (!ref) ref = flagString(args.flags, "ref") ?? null;
135
164
 
136
- const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
165
+ const dest = resolveDest(args.flags);
137
166
  const force = flagBool(args.flags, "force") ?? false;
138
167
  const update = flagBool(args.flags, "update") ?? false;
139
168
 
@@ -167,10 +196,12 @@ async function installGitSource(rawUrl: string, argv: string[]): Promise<number>
167
196
 
168
197
  // Delegate to the shared install logic (copy + registry update).
169
198
  const kitId = String(manifest.id);
199
+ warnUninstalledDependencies(manifest, path.join(tmpBase, "kit.json"), dest);
170
200
  const hash = kitContentHash(tmpBase);
171
201
  const registry = loadRegistry(dest);
172
202
  const existing = registry.kits.find((entry) => entry.id === kitId);
173
203
  const target = installedPath(dest, kitId);
204
+ fs.mkdirSync(dest, { recursive: true });
174
205
  assertPathContained(dest, target);
175
206
  const sourceText = repoUrl + (ref ? `#${ref}` : "");
176
207
  if (existing && existing.source !== sourceText && !update) {
@@ -202,7 +233,7 @@ async function installGitSource(rawUrl: string, argv: string[]): Promise<number>
202
233
 
203
234
  function list(argv: string[]): number {
204
235
  const args = parseArgs(argv);
205
- const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
236
+ const dest = resolveDest(args.flags);
206
237
  const entries = loadRegistry(dest).kits;
207
238
  if (!entries.length) {
208
239
  console.log("No local Flow Kits installed.");
@@ -216,7 +247,7 @@ function list(argv: string[]): number {
216
247
 
217
248
  function status(argv: string[]): number {
218
249
  const args = parseArgs(argv);
219
- const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
250
+ const dest = resolveDest(args.flags);
220
251
  let entries = loadRegistry(dest).kits;
221
252
  const kitId = args.positionals[0];
222
253
  if (kitId) {
@@ -241,7 +272,7 @@ const AVAILABLE_ADAPTERS = ["codex-local", "strands-local"];
241
272
 
242
273
  function activate(argv: string[]): number {
243
274
  const args = parseArgs(argv);
244
- const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
275
+ const dest = resolveDest(args.flags);
245
276
  const sourceRoot = path.resolve(flagString(args.flags, "source-root", path.resolve(path.dirname(process.argv[1]), "..")) ?? ".");
246
277
  const adapter = flagString(args.flags, "adapter");
247
278
  if (adapter && !AVAILABLE_ADAPTERS.includes(adapter)) {
@@ -1,21 +1,73 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
3
5
  import path from "node:path";
6
+ import { createRequire } from "node:module";
4
7
 
5
8
  import {
6
9
  defaultArtifactRootForRead,
10
+ defaultCodexHome,
11
+ durableFlowAgentsRoot,
12
+ durableInstallRecordPath,
13
+ DURABLE_FLOW_AGENTS_DIR,
14
+ FLOW_AGENTS_RUNTIME_DIR,
15
+ FLOW_AGENTS_RUNTIME_SUBDIR,
7
16
  flowAgentsArtifactRoot,
8
17
  KONTOURAI_DIR,
9
- legacyFlowAgentsArtifactRoot,
10
- LEGACY_FLOW_AGENTS_DIR,
11
18
  } from "../../build/src/index.js";
12
19
 
13
20
  test("public API exports local artifact root helpers", () => {
14
21
  const cwd = path.resolve("/tmp/flow-agents-public-api");
15
22
 
16
23
  assert.equal(KONTOURAI_DIR, ".kontourai");
17
- assert.equal(LEGACY_FLOW_AGENTS_DIR, ".flow-agents");
18
- assert.equal(flowAgentsArtifactRoot(cwd), path.join(cwd, ".kontourai", "flow-agents"));
19
- assert.equal(legacyFlowAgentsArtifactRoot(cwd), path.join(cwd, ".flow-agents"));
20
- assert.equal(defaultArtifactRootForRead(cwd), path.join(cwd, ".kontourai", "flow-agents"));
24
+ assert.equal(FLOW_AGENTS_RUNTIME_SUBDIR, "flow-agents");
25
+ assert.equal(FLOW_AGENTS_RUNTIME_DIR, path.join(".kontourai", "flow-agents"));
26
+ assert.equal(DURABLE_FLOW_AGENTS_DIR, ".flow-agents");
27
+ assert.equal(flowAgentsArtifactRoot(cwd), path.join(cwd, FLOW_AGENTS_RUNTIME_DIR));
28
+ assert.equal(durableFlowAgentsRoot(cwd), path.join(cwd, ".flow-agents"));
29
+ assert.equal(durableInstallRecordPath(cwd), path.join(cwd, ".flow-agents", "install.json"));
30
+ assert.equal(defaultArtifactRootForRead(cwd), path.join(cwd, FLOW_AGENTS_RUNTIME_DIR));
31
+ assert.notEqual(defaultArtifactRootForRead(cwd), durableFlowAgentsRoot(cwd));
32
+ });
33
+
34
+ test("TS and CJS artifact helpers stay in parity without durable-root fallback", () => {
35
+ const require = createRequire(import.meta.url);
36
+ const cjs = require("../../scripts/hooks/lib/local-artifact-paths.js");
37
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "flow-agents-paths-"));
38
+
39
+ fs.mkdirSync(path.join(cwd, DURABLE_FLOW_AGENTS_DIR, "previous-session"), { recursive: true });
40
+ fs.writeFileSync(path.join(cwd, DURABLE_FLOW_AGENTS_DIR, "current.json"), "{}\n");
41
+
42
+ assert.equal(cjs.KONTOURAI_DIR, KONTOURAI_DIR);
43
+ assert.equal(cjs.FLOW_AGENTS_RUNTIME_SUBDIR, FLOW_AGENTS_RUNTIME_SUBDIR);
44
+ assert.equal(cjs.FLOW_AGENTS_RUNTIME_DIR, FLOW_AGENTS_RUNTIME_DIR);
45
+ assert.equal(cjs.DURABLE_FLOW_AGENTS_DIR, DURABLE_FLOW_AGENTS_DIR);
46
+
47
+ assert.equal(cjs.flowAgentsArtifactRoot(cwd), flowAgentsArtifactRoot(cwd));
48
+ assert.equal(cjs.defaultArtifactRootForRead(cwd), defaultArtifactRootForRead(cwd));
49
+ assert.equal(cjs.durableFlowAgentsRoot(cwd), durableFlowAgentsRoot(cwd));
50
+ assert.deepEqual(cjs.flowAgentsArtifactRootsForRead(cwd), []);
51
+ });
52
+
53
+ test("defaultCodexHome honors CODEX_HOME env override", () => {
54
+ const previous = process.env["CODEX_HOME"];
55
+ try {
56
+ process.env["CODEX_HOME"] = "/custom/codex-home";
57
+ assert.equal(defaultCodexHome(), "/custom/codex-home");
58
+ } finally {
59
+ if (previous === undefined) delete process.env["CODEX_HOME"];
60
+ else process.env["CODEX_HOME"] = previous;
61
+ }
62
+ });
63
+
64
+ test("defaultCodexHome falls back to ~/.codex when CODEX_HOME is unset", () => {
65
+ const previous = process.env["CODEX_HOME"];
66
+ try {
67
+ delete process.env["CODEX_HOME"];
68
+ assert.equal(defaultCodexHome(), path.join(os.homedir(), ".codex"));
69
+ } finally {
70
+ if (previous === undefined) delete process.env["CODEX_HOME"];
71
+ else process.env["CODEX_HOME"] = previous;
72
+ }
21
73
  });
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { parseArgs, flagList, flagString } from "../lib/args.js";
4
4
 
5
- const FLOW_ARTIFACT_PATTERN = /(?<path>\.flow-agents\/[^\s`'")]+)/g;
5
+ const FLOW_ARTIFACT_PATTERN = /(?<path>\.kontourai\/flow-agents\/[^\s`'")]+)/g;
6
6
  const GITHUB_ISSUE_PATTERN = /(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+))?#(?<number>\d+)/g;
7
7
  const GITHUB_URL_PATTERN = /https:\/\/github\.com\/(?<owner>[^/\s]+)\/(?<repo>[^/\s]+)\/(?:issues|pull)\/(?<number>\d+)/g;
8
8
  const BLOCKER_PATTERN = /\b(?:blocked by|depends on|waiting on|requires|should follow|coordinate with)\b[:\s-]*(?<refs>[^\n]+)/gi;