@kontourai/flow-agents 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +41 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +54 -1
  34. package/build/src/lib/flow-resolver.js +112 -5
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +117 -4
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
3
4
  export function readJson(file) {
4
5
  return JSON.parse(fs.readFileSync(file, "utf8"));
5
6
  }
@@ -7,6 +8,123 @@ export function writeJson(file, value) {
7
8
  fs.mkdirSync(path.dirname(file), { recursive: true });
8
9
  fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
9
10
  }
11
+ function existingAncestor(file) {
12
+ let current = path.resolve(file);
13
+ const missing = [];
14
+ while (!fs.existsSync(current)) {
15
+ missing.unshift(path.basename(current));
16
+ const parent = path.dirname(current);
17
+ if (parent === current)
18
+ break;
19
+ current = parent;
20
+ }
21
+ return { ancestor: current, missing };
22
+ }
23
+ /** Resolve a possibly-missing path through its nearest existing ancestor. */
24
+ export function canonicalProspectivePath(file) {
25
+ const { ancestor, missing } = existingAncestor(file);
26
+ return path.resolve(fs.realpathSync(ancestor), ...missing);
27
+ }
28
+ function isContained(root, target) {
29
+ const relative = path.relative(root, target);
30
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
31
+ }
32
+ /** Reject equality and either-direction containment after canonical resolution. */
33
+ export function assertPathsDisjoint(first, second) {
34
+ const firstReal = canonicalProspectivePath(first);
35
+ const secondReal = canonicalProspectivePath(second);
36
+ if (isContained(firstReal, secondReal) || isContained(secondReal, firstReal)) {
37
+ throw new Error(`refusing overlapping paths: ${firstReal} and ${secondReal}`);
38
+ }
39
+ }
40
+ /**
41
+ * Create a directory below root without following any symlink that already
42
+ * exists at or below the root. Ambient aliases above root (for example macOS
43
+ * /tmp -> /private/tmp) are resolved once and are not treated as destination
44
+ * components.
45
+ */
46
+ export function ensureSafeDirectory(root, directory) {
47
+ const rootPath = path.resolve(root);
48
+ if (fs.existsSync(rootPath)) {
49
+ const rootStat = fs.lstatSync(rootPath);
50
+ if (rootStat.isSymbolicLink())
51
+ throw new Error(`refusing symlink destination root: ${rootPath}`);
52
+ if (!rootStat.isDirectory())
53
+ throw new Error(`destination root is not a directory: ${rootPath}`);
54
+ }
55
+ else {
56
+ const { ancestor, missing } = existingAncestor(rootPath);
57
+ let current = fs.realpathSync(ancestor);
58
+ for (const part of missing) {
59
+ current = path.join(current, part);
60
+ if (fs.existsSync(current)) {
61
+ const stat = fs.lstatSync(current);
62
+ if (stat.isSymbolicLink())
63
+ throw new Error(`refusing to write through symlink: ${current}`);
64
+ if (!stat.isDirectory())
65
+ throw new Error(`destination component is not a directory: ${current}`);
66
+ }
67
+ else {
68
+ fs.mkdirSync(current);
69
+ }
70
+ }
71
+ }
72
+ const rootReal = fs.realpathSync(rootPath);
73
+ const target = path.resolve(directory);
74
+ const prospective = canonicalProspectivePath(target);
75
+ if (!isContained(rootReal, prospective))
76
+ throw new Error(`path escapes root: ${target}`);
77
+ const relative = path.relative(rootReal, prospective);
78
+ let current = rootReal;
79
+ for (const part of relative.split(path.sep).filter(Boolean)) {
80
+ current = path.join(current, part);
81
+ if (fs.existsSync(current)) {
82
+ const stat = fs.lstatSync(current);
83
+ if (stat.isSymbolicLink())
84
+ throw new Error(`refusing to write through symlink: ${current}`);
85
+ if (!stat.isDirectory())
86
+ throw new Error(`destination component is not a directory: ${current}`);
87
+ }
88
+ else {
89
+ fs.mkdirSync(current);
90
+ }
91
+ }
92
+ return prospective;
93
+ }
94
+ function assertSafeFileTarget(root, file) {
95
+ const target = path.resolve(file);
96
+ ensureSafeDirectory(root, path.dirname(target));
97
+ const rootReal = fs.realpathSync(path.resolve(root));
98
+ const prospective = canonicalProspectivePath(target);
99
+ if (!isContained(rootReal, prospective) || prospective === rootReal)
100
+ throw new Error(`path escapes root: ${target}`);
101
+ if (fs.existsSync(target)) {
102
+ const stat = fs.lstatSync(target);
103
+ if (stat.isSymbolicLink())
104
+ throw new Error(`refusing to replace symlink: ${target}`);
105
+ if (!stat.isFile())
106
+ throw new Error(`destination is not a regular file: ${target}`);
107
+ }
108
+ return target;
109
+ }
110
+ /** Atomically replace a regular file without following a final-component symlink. */
111
+ export function atomicWriteFile(root, file, data) {
112
+ const target = assertSafeFileTarget(root, file);
113
+ const temp = path.join(path.dirname(target), `.${path.basename(target)}.flow-agents-${process.pid}-${crypto.randomBytes(6).toString("hex")}.tmp`);
114
+ try {
115
+ fs.writeFileSync(temp, data, { flag: "wx" });
116
+ fs.renameSync(temp, target);
117
+ }
118
+ finally {
119
+ fs.rmSync(temp, { force: true });
120
+ }
121
+ }
122
+ export function atomicWriteJson(root, file, value) {
123
+ atomicWriteFile(root, file, `${JSON.stringify(value, null, 2)}\n`);
124
+ }
125
+ export function atomicCopyFile(root, source, target) {
126
+ atomicWriteFile(root, target, fs.readFileSync(source));
127
+ }
10
128
  export function copyDir(src, dest) {
11
129
  fs.mkdirSync(path.dirname(dest), { recursive: true });
12
130
  if (fs.existsSync(dest))
@@ -16,6 +134,60 @@ export function copyDir(src, dest) {
16
134
  filter: (source) => !source.split(path.sep).some((part) => [".git", "__pycache__", ".pytest_cache"].includes(part)),
17
135
  });
18
136
  }
137
+ function copiedTreeDigest(root) {
138
+ const hash = crypto.createHash("sha256");
139
+ for (const file of walkFiles(root)) {
140
+ const rel = path.relative(root, file).split(path.sep).join("/");
141
+ if (rel.split("/").some((part) => [".git", "__pycache__", ".pytest_cache"].includes(part)))
142
+ continue;
143
+ hash.update(rel);
144
+ hash.update("\0");
145
+ hash.update(fs.readFileSync(file));
146
+ hash.update("\0");
147
+ }
148
+ return hash.digest("hex");
149
+ }
150
+ /** Copy through a verified sibling directory, then swap with rollback. */
151
+ export function copyDirAtomic(root, src, dest) {
152
+ assertPathsDisjoint(src, dest);
153
+ const parent = ensureSafeDirectory(root, path.dirname(dest));
154
+ if (fs.existsSync(dest)) {
155
+ const stat = fs.lstatSync(dest);
156
+ if (stat.isSymbolicLink())
157
+ throw new Error(`refusing to replace symlink: ${dest}`);
158
+ if (!stat.isDirectory())
159
+ throw new Error(`destination is not a directory: ${dest}`);
160
+ }
161
+ const nonce = `${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
162
+ const temp = path.join(parent, `.${path.basename(dest)}.flow-agents-${nonce}.tmp`);
163
+ const backup = path.join(parent, `.${path.basename(dest)}.flow-agents-${nonce}.old`);
164
+ try {
165
+ fs.cpSync(src, temp, {
166
+ recursive: true,
167
+ errorOnExist: true,
168
+ force: false,
169
+ filter: (source) => !source.split(path.sep).some((part) => [".git", "__pycache__", ".pytest_cache"].includes(part)),
170
+ });
171
+ if (copiedTreeDigest(src) !== copiedTreeDigest(temp))
172
+ throw new Error(`copied kit verification failed: ${src}`);
173
+ if (fs.existsSync(dest))
174
+ fs.renameSync(dest, backup);
175
+ try {
176
+ fs.renameSync(temp, dest);
177
+ }
178
+ catch (error) {
179
+ if (fs.existsSync(backup))
180
+ fs.renameSync(backup, dest);
181
+ throw error;
182
+ }
183
+ fs.rmSync(backup, { recursive: true, force: true });
184
+ }
185
+ finally {
186
+ fs.rmSync(temp, { recursive: true, force: true });
187
+ if (fs.existsSync(backup) && !fs.existsSync(dest))
188
+ fs.renameSync(backup, dest);
189
+ }
190
+ }
19
191
  export function assertPathContained(root, target) {
20
192
  const rootReal = fs.realpathSync(root);
21
193
  let existingParent = path.dirname(target);
@@ -11,10 +11,53 @@ export declare const LEGACY_TELEMETRY_DIR = ".telemetry";
11
11
  * and the durable per-destination install record root
12
12
  * (`DURABLE_FLOW_AGENTS_DIR`, i.e. `.flow-agents`).
13
13
  */
14
- export declare function defaultCodexHome(): string;
14
+ export declare function defaultCodexHome(env?: NodeJS.ProcessEnv, homedir?: string): string;
15
+ /**
16
+ * #357: resolve the SHARED, git-common-dir-anchored `.kontourai/flow-agents` root for `cwd`.
17
+ *
18
+ * `git rev-parse --git-common-dir` returns the ONE `.git` directory shared by every worktree
19
+ * of a repository (in the primary checkout this is simply `.git` relative to cwd; in a linked
20
+ * worktree it resolves to the primary checkout's real `.git` directory, e.g.
21
+ * `/path/to/primary/.git`, NOT the worktree's own `.git` file). Taking `path.dirname()` of that
22
+ * resolved, absolute path yields the primary checkout's repo root — the one location every
23
+ * worktree of the same repo should treat as the shared `.kontourai/flow-agents` store, so a
24
+ * `liveness claim` or `ensure-session` invoked from ANY worktree's cwd is visible to a reader
25
+ * in any other checkout of the same repo (including the primary one).
26
+ *
27
+ * Returns null (never throws) when git is unavailable, `cwd` is not inside a git working tree,
28
+ * or the command otherwise fails — callers MUST fail open to the existing cwd-based
29
+ * `path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR)` behavior in that case (see flowAgentsArtifactRoot
30
+ * below), which is also exactly what happens today in the single-checkout case: there,
31
+ * `--git-common-dir` resolves to `.git` under cwd itself, so `path.dirname()` of its absolute
32
+ * form is cwd — byte-identical to today's plain `path.resolve(cwd, ...)` result.
33
+ *
34
+ * #413 iteration-2 Fix 3: strips the ambient GIT_DIR/GIT_COMMON_DIR/GIT_WORK_TREE/
35
+ * GIT_CEILING_DIRECTORIES env vars before shelling out. Any of these, if set in the calling
36
+ * process's environment (e.g. leaked from an outer `git rebase`/hook invocation, or a stray
37
+ * export in a dev shell), can silently redirect `--git-common-dir` to resolve a DIFFERENT
38
+ * repository's `.git` than the one `cwd` is actually inside — which would silently redirect
39
+ * the shared `.kontourai/flow-agents` store to that other repo. Passing an explicit env with
40
+ * those keys deleted makes resolution depend only on `cwd`, never on ambient process state.
41
+ */
42
+ export declare function resolveSharedRepoRoot(cwd: string): string | null;
43
+ /**
44
+ * #413 iteration-2 Fix 2 (documentation only, no behavior change): when `cwd` is a
45
+ * SUBDIRECTORY of a plain (non-worktree) checkout, `git rev-parse --git-common-dir` returns a
46
+ * RELATIVE path (e.g. `../.git` or deeper) that `resolveSharedRepoRoot` walks up to the repo
47
+ * root — so this intentionally resolves to `<repo-root>/.kontourai/flow-agents` regardless of
48
+ * which subdirectory `cwd` is, NOT `<subdir>/.kontourai/flow-agents` (the old plain
49
+ * `path.resolve(cwd, ...)` cwd-anchored behavior). This is the CORRECT shared-store semantics —
50
+ * one store per repo, matching every other cwd (primary checkout, linked worktree) this
51
+ * resolver already unifies — not a regression to guard against. See
52
+ * evals/integration/test_liveness_worktree_root.sh's AC6b for the explicit assertion, and its
53
+ * header docstring for why AC6's "byte-identical" guarantee is scoped to repo-root cwd and
54
+ * non-git cwd only, never to an arbitrary subdirectory cwd.
55
+ */
15
56
  export declare function flowAgentsArtifactRoot(cwd?: string): string;
16
57
  export declare function durableFlowAgentsRoot(cwd?: string): string;
17
58
  export declare function durableInstallRecordPath(cwd?: string): string;
59
+ /** Path to the per-skill-file content-hash drift manifest, a sibling of `install.json` under the same durable root. */
60
+ export declare function skillsManifestPath(cwd?: string): string;
18
61
  export declare function telemetryDataDir(cwd?: string): string;
19
62
  export declare function legacyTelemetryDataDir(cwd?: string): string;
20
63
  export declare function firstExistingPath(candidates: string[]): string;
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import { execFileSync } from "node:child_process";
4
5
  export const KONTOURAI_DIR = ".kontourai";
5
6
  export const FLOW_AGENTS_RUNTIME_SUBDIR = "flow-agents";
6
7
  export const FLOW_AGENTS_RUNTIME_DIR = `${KONTOURAI_DIR}/${FLOW_AGENTS_RUNTIME_SUBDIR}`;
@@ -14,11 +15,134 @@ export const LEGACY_TELEMETRY_DIR = ".telemetry";
14
15
  * and the durable per-destination install record root
15
16
  * (`DURABLE_FLOW_AGENTS_DIR`, i.e. `.flow-agents`).
16
17
  */
17
- export function defaultCodexHome() {
18
- return process.env["CODEX_HOME"] || path.join(os.homedir(), ".codex");
18
+ export function defaultCodexHome(env = process.env, homedir = os.homedir()) {
19
+ return env["CODEX_HOME"] || path.join(homedir, ".codex");
19
20
  }
21
+ /**
22
+ * #357: resolve the SHARED, git-common-dir-anchored `.kontourai/flow-agents` root for `cwd`.
23
+ *
24
+ * `git rev-parse --git-common-dir` returns the ONE `.git` directory shared by every worktree
25
+ * of a repository (in the primary checkout this is simply `.git` relative to cwd; in a linked
26
+ * worktree it resolves to the primary checkout's real `.git` directory, e.g.
27
+ * `/path/to/primary/.git`, NOT the worktree's own `.git` file). Taking `path.dirname()` of that
28
+ * resolved, absolute path yields the primary checkout's repo root — the one location every
29
+ * worktree of the same repo should treat as the shared `.kontourai/flow-agents` store, so a
30
+ * `liveness claim` or `ensure-session` invoked from ANY worktree's cwd is visible to a reader
31
+ * in any other checkout of the same repo (including the primary one).
32
+ *
33
+ * Returns null (never throws) when git is unavailable, `cwd` is not inside a git working tree,
34
+ * or the command otherwise fails — callers MUST fail open to the existing cwd-based
35
+ * `path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR)` behavior in that case (see flowAgentsArtifactRoot
36
+ * below), which is also exactly what happens today in the single-checkout case: there,
37
+ * `--git-common-dir` resolves to `.git` under cwd itself, so `path.dirname()` of its absolute
38
+ * form is cwd — byte-identical to today's plain `path.resolve(cwd, ...)` result.
39
+ *
40
+ * #413 iteration-2 Fix 3: strips the ambient GIT_DIR/GIT_COMMON_DIR/GIT_WORK_TREE/
41
+ * GIT_CEILING_DIRECTORIES env vars before shelling out. Any of these, if set in the calling
42
+ * process's environment (e.g. leaked from an outer `git rebase`/hook invocation, or a stray
43
+ * export in a dev shell), can silently redirect `--git-common-dir` to resolve a DIFFERENT
44
+ * repository's `.git` than the one `cwd` is actually inside — which would silently redirect
45
+ * the shared `.kontourai/flow-agents` store to that other repo. Passing an explicit env with
46
+ * those keys deleted makes resolution depend only on `cwd`, never on ambient process state.
47
+ */
48
+ export function resolveSharedRepoRoot(cwd) {
49
+ try {
50
+ const env = { ...process.env };
51
+ delete env["GIT_DIR"];
52
+ delete env["GIT_COMMON_DIR"];
53
+ delete env["GIT_WORK_TREE"];
54
+ delete env["GIT_CEILING_DIRECTORIES"];
55
+ const out = execFileSync("git", ["rev-parse", "--git-common-dir"], {
56
+ cwd,
57
+ env,
58
+ encoding: "utf8",
59
+ stdio: ["ignore", "pipe", "ignore"],
60
+ }).trim();
61
+ if (!out)
62
+ return null;
63
+ const absoluteCommonDir = path.resolve(cwd, out);
64
+ return path.dirname(absoluteCommonDir);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * #413 iteration-2 Fix 1: cheap discriminator for the loud-vs-silent fail-open decision below.
72
+ * Walks up from `cwd` (bounded, same 40-level cap the hook-side ancestor walks use) looking for
73
+ * a `.git` entry — a FILE (linked worktree's gitdir pointer) or a DIRECTORY (primary checkout) —
74
+ * without shelling out to git at all, so it stays meaningful even when git itself is the thing
75
+ * that's broken/absent. Never throws; a filesystem error at any level is treated as "no .git
76
+ * found here", not as a crash.
77
+ */
78
+ function isInsideGitWorkingTree(cwd) {
79
+ try {
80
+ let dir = path.resolve(cwd);
81
+ const root = path.parse(dir).root;
82
+ for (let depth = 0; depth < 40; depth++) {
83
+ if (fs.existsSync(path.join(dir, ".git")))
84
+ return true;
85
+ if (dir === root)
86
+ break;
87
+ const parent = path.dirname(dir);
88
+ if (parent === dir)
89
+ break;
90
+ dir = parent;
91
+ }
92
+ return false;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ /**
99
+ * #413 iteration-2 Fix 1 (HIGH, security): emit a LOUD stderr warning before falling open, but
100
+ * ONLY when `cwd` is genuinely inside a git working tree (per isInsideGitWorkingTree above) AND
101
+ * resolveSharedRepoRoot still returned null — i.e. git resolution was ATTEMPTED and FAILED, not
102
+ * merely "there was no git repo to resolve in the first place". That second, benign case (a
103
+ * scratch dir, a non-git cwd) must stay perfectly silent — this is exactly what
104
+ * evals/integration/test_liveness_worktree_root.sh's AC6 companion case (no git at all) already
105
+ * asserts, and it must remain byte-identical to today.
106
+ *
107
+ * The loud case (corrupted gitlink, bad GIT_DIR-in-cwd's-own-config, git binary missing, a
108
+ * permissions error on .git, etc.) is #357's exact silent-fail-open bug: an actor's
109
+ * liveness/ownership claim lands in an ISOLATED cwd-local store invisible to every other
110
+ * worktree/checkout of the same repo, with no diagnostic at all. A warning (not a hard refuse —
111
+ * this helper sits on read paths too, and a degraded read must still return something usable)
112
+ * gives an operator the signal needed to pass `--artifact-root` explicitly instead of silently
113
+ * losing coordination visibility.
114
+ */
115
+ function warnIfFailingOpenInsideGitTree(cwd, fallbackPath) {
116
+ if (!isInsideGitWorkingTree(cwd))
117
+ return;
118
+ process.stderr.write(`[artifact-root] WARNING: inside a git working tree but could not resolve the shared repo root ` +
119
+ `(git rev-parse --git-common-dir failed or returned nothing from ${cwd}); falling back to a ` +
120
+ `cwd-local store at ${fallbackPath} — coordination claims may be invisible to other ` +
121
+ `worktrees/actors. Pass --artifact-root explicitly to fix.\n`);
122
+ }
123
+ /**
124
+ * #413 iteration-2 Fix 2 (documentation only, no behavior change): when `cwd` is a
125
+ * SUBDIRECTORY of a plain (non-worktree) checkout, `git rev-parse --git-common-dir` returns a
126
+ * RELATIVE path (e.g. `../.git` or deeper) that `resolveSharedRepoRoot` walks up to the repo
127
+ * root — so this intentionally resolves to `<repo-root>/.kontourai/flow-agents` regardless of
128
+ * which subdirectory `cwd` is, NOT `<subdir>/.kontourai/flow-agents` (the old plain
129
+ * `path.resolve(cwd, ...)` cwd-anchored behavior). This is the CORRECT shared-store semantics —
130
+ * one store per repo, matching every other cwd (primary checkout, linked worktree) this
131
+ * resolver already unifies — not a regression to guard against. See
132
+ * evals/integration/test_liveness_worktree_root.sh's AC6b for the explicit assertion, and its
133
+ * header docstring for why AC6's "byte-identical" guarantee is scoped to repo-root cwd and
134
+ * non-git cwd only, never to an arbitrary subdirectory cwd.
135
+ */
20
136
  export function flowAgentsArtifactRoot(cwd = process.cwd()) {
21
- return path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
137
+ const sharedRepoRoot = resolveSharedRepoRoot(cwd);
138
+ if (sharedRepoRoot)
139
+ return path.resolve(sharedRepoRoot, FLOW_AGENTS_RUNTIME_DIR);
140
+ // Fail-open: not a git repo, git unavailable, or command failed — reproduce today's plain
141
+ // cwd-based behavior unchanged (AC6 backward-compat guarantee). #413 iteration-2 Fix 1: loud,
142
+ // not silent, when cwd IS inside a git working tree (see warnIfFailingOpenInsideGitTree).
143
+ const fallback = path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
144
+ warnIfFailingOpenInsideGitTree(cwd, fallback);
145
+ return fallback;
22
146
  }
23
147
  export function durableFlowAgentsRoot(cwd = process.cwd()) {
24
148
  return path.resolve(cwd, DURABLE_FLOW_AGENTS_DIR);
@@ -26,6 +150,10 @@ export function durableFlowAgentsRoot(cwd = process.cwd()) {
26
150
  export function durableInstallRecordPath(cwd = process.cwd()) {
27
151
  return path.join(durableFlowAgentsRoot(cwd), "install.json");
28
152
  }
153
+ /** Path to the per-skill-file content-hash drift manifest, a sibling of `install.json` under the same durable root. */
154
+ export function skillsManifestPath(cwd = process.cwd()) {
155
+ return path.join(durableFlowAgentsRoot(cwd), "skills-manifest.json");
156
+ }
29
157
  export function telemetryDataDir(cwd = process.cwd()) {
30
158
  return path.resolve(cwd, KONTOURAI_DIR, "telemetry");
31
159
  }
@@ -8,8 +8,36 @@ export type KitAsset = {
8
8
  source_kind: string;
9
9
  description?: string;
10
10
  };
11
+ export type KitWorkflowTriggerRef = {
12
+ kit_id: string;
13
+ source_kind: string;
14
+ id: string;
15
+ when: string;
16
+ target_flow_id?: string;
17
+ default_skill?: string;
18
+ conditional_skills?: {
19
+ when: string;
20
+ skill: string;
21
+ }[];
22
+ required_sequence?: string[];
23
+ post_verify_targets?: string[];
24
+ };
25
+ export type KitHookInfluenceExpectationRef = {
26
+ kit_id: string;
27
+ source_kind: string;
28
+ id: string;
29
+ description: string;
30
+ tier: string;
31
+ hook?: string;
32
+ event?: string;
33
+ must_include_guidance: string[];
34
+ must_include_actions: string[];
35
+ };
11
36
  export type KitInventory = {
12
37
  assets: KitAsset[];
38
+ dependencies: KitDependencyRef[];
39
+ workflow_triggers: KitWorkflowTriggerRef[];
40
+ hook_influence_expectations: KitHookInfluenceExpectationRef[];
13
41
  warnings: string[];
14
42
  errors: string[];
15
43
  };
@@ -24,7 +52,15 @@ export type LoadedKit = {
24
52
  kit_id: string;
25
53
  assets: KitAsset[];
26
54
  dependencies: KitDependencyRef[];
55
+ workflow_triggers: KitWorkflowTriggerRef[];
56
+ hook_influence_expectations: KitHookInfluenceExpectationRef[];
27
57
  };
28
- export declare function readKitInventory(sourceRoot: string, dest: string): KitInventory;
29
- export declare function activateCodexLocal(sourceRoot: string, dest: string): Record<string, unknown>;
30
- export declare function activateStrandsLocal(sourceRoot: string, dest: string): Record<string, unknown>;
58
+ export declare function readKitInventory(sourceRoot: string, dest: string, options?: {
59
+ kitIdFilter?: string[];
60
+ }): KitInventory;
61
+ export declare function activateCodexLocal(sourceRoot: string, dest: string, options?: {
62
+ kitIdFilter?: string[];
63
+ }): Record<string, unknown>;
64
+ export declare function activateStrandsLocal(sourceRoot: string, dest: string, options?: {
65
+ kitIdFilter?: string[];
66
+ }): Record<string, unknown>;