@kontourai/flow-agents 2.3.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 +39 -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
@@ -10,6 +10,72 @@ const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"
10
10
  const CORE_CONTAINER_FIELDS = new Set(["schema_version", "id", "name", "description", "product_name", "flows"]);
11
11
  const AGENT_EXTENSION_CLASSES = new Set(["skills", "docs", "adapters", "evals", "assets"]);
12
12
 
13
+ // Flow Agents-recognized metadata fields that are neither core container fields nor
14
+ // agent-extension asset classes. Recognized here so they are never misreported as
15
+ // unknown third-party extension namespaces. `dependencies` declares cross-kit skill
16
+ // dependencies (extension-layer ownership; see docs/adr/0019-kit-dependency-ownership.md).
17
+ const KNOWN_METADATA_FIELDS = new Set(["dependencies"]);
18
+
19
+ export interface KitDependencyEntry {
20
+ kit_id: string;
21
+ reason?: string;
22
+ }
23
+
24
+ /**
25
+ * Parse and shape-validate a kit manifest's `dependencies` field (Flow Agents
26
+ * extension-layer metadata; see docs/adr/0019-kit-dependency-ownership.md).
27
+ *
28
+ * Shape rules:
29
+ * - `dependencies` (if present) must be an array.
30
+ * - each entry must be an object with a `kit_id` string matching ^[a-z][a-z0-9-]*$.
31
+ * - `kit_id` must not equal the declaring kit's own id (no self-reference).
32
+ * - no duplicate `kit_id` across entries.
33
+ * - `reason` is an optional string.
34
+ *
35
+ * Returns the parsed entries plus any shape errors (empty errors = valid). This is
36
+ * a sibling shape check, not a member of the file-path-oriented EXTENSION_ASSET_CLASSES
37
+ * loop, because dependency entries have no `path`/file to check.
38
+ */
39
+ export function parseKitDependencies(manifest: Record<string, unknown>, manifestPath: string): { entries: KitDependencyEntry[]; errors: string[] } {
40
+ const entries: KitDependencyEntry[] = [];
41
+ const errors: string[] = [];
42
+ const raw = manifest.dependencies;
43
+ if (raw === undefined) return { entries, errors };
44
+ if (!Array.isArray(raw)) {
45
+ errors.push(`${manifestPath}: .dependencies must be a list`);
46
+ return { entries, errors };
47
+ }
48
+ const ownId = typeof manifest.id === "string" ? manifest.id : "";
49
+ const seen = new Set<string>();
50
+ raw.forEach((entry, index) => {
51
+ if (typeof entry !== "object" || entry === null) {
52
+ errors.push(`${manifestPath}: dependencies[${index}] must be an object`);
53
+ return;
54
+ }
55
+ const record = entry as Record<string, unknown>;
56
+ const kitId = record.kit_id;
57
+ if (typeof kitId !== "string" || !/^[a-z][a-z0-9-]*$/.test(kitId)) {
58
+ errors.push(`${manifestPath}: dependencies[${index}].kit_id must be a kebab-case kit id (^[a-z][a-z0-9-]*$)`);
59
+ return;
60
+ }
61
+ if (ownId && kitId === ownId) {
62
+ errors.push(`${manifestPath}: dependencies[${index}].kit_id must not reference the declaring kit itself ('${kitId}')`);
63
+ return;
64
+ }
65
+ if (seen.has(kitId)) {
66
+ errors.push(`${manifestPath}: dependencies[${index}].kit_id duplicates '${kitId}'`);
67
+ return;
68
+ }
69
+ seen.add(kitId);
70
+ if (record.reason !== undefined && typeof record.reason !== "string") {
71
+ errors.push(`${manifestPath}: dependencies[${index}].reason must be a string when present`);
72
+ }
73
+ const reason = typeof record.reason === "string" ? record.reason : undefined;
74
+ entries.push({ kit_id: kitId, ...(reason ? { reason } : {}) });
75
+ });
76
+ return { entries, errors };
77
+ }
78
+
13
79
  export type KitTargetConsumer =
14
80
  | "flow"
15
81
  | "flow-agents"
@@ -167,7 +233,7 @@ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir
167
233
  // Detect third-party extension namespaces: top-level keys that are neither
168
234
  // core fields nor Flow Agents extension classes.
169
235
  const thirdPartyExtensions: string[] = Object.keys(manifest)
170
- .filter((key) => !CORE_CONTAINER_FIELDS.has(key) && !AGENT_EXTENSION_CLASSES.has(key))
236
+ .filter((key) => !CORE_CONTAINER_FIELDS.has(key) && !AGENT_EXTENSION_CLASSES.has(key) && !KNOWN_METADATA_FIELDS.has(key))
171
237
  .sort();
172
238
 
173
239
  const targets: KitTargetConsumer[] = [];
@@ -247,6 +313,13 @@ export async function validateKitRepository(kitDir: string): Promise<string[]> {
247
313
  }
248
314
  });
249
315
  }
316
+
317
+ // Flow Agents metadata: cross-kit dependency declarations (extension-layer;
318
+ // see docs/adr/0019-kit-dependency-ownership.md). Shape-only here — presence is
319
+ // checked separately (non-blocking at install, hard error at activation).
320
+ const depResult = parseKitDependencies(manifest, manifestPath);
321
+ for (const err of depResult.errors) errors.push(err);
322
+
250
323
  return errors;
251
324
  }
252
325
 
package/src/index.ts CHANGED
@@ -18,13 +18,17 @@ import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-s
18
18
 
19
19
  export {
20
20
  defaultArtifactRootForRead,
21
+ defaultCodexHome,
21
22
  defaultTelemetryDirForRead,
22
23
  defaultTelemetryDirsForRead,
24
+ durableFlowAgentsRoot,
25
+ durableInstallRecordPath,
26
+ DURABLE_FLOW_AGENTS_DIR,
27
+ FLOW_AGENTS_RUNTIME_DIR,
28
+ FLOW_AGENTS_RUNTIME_SUBDIR,
23
29
  firstExistingPath,
24
30
  flowAgentsArtifactRoot,
25
31
  KONTOURAI_DIR,
26
- legacyFlowAgentsArtifactRoot,
27
- LEGACY_FLOW_AGENTS_DIR,
28
32
  legacyTelemetryDataDir,
29
33
  LEGACY_TELEMETRY_DIR,
30
34
  telemetryDataDir,
@@ -20,9 +20,9 @@ import * as path from "node:path";
20
20
  //
21
21
  // Both kitId and flowName originate from agent-writable sources (active_flow_id
22
22
  // in current.json, and FLOW_AGENTS_FLOW_DEFS_DIR set by the runtime). A crafted
23
- // value like "builder.../../../.flow-agents/slug/fake-flow" produces:
23
+ // value like "builder.../../../.kontourai/flow-agents/slug/fake-flow" produces:
24
24
  // kitId = "builder"
25
- // flowName = "../../../.flow-agents/slug/fake-flow"
25
+ // flowName = "../../../.kontourai/flow-agents/slug/fake-flow"
26
26
  // which resolves OUTSIDE kits/ via path.join traversal.
27
27
  //
28
28
  // SLUG_RE closes this: it rejects any value containing path separators, dots,
@@ -36,12 +36,23 @@ import * as path from "node:path";
36
36
  const SLUG_RE = /^[a-zA-Z0-9_-]+$/;
37
37
 
38
38
  /**
39
- * Returns true when the given resolved absolute path falls within a .flow-agents
40
- * directory (an agent-writable area). Used to reject FLOW_AGENTS_FLOW_DEFS_DIR
39
+ * Returns true when the given resolved absolute path falls within a Flow Agents
40
+ * runtime artifact directory (an agent-writable area). Used to reject FLOW_AGENTS_FLOW_DEFS_DIR
41
41
  * overrides that point into agent-controlled storage.
42
42
  */
43
+ function hasAgentWritableRuntimeSegment(resolvedDir: string): boolean {
44
+ const parts = resolvedDir.split(path.sep);
45
+ if (parts.includes(".flow-agents")) return true;
46
+ return parts.some((part, index) => part === ".kontourai" && parts[index + 1] === "flow-agents");
47
+ }
48
+
43
49
  function isAgentWritableDir(resolvedDir: string): boolean {
44
- return resolvedDir.split(path.sep).includes(".flow-agents");
50
+ if (hasAgentWritableRuntimeSegment(resolvedDir)) return true;
51
+ try {
52
+ return hasAgentWritableRuntimeSegment(fs.realpathSync.native(resolvedDir));
53
+ } catch {
54
+ return false;
55
+ }
45
56
  }
46
57
 
47
58
  /**
@@ -49,11 +60,11 @@ function isAgentWritableDir(resolvedDir: string): boolean {
49
60
  *
50
61
  * Returns the validated absolute file path, or null when:
51
62
  * - kitId or flowName contains chars outside SLUG_RE (rejects traversal)
52
- * - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a .flow-agents directory
63
+ * - FLOW_AGENTS_FLOW_DEFS_DIR resolves into a runtime artifact directory
53
64
  * - The resolved path escapes the expected root (belt-and-suspenders)
54
65
  *
55
- * When the override is unsafe (points into .flow-agents), falls back silently
56
- * to the repoRoot/kits/ path so the resolver still works for legit flows.
66
+ * When the override is unsafe, it is ignored and the resolver uses the
67
+ * canonical repoRoot/kits/ source for legitimate flows.
57
68
  */
58
69
  export function resolveFlowFilePath(
59
70
  kitId: string,
@@ -72,8 +83,8 @@ export function resolveFlowFilePath(
72
83
  if (override) {
73
84
  const resolvedOverride = path.resolve(override);
74
85
  if (isAgentWritableDir(resolvedOverride)) {
75
- // Override targets an agent-writable .flow-agents path reject it and
76
- // fall back to the kit root. The session will resolve the real kit flow.
86
+ // Override targets an agent-writable runtime path; ignore it and use
87
+ // the canonical kit root. The session will resolve the real kit flow.
77
88
  expectedRoot = path.resolve(repoRoot, "kits");
78
89
  flowFilePath = path.join(repoRoot, "kits", kitId, "flows", `${flowName}.flow.json`);
79
90
  } else {
@@ -95,7 +106,19 @@ export function resolveFlowFilePath(
95
106
  return null; // traversal still detected — paranoid fallback
96
107
  }
97
108
 
98
- return resolvedPath;
109
+ // If the file exists, resolve final symlinks before returning a readable path.
110
+ // `readFileSync` follows symlinks; this keeps lexical containment from turning
111
+ // into an out-of-root file read through a symlinked FlowDefinition.
112
+ try {
113
+ const realExpectedRoot = fs.existsSync(expectedRoot) ? fs.realpathSync.native(expectedRoot) : expectedRoot;
114
+ const realPath = fs.realpathSync.native(resolvedPath);
115
+ if (!realPath.startsWith(realExpectedRoot + path.sep) && realPath !== realExpectedRoot) {
116
+ return null;
117
+ }
118
+ return realPath;
119
+ } catch {
120
+ return resolvedPath;
121
+ }
99
122
  }
100
123
 
101
124
  // ─── Public types ─────────────────────────────────────────────────────────────
@@ -118,6 +141,8 @@ export type ActiveFlowStep = {
118
141
  stepId: string;
119
142
  gateId: string;
120
143
  gateExpects: GateExpectation[];
144
+ /** When resolved through a parent step's uses_flow edge, names the child FlowDefinition that owns the gate. */
145
+ sourceFlowId?: string;
121
146
  };
122
147
 
123
148
  /** Shape of a gate entry in the FlowDefinition JSON. */
@@ -138,10 +163,38 @@ type FlowDefinition = {
138
163
  * (e.g. "execution") to step ids (e.g. "execute") so advance-state can write active_step_id
139
164
  * without hardcoding any vocabulary in the core. */
140
165
  phase_map?: Record<string, string>;
141
- steps?: Array<{ id: string; next: string | null }>;
166
+ steps?: Array<{ id: string; next: string | null; uses_flow?: string }>;
142
167
  gates?: Record<string, FlowGate>;
168
+ /** Optional claim types or expectation ids this flow intentionally exposes to parent/composed flows. */
169
+ exports?: string[];
143
170
  };
144
171
 
172
+ type InternalActiveFlowStep = ActiveFlowStep & { flowExports?: string[] };
173
+
174
+ function flowIdParts(flowId: string): { kitId: string; flowName: string } | null {
175
+ if (!flowId) return null;
176
+ const dotIdx = flowId.indexOf(".");
177
+ if (dotIdx < 1) return null;
178
+ const kitId = flowId.slice(0, dotIdx);
179
+ const flowName = flowId.slice(dotIdx + 1);
180
+ if (!kitId || !flowName) return null;
181
+ return { kitId, flowName };
182
+ }
183
+
184
+ function readFlowDefinition(flowId: string, repoRoot: string): FlowDefinition | null {
185
+ const parts = flowIdParts(flowId);
186
+ if (!parts) return null;
187
+ const flowFilePath = resolveFlowFilePath(parts.kitId, parts.flowName, flowId, repoRoot);
188
+ if (!flowFilePath) return null;
189
+
190
+ try {
191
+ const raw = fs.readFileSync(flowFilePath, "utf8");
192
+ return JSON.parse(raw) as FlowDefinition;
193
+ } catch {
194
+ return null; // ENOENT, permission error, or parse error → fail-open
195
+ }
196
+ }
197
+
145
198
  /**
146
199
  * Resolve the gate expects[] for a specific (flowId, stepId) pair.
147
200
  *
@@ -152,38 +205,69 @@ type FlowDefinition = {
152
205
  * @returns ActiveFlowStep with the matched gate's expects[], or null on any error.
153
206
  */
154
207
  export function resolveFlowStep(flowId: string, stepId: string, repoRoot: string): ActiveFlowStep | null {
208
+ const resolved = resolveFlowStepInternal(flowId, stepId, repoRoot, new Set<string>());
209
+ if (!resolved) return null;
210
+ const { flowExports: _flowExports, ...publicStep } = resolved;
211
+ return publicStep;
212
+ }
213
+
214
+ function expectationExportKeys(expectation: GateExpectation): string[] {
215
+ const keys: string[] = [];
216
+ if (typeof expectation.id === "string" && expectation.id) keys.push(expectation.id);
217
+ const claimType = expectation.bundle_claim?.claimType;
218
+ if (typeof claimType === "string" && claimType) keys.push(claimType);
219
+ return keys;
220
+ }
221
+
222
+ function exportedExpectations(expectations: GateExpectation[], exportsList: unknown): GateExpectation[] | null {
223
+ if (!Array.isArray(exportsList)) return null;
224
+ const exported = new Set(exportsList.filter((item): item is string => typeof item === "string" && item.length > 0));
225
+ const allowed = expectations.filter((expectation) => expectationExportKeys(expectation).some((key) => exported.has(key)));
226
+ return allowed.length === expectations.length ? allowed : null;
227
+ }
228
+
229
+ function resolveFlowStepInternal(flowId: string, stepId: string, repoRoot: string, seen: Set<string>): InternalActiveFlowStep | null {
155
230
  if (!flowId || !stepId) return null;
156
- const dotIdx = flowId.indexOf(".");
157
- if (dotIdx < 1) return null; // flowId must have at least one "." to derive kitId
158
- const kitId = flowId.slice(0, dotIdx);
159
- // The flow filename is the part after the first "." (e.g. "build" from "builder.build")
160
- const flowName = flowId.slice(dotIdx + 1);
161
- if (!kitId || !flowName) return null;
231
+ if (!flowIdParts(flowId)) return null;
162
232
 
163
233
  // Layer 1 defense: validate stepId too — it is matched against gate.step values but
164
234
  // still originates from agent-writable current.json active_step_id.
165
235
  if (!SLUG_RE.test(stepId)) return null;
236
+ const seenKey = `${flowId}:${stepId}`;
237
+ if (seen.has(seenKey)) return null;
238
+ seen.add(seenKey);
166
239
 
167
- // Determine the FlowDefinition file path with slug validation + path containment check.
168
- // Returns null for traversal attempts (e.g. flowName = "../../../.flow-agents/fake").
169
- const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
170
- if (!flowFilePath) return null;
240
+ const flowDef = readFlowDefinition(flowId, repoRoot);
241
+ if (!flowDef) return null;
171
242
 
172
- let flowDef: FlowDefinition;
173
- try {
174
- const raw = fs.readFileSync(flowFilePath, "utf8");
175
- flowDef = JSON.parse(raw) as FlowDefinition;
176
- } catch {
177
- return null; // ENOENT, permission error, or parse error → fail-open
178
- }
179
-
180
- if (!flowDef || typeof flowDef !== "object" || !flowDef.gates) return null;
243
+ if (!flowDef || typeof flowDef !== "object") return null;
181
244
 
182
245
  // Find the gate whose .step matches stepId.
183
- for (const [gateId, gate] of Object.entries(flowDef.gates)) {
184
- if (!gate || gate.step !== stepId) continue;
185
- const expects = Array.isArray(gate.expects) ? gate.expects : [];
186
- return { flowId, stepId, gateId, gateExpects: expects };
246
+ if (flowDef.gates) {
247
+ for (const [gateId, gate] of Object.entries(flowDef.gates)) {
248
+ if (!gate || gate.step !== stepId) continue;
249
+ const expects = Array.isArray(gate.expects) ? gate.expects : [];
250
+ return { flowId, stepId, gateId, gateExpects: expects, flowExports: flowDef.exports };
251
+ }
252
+ }
253
+
254
+ const composedStep = Array.isArray(flowDef.steps)
255
+ ? flowDef.steps.find((step) => step && step.id === stepId && typeof step.uses_flow === "string" && step.uses_flow.trim())
256
+ : null;
257
+ if (composedStep?.uses_flow) {
258
+ const child = resolveFlowStepInternal(composedStep.uses_flow, stepId, repoRoot, seen);
259
+ if (child) {
260
+ const childGateExpects = exportedExpectations(child.gateExpects, child.flowExports);
261
+ if (!childGateExpects) return null;
262
+ return {
263
+ flowId,
264
+ stepId,
265
+ gateId: `${child.flowId}:${child.gateId}`,
266
+ gateExpects: childGateExpects,
267
+ sourceFlowId: child.flowId,
268
+ flowExports: flowDef.exports,
269
+ };
270
+ }
187
271
  }
188
272
 
189
273
  return null; // no gate matched the given stepId
@@ -205,24 +289,8 @@ export function resolveFlowStep(flowId: string, stepId: string, repoRoot: string
205
289
  * @returns Record<string,string> phase→stepId map, or null on absence/error.
206
290
  */
207
291
  export function resolvePhaseMap(flowId: string, repoRoot: string): Record<string, string> | null {
208
- if (!flowId) return null;
209
- const dotIdx = flowId.indexOf(".");
210
- if (dotIdx < 1) return null;
211
- const kitId = flowId.slice(0, dotIdx);
212
- const flowName = flowId.slice(dotIdx + 1);
213
- if (!kitId || !flowName) return null;
214
-
215
- // Layer 1 defense: same slug validation + path containment as resolveFlowStep.
216
- const flowFilePath = resolveFlowFilePath(kitId, flowName, flowId, repoRoot);
217
- if (!flowFilePath) return null;
218
-
219
- let flowDef: FlowDefinition;
220
- try {
221
- const raw = fs.readFileSync(flowFilePath, "utf8");
222
- flowDef = JSON.parse(raw) as FlowDefinition;
223
- } catch {
224
- return null; // ENOENT, permission error, or parse error → fail-open
225
- }
292
+ const flowDef = readFlowDefinition(flowId, repoRoot);
293
+ if (!flowDef) return null;
226
294
 
227
295
  if (!flowDef || typeof flowDef !== "object") return null;
228
296
  const pm = flowDef.phase_map;
@@ -239,7 +307,7 @@ export function resolvePhaseMap(flowId: string, repoRoot: string): Record<string
239
307
  * the nearest ancestor that contains a `kits/` subdirectory. If none is found,
240
308
  * falls back to `process.cwd()` so the default "run from repo root" case still works.
241
309
  *
242
- * This is required because the .flow-agents directory can live anywhere (temp dirs,
310
+ * This is required because the runtime artifact directory can live anywhere (temp dirs,
243
311
  * subprojects, CI workspaces) while the kits/ directory is always at the repo root.
244
312
  */
245
313
  function findRepoRoot(startDir: string): string {
@@ -264,7 +332,7 @@ function findRepoRoot(startDir: string): string {
264
332
  * with a fallback to process.cwd(). This handles temp dirs, CI workspaces, and
265
333
  * subproject layouts without hardcoding the repo structure.
266
334
  *
267
- * @param flowAgentsDir Path to the .flow-agents directory (contains current.json).
335
+ * @param flowAgentsDir Path to the runtime artifact root directory (contains current.json).
268
336
  * @returns ActiveFlowStep or null when fields are absent or resolution fails.
269
337
  */
270
338
  export function resolveActiveFlowStep(flowAgentsDir: string): ActiveFlowStep | null {
@@ -1,16 +1,35 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
 
4
5
  export const KONTOURAI_DIR = ".kontourai";
5
- export const LEGACY_FLOW_AGENTS_DIR = ".flow-agents";
6
+ export const FLOW_AGENTS_RUNTIME_SUBDIR = "flow-agents";
7
+ export const FLOW_AGENTS_RUNTIME_DIR = `${KONTOURAI_DIR}/${FLOW_AGENTS_RUNTIME_SUBDIR}`;
8
+ export const DURABLE_FLOW_AGENTS_DIR = ".flow-agents";
6
9
  export const LEGACY_TELEMETRY_DIR = ".telemetry";
7
10
 
11
+ /**
12
+ * Default Codex home: `$CODEX_HOME` when set, else `~/.codex`.
13
+ *
14
+ * This is the Codex CLI's own config/state root and is conceptually
15
+ * distinct from the Flow Agents global bundle install root (`~/.flow-agents`)
16
+ * and the durable per-destination install record root
17
+ * (`DURABLE_FLOW_AGENTS_DIR`, i.e. `.flow-agents`).
18
+ */
19
+ export function defaultCodexHome(): string {
20
+ return process.env["CODEX_HOME"] || path.join(os.homedir(), ".codex");
21
+ }
22
+
8
23
  export function flowAgentsArtifactRoot(cwd = process.cwd()): string {
9
- return path.resolve(cwd, KONTOURAI_DIR, "flow-agents");
24
+ return path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
25
+ }
26
+
27
+ export function durableFlowAgentsRoot(cwd = process.cwd()): string {
28
+ return path.resolve(cwd, DURABLE_FLOW_AGENTS_DIR);
10
29
  }
11
30
 
12
- export function legacyFlowAgentsArtifactRoot(cwd = process.cwd()): string {
13
- return path.resolve(cwd, LEGACY_FLOW_AGENTS_DIR);
31
+ export function durableInstallRecordPath(cwd = process.cwd()): string {
32
+ return path.join(durableFlowAgentsRoot(cwd), "install.json");
14
33
  }
15
34
 
16
35
  export function telemetryDataDir(cwd = process.cwd()): string {
@@ -26,7 +45,7 @@ export function firstExistingPath(candidates: string[]): string {
26
45
  }
27
46
 
28
47
  export function defaultArtifactRootForRead(cwd = process.cwd()): string {
29
- return firstExistingPath([flowAgentsArtifactRoot(cwd), legacyFlowAgentsArtifactRoot(cwd)]);
48
+ return flowAgentsArtifactRoot(cwd);
30
49
  }
31
50
 
32
51
  export function defaultTelemetryDirForRead(cwd = process.cwd()): string {
@@ -176,7 +176,7 @@ const KNOWN_CORRECTION_TYPES = new Set<WorkflowLearningCorrectionType>([
176
176
  "provider",
177
177
  "none",
178
178
  ]);
179
- const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history"]);
179
+ const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history", "liveness"]);
180
180
  const MAX_SIDECAR_BYTES = 1024 * 1024;
181
181
 
182
182
  export function readWorkflowLearningSources(artifactRoot: string): WorkflowLearningSource[] {
@@ -229,7 +229,7 @@ export function buildWorkflowLearningProjection(
229
229
  sourceRef: {
230
230
  product: "flow-agents",
231
231
  kind: "workflow-learning",
232
- id: ".flow-agents/*/learning.json",
232
+ id: ".kontourai/flow-agents/*/learning.json",
233
233
  label: "Local workflow learning sidecars",
234
234
  },
235
235
  },
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readJson, relPath, writeJson } from "./lib/fs.js";
4
+ import { flowAgentsArtifactRoot } from "./lib/local-artifact-root.js";
5
+ import { parseKitDependencies } from "./flow-kit/validate.js";
4
6
 
5
7
  export type KitAsset = {
6
8
  kit_id: string;
@@ -15,6 +17,12 @@ export type KitAsset = {
15
17
 
16
18
  export type KitInventory = { assets: KitAsset[]; warnings: string[]; errors: string[] };
17
19
 
20
+ /** A cross-kit dependency edge discovered while loading a kit manifest. */
21
+ export type KitDependencyRef = { from_kit_id: string; kit_id: string; reason?: string };
22
+
23
+ /** The parsed result of a single kit manifest: its id, activatable assets, and declared dependencies. */
24
+ export type LoadedKit = { kit_id: string; assets: KitAsset[]; dependencies: KitDependencyRef[] };
25
+
18
26
  const ASSET_CLASSES = ["flows", "skills", "docs", "adapters", "evals", "assets"];
19
27
 
20
28
  function assetPath(root: string, value: string): string | null {
@@ -24,24 +32,24 @@ function assetPath(root: string, value: string): string | null {
24
32
  return resolved === absRoot || resolved.startsWith(`${absRoot}${path.sep}`) ? resolved : null;
25
33
  }
26
34
 
27
- function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[], errors: string[]): KitAsset[] {
35
+ function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[], errors: string[]): LoadedKit {
28
36
  const manifestPath = path.join(kitRoot, "kit.json");
29
37
  if (!fs.existsSync(manifestPath)) {
30
38
  errors.push(`${kitRoot}: missing kit.json`);
31
- return [];
39
+ return { kit_id: "", assets: [], dependencies: [] };
32
40
  }
33
41
  let manifest: Record<string, unknown>;
34
42
  try {
35
43
  manifest = readJson(manifestPath) as Record<string, unknown>;
36
44
  } catch (error) {
37
45
  errors.push(`${manifestPath}: ${(error as Error).message}`);
38
- return [];
46
+ return { kit_id: "", assets: [], dependencies: [] };
39
47
  }
40
48
  const kitId = typeof manifest.id === "string" ? manifest.id : "";
41
49
  const kitName = typeof manifest.name === "string" ? manifest.name : kitId;
42
50
  if (!kitId) {
43
51
  errors.push(`${manifestPath}: missing kit id`);
44
- return [];
52
+ return { kit_id: "", assets: [], dependencies: [] };
45
53
  }
46
54
  const assets: KitAsset[] = [];
47
55
  for (const assetClass of ASSET_CLASSES) {
@@ -78,13 +86,21 @@ function loadKitAssets(kitRoot: string, sourceKind: string, warnings: string[],
78
86
  assets.push({ kit_id: kitId, kit_name: kitName, asset_class: assetClass, asset_id: assetId, relative_path: rel, source_path: source, source_kind: sourceKind, description });
79
87
  }
80
88
  }
81
- return assets;
89
+ // Parse cross-kit dependency declarations (extension-layer metadata; see
90
+ // docs/adr/0019-kit-dependency-ownership.md). Shape errors surface at
91
+ // install/inspect/validate time via validateKitRepository; here we only collect
92
+ // the edges so activation can enforce presence against the full inventory.
93
+ const dependencies: KitDependencyRef[] = parseKitDependencies(manifest, manifestPath).entries
94
+ .map((dep) => ({ from_kit_id: kitId, kit_id: dep.kit_id, ...(dep.reason ? { reason: dep.reason } : {}) }));
95
+ return { kit_id: kitId, assets, dependencies };
82
96
  }
83
97
 
84
98
  export function readKitInventory(sourceRoot: string, dest: string): KitInventory {
85
99
  const warnings: string[] = [];
86
100
  const errors: string[] = [];
87
101
  const assets: KitAsset[] = [];
102
+ const dependencies: KitDependencyRef[] = [];
103
+ const presentKitIds = new Set<string>();
88
104
  const catalogPath = path.join(sourceRoot, "kits", "catalog.json");
89
105
  if (!fs.existsSync(catalogPath)) warnings.push(`${catalogPath}: built-in Kit Catalog not found; skipping built-in kits (this is normal when running outside a flow-agents checkout)`);
90
106
  else {
@@ -99,7 +115,12 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
99
115
  }
100
116
  const kitRoot = assetPath(sourceRoot, rel);
101
117
  if (!kitRoot || !fs.existsSync(kitRoot)) warnings.push(`${catalogPath}: kits[${index}].path unavailable: ${rel}`);
102
- else assets.push(...loadKitAssets(kitRoot, "builtin", warnings, errors));
118
+ else {
119
+ const loaded = loadKitAssets(kitRoot, "builtin", warnings, errors);
120
+ assets.push(...loaded.assets);
121
+ dependencies.push(...loaded.dependencies);
122
+ if (loaded.kit_id) presentKitIds.add(loaded.kit_id);
123
+ }
103
124
  });
104
125
  }
105
126
  }
@@ -115,10 +136,23 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
115
136
  if (typeof id !== "string") return;
116
137
  const kitRoot = path.join(dest, "kits", "local", "repositories", id);
117
138
  if (!fs.existsSync(kitRoot)) warnings.push(`${id}: installed kit copy missing at ${kitRoot}; skipping`);
118
- else assets.push(...loadKitAssets(kitRoot, "local", warnings, errors));
139
+ else {
140
+ const loaded = loadKitAssets(kitRoot, "local", warnings, errors);
141
+ assets.push(...loaded.assets);
142
+ dependencies.push(...loaded.dependencies);
143
+ if (loaded.kit_id) presentKitIds.add(loaded.kit_id);
144
+ }
119
145
  });
120
146
  }
121
147
  }
148
+ // Activation-time cross-kit dependency enforcement: any declared dependency whose
149
+ // kit_id is not present among the union of built-in catalog kits and locally-installed
150
+ // kits is a hard error (feeds the errors[] -> activation-manifest -> non-zero exit path).
151
+ for (const dep of dependencies) {
152
+ if (!presentKitIds.has(dep.kit_id)) {
153
+ errors.push(`${dep.from_kit_id}: declares a dependency on kit '${dep.kit_id}'${dep.reason ? ` (${dep.reason})` : ""} which is not installed or activated. Install it with 'flow-agents kit install <source>' and re-activate, or remove the dependency declaration if it is no longer needed.`);
154
+ }
155
+ }
122
156
  return { assets, warnings, errors };
123
157
  }
124
158
 
@@ -134,7 +168,7 @@ const ACTIVATED_ASSET_CLASSES = new Set(["flows", "skills", "docs"]);
134
168
 
135
169
  export function activateCodexLocal(sourceRoot: string, dest: string): Record<string, unknown> {
136
170
  const inventory = readKitInventory(sourceRoot, dest);
137
- const runtimeDir = path.join(dest, ".flow-agents", "runtime", "codex");
171
+ const runtimeDir = path.join(flowAgentsArtifactRoot(dest), "projections", "codex");
138
172
  const generated: Record<string, string>[] = [];
139
173
  const skipped: Record<string, string | null>[] = [];
140
174
  for (const asset of inventory.assets) {
@@ -152,7 +186,13 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
152
186
  // agent's guidance index (AGENTS.md) can reference them and they are co-located
153
187
  // with flow definitions for the same kit.
154
188
  const filename = path.basename(asset.source_path);
155
- const output = path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
189
+ // Namespace skills by BOTH kit id AND the skill's own directory name: every skill
190
+ // file is literally named SKILL.md, so keying only on kit id collides all of a
191
+ // kit's skills onto one .../<kit-id>/SKILL.md file. Docs keep the flat
192
+ // <kit-id>/<filename> layout (no per-directory collision found).
193
+ const output = asset.asset_class === "skills"
194
+ ? path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), safeSegment(path.basename(path.dirname(asset.source_path))), filename)
195
+ : path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
156
196
  fs.mkdirSync(path.dirname(output), { recursive: true });
157
197
  fs.copyFileSync(asset.source_path, output);
158
198
  generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
@@ -177,11 +217,11 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
177
217
  // layout knowledge. The Strands steering layer then reads the written runtime files.
178
218
  export function activateStrandsLocal(sourceRoot: string, dest: string): Record<string, unknown> {
179
219
  const inventory = readKitInventory(sourceRoot, dest);
180
- // Runtime flows land at .flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json
220
+ // Runtime flows land at .kontourai/flow-agents/projections/strands/flows/<kit-id>/<asset-id>.flow.json
181
221
  // so the Strands steering context can glob for *.flow.json under this path.
182
- // Runtime skills land at .flow-agents/runtime/strands/skills/<kit-id>/<filename> and
183
- // docs at .flow-agents/runtime/strands/docs/<kit-id>/<filename> for system-prompt injection.
184
- const runtimeDir = path.join(dest, ".flow-agents", "runtime", "strands");
222
+ // Runtime skills land at .kontourai/flow-agents/projections/strands/skills/<kit-id>/<filename> and
223
+ // docs at .kontourai/flow-agents/projections/strands/docs/<kit-id>/<filename> for system-prompt injection.
224
+ const runtimeDir = path.join(flowAgentsArtifactRoot(dest), "projections", "strands");
185
225
  const generated: Record<string, string>[] = [];
186
226
  const skipped: Record<string, string | null>[] = [];
187
227
  for (const asset of inventory.assets) {
@@ -197,9 +237,15 @@ export function activateStrandsLocal(sourceRoot: string, dest: string): Record<s
197
237
  } else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
198
238
  // Mirror the codex-local layout: strands/<class>/<kit-id>/<filename>.
199
239
  // The Strands system-prompt injection layer can glob for all *.md files under
200
- // .flow-agents/runtime/strands/skills/ to include agent guidance in the context.
240
+ // .kontourai/flow-agents/projections/strands/skills/ to include agent guidance in the context.
201
241
  const filename = path.basename(asset.source_path);
202
- const output = path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
242
+ // Namespace skills by BOTH kit id AND the skill's own directory name: every skill
243
+ // file is literally named SKILL.md, so keying only on kit id collides all of a
244
+ // kit's skills onto one .../<kit-id>/SKILL.md file. Docs keep the flat
245
+ // <kit-id>/<filename> layout (no per-directory collision found).
246
+ const output = asset.asset_class === "skills"
247
+ ? path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), safeSegment(path.basename(path.dirname(asset.source_path))), filename)
248
+ : path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
203
249
  fs.mkdirSync(path.dirname(output), { recursive: true });
204
250
  fs.copyFileSync(asset.source_path, output);
205
251
  generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });