@kontourai/flow-agents 2.2.0 → 2.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 (250) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/AGENTS.md +4 -4
  4. package/CHANGELOG.md +46 -0
  5. package/CONTEXT.md +14 -0
  6. package/README.md +9 -3
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-code-reviewer.json +1 -1
  9. package/agents/tool-planner.json +3 -3
  10. package/agents/tool-verifier.json +3 -3
  11. package/build/src/cli/console-learning-projection.js +3 -2
  12. package/build/src/cli/init.js +104 -21
  13. package/build/src/cli/kit.js +37 -6
  14. package/build/src/cli/pull-work-provider.js +1 -1
  15. package/build/src/cli/usage-feedback.js +3 -3
  16. package/build/src/cli/validate-hook-influence.js +1 -0
  17. package/build/src/cli/validate-workflow-artifacts.js +22 -6
  18. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  19. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  20. package/build/src/cli/workflow-sidecar.js +792 -69
  21. package/build/src/flow-kit/validate.d.ts +23 -0
  22. package/build/src/flow-kit/validate.js +67 -1
  23. package/build/src/index.d.ts +1 -1
  24. package/build/src/index.js +1 -1
  25. package/build/src/lib/flow-resolver.d.ts +6 -4
  26. package/build/src/lib/flow-resolver.js +125 -58
  27. package/build/src/lib/local-artifact-root.d.ts +14 -2
  28. package/build/src/lib/local-artifact-root.js +22 -5
  29. package/build/src/lib/workflow-learning-projection.js +2 -2
  30. package/build/src/runtime-adapters.d.ts +12 -0
  31. package/build/src/runtime-adapters.js +56 -16
  32. package/build/src/tools/build-universal-bundles.js +49 -16
  33. package/build/src/tools/generate-context-map.js +10 -8
  34. package/build/src/tools/validate-source-tree.js +23 -1
  35. package/console.telemetry.json +22 -0
  36. package/context/contracts/artifact-contract.md +38 -7
  37. package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
  38. package/context/contracts/decision-registry-contract.md +263 -0
  39. package/context/contracts/delivery-contract.md +1 -1
  40. package/context/contracts/planning-contract.md +2 -1
  41. package/context/contracts/review-contract.md +5 -3
  42. package/context/contracts/verification-contract.md +9 -1
  43. package/context/contracts/work-item-contract.md +1 -1
  44. package/context/deferred/parallelization.md +1 -1
  45. package/context/gate-awareness.md +4 -4
  46. package/context/scripts/git-status.sh +2 -2
  47. package/context/scripts/hooks/config-protection.js +502 -2
  48. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  49. package/context/scripts/hooks/run-hook.js +14 -1
  50. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  51. package/context/scripts/hooks/workflow-steering.js +212 -6
  52. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  54. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  55. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  56. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  57. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
  58. package/docs/agent-system-guidebook.md +3 -3
  59. package/docs/agent-usage-feedback-loop.md +6 -6
  60. package/docs/context-map.md +9 -7
  61. package/docs/decisions/decision-records.md +57 -0
  62. package/docs/decisions/index.md +14 -0
  63. package/docs/decisions/promotion-gate.md +52 -0
  64. package/docs/developer-architecture.md +1 -1
  65. package/docs/fixture-ownership.md +3 -0
  66. package/docs/flow-kit-repository-contract.md +5 -5
  67. package/docs/getting-started.md +12 -2
  68. package/docs/integrations/harness-install.md +2 -1
  69. package/docs/kit-authoring-guide.md +64 -5
  70. package/docs/knowledge-kit.md +5 -1
  71. package/docs/learnings/2026-07-improvement-program.md +203 -0
  72. package/docs/migrations.md +6 -5
  73. package/docs/north-star.md +2 -2
  74. package/docs/operating-layers.md +4 -3
  75. package/docs/repository-structure.md +5 -5
  76. package/docs/skills-map.md +8 -8
  77. package/docs/spec/runtime-hook-surface.md +19 -15
  78. package/docs/spikes/graph-provider-2026-07.md +155 -0
  79. package/docs/standards-register.md +7 -7
  80. package/docs/trust-anchor-adoption.md +105 -4
  81. package/docs/work-item-adapters.md +1 -1
  82. package/docs/workflow-artifact-lifecycle.md +72 -9
  83. package/docs/workflow-eval-strategy.md +3 -3
  84. package/docs/workflow-shared-contracts.md +1 -1
  85. package/docs/workflow-usage-guide.md +254 -21
  86. package/evals/acceptance/DEMO-false-completion.md +20 -11
  87. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  88. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  89. package/evals/acceptance/test_kiro_harness.sh +1 -1
  90. package/evals/ci/antigaming-suite.sh +5 -0
  91. package/evals/ci/run-baseline.sh +65 -1
  92. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  93. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  94. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  95. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  96. package/evals/fixtures/hook-influence/cases.json +32 -0
  97. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  98. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  99. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  100. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  101. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  102. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  103. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  104. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  105. package/evals/integration/test_actor_identity.sh +254 -0
  106. package/evals/integration/test_builder_step_producers.sh +33 -23
  107. package/evals/integration/test_bundle_install.sh +22 -16
  108. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  109. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  110. package/evals/integration/test_checkpoint_signing.sh +43 -0
  111. package/evals/integration/test_claim_lookup.sh +4 -4
  112. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  113. package/evals/integration/test_command_log_concurrency.sh +8 -8
  114. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  115. package/evals/integration/test_command_log_integrity.sh +16 -16
  116. package/evals/integration/test_console_learning_projection.sh +1 -1
  117. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  118. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  119. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  120. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  121. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  122. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  123. package/evals/integration/test_flow_kit_repository.sh +2 -0
  124. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  125. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  126. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  127. package/evals/integration/test_gate_lockdown.sh +97 -73
  128. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  129. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  130. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  131. package/evals/integration/test_goal_fit_hook.sh +32 -32
  132. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  133. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  134. package/evals/integration/test_hook_influence_cases.sh +1 -0
  135. package/evals/integration/test_install_merge.sh +262 -1
  136. package/evals/integration/test_kit_identity_trust.sh +6 -6
  137. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  138. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  139. package/evals/integration/test_liveness_verdict.sh +394 -0
  140. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  141. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  142. package/evals/integration/test_promote_gate.sh +178 -0
  143. package/evals/integration/test_publish_delivery.sh +2 -2
  144. package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
  145. package/evals/integration/test_pull_work_provider.sh +2 -2
  146. package/evals/integration/test_reconcile_soundness.sh +4 -4
  147. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  148. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  149. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  150. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  151. package/evals/integration/test_trust_reconcile.sh +2 -2
  152. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  153. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  154. package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
  155. package/evals/integration/test_usage_feedback_global.sh +5 -5
  156. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  157. package/evals/integration/test_verify_cli.sh +2 -2
  158. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  159. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  160. package/evals/integration/test_workflow_artifacts.sh +33 -0
  161. package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
  162. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  163. package/evals/lib/codex-provider.sh +1 -1
  164. package/evals/lib/node.sh +19 -1
  165. package/evals/run.sh +22 -0
  166. package/evals/static/test_decisions.sh +150 -0
  167. package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
  168. package/evals/static/test_package.sh +19 -10
  169. package/evals/static/test_universal_bundles.sh +48 -3
  170. package/evals/static/test_workflow_skills.sh +41 -8
  171. package/kits/builder/flows/build.flow.json +3 -74
  172. package/kits/builder/flows/publish-learn.flow.json +90 -0
  173. package/kits/builder/kit.json +11 -0
  174. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  175. package/kits/builder/skills/deliver/SKILL.md +13 -11
  176. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  177. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  178. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  179. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  180. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  181. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  182. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  183. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  184. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  185. package/kits/builder/skills/pull-work/SKILL.md +80 -5
  186. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  187. package/kits/builder/skills/review-work/SKILL.md +24 -1
  188. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  189. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  190. package/kits/catalog.json +6 -0
  191. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  192. package/kits/veritas-governance/docs/README.md +75 -0
  193. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  194. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  195. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  196. package/kits/veritas-governance/kit.json +16 -0
  197. package/package.json +8 -4
  198. package/packaging/README.md +2 -2
  199. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  200. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  201. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  202. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  203. package/packaging/manifest.json +9 -10
  204. package/schemas/decision-record.schema.json +109 -0
  205. package/schemas/workflow-evidence.schema.json +1 -1
  206. package/schemas/workflow-state.schema.json +5 -0
  207. package/scripts/README.md +8 -3
  208. package/scripts/check-content-boundary.cjs +1 -1
  209. package/scripts/check-decisions.cjs +356 -0
  210. package/scripts/ci/derive-claim-status.mjs +74 -0
  211. package/scripts/ci/trust-reconcile.js +436 -79
  212. package/scripts/git-status.sh +2 -2
  213. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  214. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  215. package/scripts/hooks/config-protection.js +102 -30
  216. package/scripts/hooks/evidence-capture.js +3 -3
  217. package/scripts/hooks/lib/actor-identity.js +368 -0
  218. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  219. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  220. package/scripts/hooks/lib/liveness-policy.js +127 -0
  221. package/scripts/hooks/lib/liveness-read.js +105 -12
  222. package/scripts/hooks/lib/liveness-write.js +46 -0
  223. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  224. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  225. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  226. package/scripts/hooks/stop-goal-fit.js +157 -26
  227. package/scripts/hooks/workflow-steering.js +77 -3
  228. package/scripts/install-codex-home.sh +138 -38
  229. package/scripts/statusline/flow-agents-statusline.js +2 -2
  230. package/src/cli/console-learning-projection.ts +3 -2
  231. package/src/cli/init.ts +101 -21
  232. package/src/cli/kit.ts +37 -6
  233. package/src/cli/public-api.test.mjs +58 -6
  234. package/src/cli/pull-work-provider.ts +1 -1
  235. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  236. package/src/cli/usage-feedback.ts +3 -3
  237. package/src/cli/validate-hook-influence.ts +1 -0
  238. package/src/cli/validate-workflow-artifacts.ts +22 -6
  239. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  240. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  241. package/src/cli/workflow-sidecar.ts +795 -71
  242. package/src/flow-kit/validate.ts +74 -1
  243. package/src/index.ts +6 -2
  244. package/src/lib/flow-resolver.ts +123 -55
  245. package/src/lib/local-artifact-root.ts +24 -5
  246. package/src/lib/workflow-learning-projection.ts +2 -2
  247. package/src/runtime-adapters.ts +61 -15
  248. package/src/tools/build-universal-bundles.ts +49 -16
  249. package/src/tools/generate-context-map.ts +10 -8
  250. package/src/tools/validate-source-tree.ts +23 -1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # install-codex-home.sh - Install Flow Agents as an isolated Codex home.
2
+ # install-codex-home.sh - Install Flow Agents into a Codex home.
3
3
  set -euo pipefail
4
4
 
5
5
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -19,7 +19,7 @@ Options:
19
19
  EOF
20
20
  }
21
21
 
22
- DEST="$HOME/.flow-agents/codex"
22
+ DEST="${CODEX_HOME:-$HOME/.codex}"
23
23
  DEST_SET=0
24
24
  CONSOLE_CONFIG_ARGS=()
25
25
  while [[ $# -gt 0 ]]; do
@@ -59,47 +59,144 @@ else
59
59
  fi
60
60
 
61
61
  mkdir -p "$DEST"
62
+ DEST_REAL="$(cd "$DEST" && pwd -P)"
62
63
 
63
- # Stash the user's existing hooks.json (if any) BEFORE cleaning, so the merge
64
- # step below can preserve user hooks across re-installs.
64
+ assert_safe_dest_path() {
65
+ local rel="$1"
66
+ local current="$DEST"
67
+ local part
68
+ IFS='/' read -r -a parts <<< "$rel"
69
+ for part in "${parts[@]}"; do
70
+ [[ -n "$part" && "$part" != "." ]] || continue
71
+ [[ "$part" != ".." ]] || { echo "install-codex-home.sh: unsafe destination path: $rel" >&2; exit 1; }
72
+ current="$current/$part"
73
+ if [[ -L "$current" ]]; then
74
+ echo "install-codex-home.sh: refusing to write through symlink: $current" >&2
75
+ exit 1
76
+ fi
77
+ done
78
+ local parent
79
+ parent="$(dirname "$current")"
80
+ mkdir -p "$parent"
81
+ local parent_real
82
+ parent_real="$(cd "$parent" && pwd -P)"
83
+ case "$parent_real/" in
84
+ "$DEST_REAL"/*) ;;
85
+ *) echo "install-codex-home.sh: destination escapes Codex home: $current" >&2; exit 1 ;;
86
+ esac
87
+ }
88
+
89
+ # Stash the user's existing hooks.json (if any), so the merge step below can
90
+ # preserve user hooks across installs while replacing Flow Agents-managed groups.
65
91
  FA_USER_HOOKS_STASH=""
92
+ assert_safe_dest_path "hooks.json"
66
93
  if [[ -f "$DEST/hooks.json" ]]; then
67
94
  FA_USER_HOOKS_STASH="$(mktemp /tmp/fa-user-hooks.XXXXXX.json)"
68
95
  cp "$DEST/hooks.json" "$FA_USER_HOOKS_STASH"
69
96
  fi
70
97
 
71
- # This is an isolated generated Codex home. Clean generated bundle content before
72
- # overlaying so renamed/deleted source files do not survive across installs.
73
- rm -rf \
74
- "$DEST/.flow-agents" \
75
- "$DEST/.codex" \
76
- "$DEST/AGENTS.md" \
77
- "$DEST/README.md" \
78
- "$DEST/console.telemetry.json" \
79
- "$DEST/install.sh" \
80
- "$DEST/config.toml" \
81
- "$DEST/hooks.json" \
82
- "$DEST/agent-cards" \
83
- "$DEST/agents" \
84
- "$DEST/context" \
85
- "$DEST/docs" \
86
- "$DEST/evals" \
87
- "$DEST/integrations" \
88
- "$DEST/kits" \
89
- "$DEST/packaging" \
90
- "$DEST/powers" \
91
- "$DEST/prompts" \
92
- "$DEST/schemas" \
93
- "$DEST/scripts" \
94
- "$DEST/skills"
95
- find "$DEST" -maxdepth 1 -type f -name 'k*.config.toml' -delete
96
-
97
- rsync -a "$ROOT_DIR/dist/codex/." "$DEST/"
98
- rsync -a "$ROOT_DIR/dist/codex/.codex/." "$DEST/"
99
- rm -rf "$DEST/.codex" 2>/dev/null || true
98
+ # A real Codex home can contain user-owned config, profiles, auth, hooks, and
99
+ # locally installed kits. Update Flow Agents-owned bundle trees in place without
100
+ # deleting that state. `kits/local` is user/runtime state; everything else under
101
+ # the generated bundle paths is managed by this installer.
102
+ for managed_dir in \
103
+ .flow-agents \
104
+ agent-cards \
105
+ build \
106
+ context \
107
+ docs \
108
+ evals \
109
+ integrations \
110
+ packaging \
111
+ powers \
112
+ prompts \
113
+ schemas \
114
+ scripts
115
+ do
116
+ if [[ -d "$ROOT_DIR/dist/codex/$managed_dir" ]]; then
117
+ assert_safe_dest_path "$managed_dir"
118
+ mkdir -p "$DEST/$managed_dir"
119
+ rsync -a --delete "$ROOT_DIR/dist/codex/$managed_dir/" "$DEST/$managed_dir/"
120
+ fi
121
+ done
122
+
123
+ # Skills are user-extensible in a real Codex home. Merge both bundle skill
124
+ # layers into the flattened destination without deleting user-owned skills.
125
+ if [[ -d "$ROOT_DIR/dist/codex/skills" ]]; then
126
+ assert_safe_dest_path "skills"
127
+ mkdir -p "$DEST/skills"
128
+ rsync -a "$ROOT_DIR/dist/codex/skills/" "$DEST/skills/"
129
+ fi
130
+
131
+ if [[ -d "$ROOT_DIR/dist/codex/.codex/skills" ]]; then
132
+ assert_safe_dest_path "skills"
133
+ mkdir -p "$DEST/skills"
134
+ rsync -a "$ROOT_DIR/dist/codex/.codex/skills/" "$DEST/skills/"
135
+ fi
136
+
137
+ if [[ -d "$ROOT_DIR/dist/codex/kits" ]]; then
138
+ assert_safe_dest_path "kits"
139
+ mkdir -p "$DEST/kits"
140
+ rsync -a --delete --exclude 'local/' "$ROOT_DIR/dist/codex/kits/" "$DEST/kits/"
141
+ fi
142
+
143
+ # Agents are user-extensible in a real Codex home. Merge generated agents
144
+ # without deleting user-owned agents.
145
+ if [[ -d "$ROOT_DIR/dist/codex/.codex/agents" ]]; then
146
+ assert_safe_dest_path "agents"
147
+ mkdir -p "$DEST/agents"
148
+ rsync -a "$ROOT_DIR/dist/codex/.codex/agents/" "$DEST/agents/"
149
+ fi
150
+
151
+ for bundle_file in README.md console.telemetry.json install.sh; do
152
+ if [[ -f "$ROOT_DIR/dist/codex/$bundle_file" ]]; then
153
+ assert_safe_dest_path "$bundle_file"
154
+ cp "$ROOT_DIR/dist/codex/$bundle_file" "$DEST/$bundle_file"
155
+ fi
156
+ done
157
+
158
+ # Root Codex config/profiles and AGENTS.md may be user-owned in ~/.codex.
159
+ # Seed Flow Agents defaults only when absent.
160
+ for seed_file in AGENTS.md; do
161
+ if [[ -f "$ROOT_DIR/dist/codex/$seed_file" && ! -e "$DEST/$seed_file" ]]; then
162
+ assert_safe_dest_path "$seed_file"
163
+ cp "$ROOT_DIR/dist/codex/$seed_file" "$DEST/$seed_file"
164
+ fi
165
+ done
166
+ generated_profile_files=()
167
+ profile_names=()
168
+ for seed_source in "$ROOT_DIR/dist/codex/.codex/config.toml" "$ROOT_DIR"/dist/codex/.codex/*.config.toml; do
169
+ [[ -f "$seed_source" ]] || continue
170
+ seed_file="$(basename "$seed_source")"
171
+ if [[ "$seed_file" == *.config.toml ]]; then
172
+ generated_profile_files+=("$seed_file")
173
+ profile_names+=("${seed_file%.config.toml}")
174
+ fi
175
+ if [[ ! -e "$DEST/$seed_file" ]] || grep -q 'Generated from packaging/manifest.json' "$DEST/$seed_file"; then
176
+ assert_safe_dest_path "$seed_file"
177
+ cp "$seed_source" "$DEST/$seed_file"
178
+ fi
179
+ done
180
+ for profile_path in "$DEST"/*.config.toml; do
181
+ [[ -f "$profile_path" ]] || continue
182
+ profile_file="$(basename "$profile_path")"
183
+ keep_generated=0
184
+ for generated_file in "${generated_profile_files[@]}"; do
185
+ if [[ "$profile_file" == "$generated_file" ]]; then
186
+ keep_generated=1
187
+ break
188
+ fi
189
+ done
190
+ if [[ "$keep_generated" -eq 0 ]] && grep -q 'Generated from packaging/manifest.json' "$profile_path"; then
191
+ assert_safe_dest_path "$profile_file"
192
+ rm -f "$profile_path"
193
+ fi
194
+ done
195
+ rmdir "$DEST/.codex" 2>/dev/null || true
100
196
 
101
197
  for auth_file in auth.json version.json installation_id models_cache.json; do
102
- if [[ -f "$REAL_CODEX_HOME/$auth_file" ]]; then
198
+ if [[ "$REAL_CODEX_HOME" != "$DEST" && -f "$REAL_CODEX_HOME/$auth_file" ]]; then
199
+ assert_safe_dest_path "$auth_file"
103
200
  cp "$REAL_CODEX_HOME/$auth_file" "$DEST/$auth_file"
104
201
  fi
105
202
  done
@@ -122,7 +219,7 @@ if command -v node >/dev/null 2>&1 && [[ -f "$FA_MANAGED_HOOKS" ]]; then
122
219
  --managed-hooks "$FA_MANAGED_HOOKS" \
123
220
  --version "$FA_VERSION" \
124
221
  --install-record "$DEST/.flow-agents/install.json" \
125
- --runtime "codex" || true
222
+ --runtime "codex"
126
223
  # Move the merged result into the destination.
127
224
  cp "$FA_USER_HOOKS_STASH" "$DEST/hooks.json"
128
225
  rm -f "$FA_USER_HOOKS_STASH"
@@ -133,7 +230,7 @@ if command -v node >/dev/null 2>&1 && [[ -f "$FA_MANAGED_HOOKS" ]]; then
133
230
  --managed-hooks "$FA_MANAGED_HOOKS" \
134
231
  --version "$FA_VERSION" \
135
232
  --install-record "$DEST/.flow-agents/install.json" \
136
- --runtime "codex" || true
233
+ --runtime "codex"
137
234
  fi
138
235
  fi
139
236
 
@@ -141,5 +238,8 @@ if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" |
141
238
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
142
239
  fi
143
240
 
144
- echo "Installed isolated Flow Agents Codex home at $DEST"
145
- echo "Use: CODEX_HOME=$DEST codex --profile kdev"
241
+ echo "Installed Flow Agents into Codex home at $DEST"
242
+ if [[ "${#profile_names[@]}" -gt 0 ]]; then
243
+ echo "Profiles: ${profile_names[*]}"
244
+ fi
245
+ echo "Use: codex --profile builder"
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Compact Flow Agents workflow status for runtimes with status bar support.
4
4
  *
5
- * Reads optional runtime JSON from stdin, discovers `.flow-agents`, and
5
+ * Reads optional runtime JSON from stdin, discovers `.kontourai/flow-agents`, and
6
6
  * prints one ASCII line suitable for Claude Code statusLine or Pi setStatus.
7
7
  */
8
8
 
@@ -56,7 +56,7 @@ function runtimeCwd(input) {
56
56
  function findWorkflowRoot(startDir) {
57
57
  let current = path.resolve(startDir || process.cwd());
58
58
  for (;;) {
59
- const candidate = path.join(current, '.flow-agents');
59
+ const candidate = path.join(current, '.kontourai', 'flow-agents');
60
60
  if (fs.existsSync(candidate)) return candidate;
61
61
  const parent = path.dirname(current);
62
62
  if (parent === current) return null;
@@ -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 = {
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)) {