akm-cli 0.9.17-alpha.2 → 0.9.17-alpha.4

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 (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. package/dist/workflows/validator.js +0 -60
@@ -3,17 +3,15 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { ConfigError } from "../../core/errors.js";
5
5
  import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
6
- import { assertFrozenExecutableIdentity } from "../../execution/executable-identity.js";
7
- import { canonicalResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js";
8
- import { dispatchLoweredExecutionRequest, lowerResolvedExecutionRequestWithRunner, } from "../../integrations/agent/execution-lowering.js";
6
+ import { canonicalResolvedExecutionRequest } from "../../execution/resolved-request.js";
7
+ import { buildExecutionFromWire } from "../../integrations/agent/execution.js";
8
+ import { runExecution } from "../../integrations/agent/runner-dispatch.js";
9
9
  import { getHarness } from "../../integrations/harnesses/index.js";
10
- /** Lower a persisted v4 common request through its persisted runner only. */
10
+ /** Lower a frozen common request through its frozen runner only. */
11
11
  export function prepareWorkflowExecution(request, prompt = request.prompt) {
12
12
  const target = request.frozenTarget;
13
13
  if (target.cwdIdentity)
14
14
  assertFrozenDirectoryContained(target.cwdIdentity);
15
- if (target.executable)
16
- assertFrozenExecutableIdentity(target.executable, `unit ${request.unitId} executable`);
17
15
  const wire = JSON.parse(canonicalResolvedExecutionRequest(target.request));
18
16
  const command = { ...wire.command, content: prompt };
19
17
  const runtime = {
@@ -30,42 +28,15 @@ export function prepareWorkflowExecution(request, prompt = request.prompt) {
30
28
  if (request.systemPrompt !== undefined) {
31
29
  wire.conversation = [{ role: "system", content: request.systemPrompt }];
32
30
  }
33
- return lowerResolvedExecutionRequestWithRunner(decodeResolvedExecutionRequest(wire), target.runner);
31
+ return buildExecutionFromWire({ request: wire, runner: target.runner });
34
32
  }
35
33
  function message(err) {
36
34
  return err instanceof Error ? err.message : String(err);
37
35
  }
38
36
  /**
39
- * The `eventSource` value `dispatchWorkflowExecution` should forward into
40
- * `dispatchLoweredExecutionRequest`'s options, or `undefined` to forward
41
- * nothing.
42
- *
43
- * Precedence fix (P1b Lane C code review, round 2). The gap-fix originally
44
- * forwarded `request.eventSource` unconditionally.
45
- * `dispatchLoweredExecutionRequest` applies a forwarded value as `env: {
46
- * ...lowered.options.env, AKM_EVENT_SOURCE: eventSource }`
47
- * (execution-lowering.ts:998-1001) — an unconditional override of that one
48
- * key — and `lowered.options.env` IS the unit's own authored/resolved `env:`
49
- * binding (`request.env`, folded in by `prepareWorkflowExecution` above via
50
- * `request.runtime.environment`), so the unconditional forward let the
51
- * provenance stamp win over an authored `env: { AKM_EVENT_SOURCE: ... }`
52
- * binding. That inverts the precedence pre-P1b had (the child env was built
53
- * from ambient passthrough with `options.env` — the authored binding —
54
- * applied AFTER it, at highest precedence, in `buildChildEnv`/`spawn.ts`) and
55
- * disagrees with the sibling "script"/"shell" arm: `exec-unit.ts`'s own
56
- * `childEnv` stamps its allowlisted base only when the name is absent there,
57
- * strictly BEFORE the bindings overlay runs, so an authored binding always
58
- * wins there. Gating the forward on `request.env` not already binding the
59
- * name restores agreement: an authored binding leaves `eventSource`
60
- * unforwarded (so the merge above never touches the key, and the authored
61
- * value in `lowered.options.env` stands), while an absent binding still
62
- * forwards the resolved value exactly as before.
63
- *
64
- * Exported so this precedence rule is pinned directly:
65
- * `dispatchWorkflowExecution` itself has no injectable
66
- * `runAgent`/`executeRunner`/`chat` seam to exercise the decision end-to-end
67
- * without a live agent/LLM dispatch (see the P1b spec's Review log, which
68
- * records the same constraint for the gap-fix this corrects).
37
+ * The `eventSource` to forward into `runExecution`, or undefined when the
38
+ * unit's own `env:` already binds `AKM_EVENT_SOURCE` — an authored binding
39
+ * wins, as it does for exec units.
69
40
  */
70
41
  export function forwardedDispatchEventSource(request) {
71
42
  if (request.eventSource === undefined)
@@ -80,20 +51,8 @@ export function forwardedDispatchEventSource(request) {
80
51
  */
81
52
  export async function dispatchWorkflowExecution(request, feedback) {
82
53
  const prompt = feedback ? `${request.prompt}\n\n${feedback}` : request.prompt;
83
- // B-N11 (P3b, spec docs/plans/specs/p3b-child-executor.md §1.6): an
84
- // internal-invariant guard, not a user-facing one. `dispatchJournaledAttempt`
85
- // (native-executor.ts, P3b §3.2) routes a `child-workflow` unit to the child
86
- // executor (child-workflow.ts) BEFORE dispatch is ever reached, so arriving
87
- // HERE with one means that seam was BYPASSED — an engine routing bug, never
88
- // a not-yet-implemented feature (that premise, P3a Review log R8's, is gone
89
- // now that P3b ships a production caller). A plain `Error` naming the seam
90
- // that should have been reached instead, not a `UsageError`: nothing a user
91
- // can author reaches this line once the seam exists, so there is no
92
- // user-facing code to carry. Kept here, rather than deleted outright, so a
93
- // bypassed seam still fails closed instead of falling into the generic
94
- // `kind !== "command"` guard below, which would blame a legitimate target
95
- // kind as "not a command target" — the exact false, unhelpful message R8
96
- // was opened to remove.
54
+ // Internal invariant: native-executor routes child-workflow units to the
55
+ // child executor before dispatch, so reaching here is an engine bug.
97
56
  if (request.frozenTarget.kind === "child-workflow") {
98
57
  throw new Error(`unit ${JSON.stringify(request.unitId)} targets child workflow ${JSON.stringify(request.frozenTarget.ref)}, ` +
99
58
  "but reached dispatchWorkflowExecution directly. The child-workflow dispatch seam " +
@@ -128,23 +87,12 @@ export async function dispatchWorkflowExecution(request, feedback) {
128
87
  let result;
129
88
  try {
130
89
  const eventSource = forwardedDispatchEventSource(request);
131
- result = await dispatchLoweredExecutionRequest(lowered, {
90
+ result = await runExecution(lowered, {
132
91
  runOptions: {
133
92
  stdio: "captured",
134
93
  parseOutput: "text",
135
94
  ...(request.signal ? { signal: request.signal } : {}),
136
95
  },
137
- // Gap fix (P1b Lane C code review, spec §5.2(2)); precedence-gated
138
- // (round 2, see forwardedDispatchEventSource above): forward the
139
- // resolved provenance event source so an "agent"/"sdk" unit's
140
- // dispatched child env carries AKM_EVENT_SOURCE too, not only a
141
- // "script"/"shell" unit's — but only when the unit's own authored
142
- // `env:` binding does not already set the name, so an authored binding
143
- // still wins, mirroring exec-unit.ts's childEnv guard.
144
- // dispatchLoweredExecutionRequest applies a forwarded value as exactly
145
- // one child-env key (execution-lowering.ts:998-1001) — the same
146
- // mechanism the R-07 command-arm fix (command-execution.ts) already
147
- // uses.
148
96
  ...(eventSource !== undefined ? { eventSource } : {}),
149
97
  });
150
98
  }
@@ -2,62 +2,18 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Serialized writer queue for `workflow_run_units` (orchestration plan,
6
- * *Persistence changes*).
7
- *
8
- * ## What the chain actually protects
9
- *
10
- * SQLite allows exactly ONE writer per database FILE. Outside a
11
- * `withWorkflowRunsConnection` scope, `withWorkflowRunsRepo` opens a fresh
12
- * connection per call, so N units completing at once would be N separate
13
- * connections racing for the same file's write lock, burning the 30 s
14
- * `busy_timeout` on contention we created ourselves. Bun runs a single-threaded
15
- * event loop, so an in-process promise chain is a sufficient (and free)
16
- * admission control: every unit write executes strictly in enqueue order.
17
- *
18
- * That is the ONLY invariant the chain owns. In particular it does NOT own
19
- * per-unit reserve→finish ordering: `dispatchJournaledAttempt` awaits its reservation
20
- * before it dispatches and only finishes after the dispatch resolves, so a
21
- * unit's own writes are ordered by program order, not by queue position. The
22
- * queue could be reordered arbitrarily between units without breaking the
23
- * attempt claim/dispatch CAS.
24
- *
25
- * ## Scope of the serialization (issue B)
26
- *
27
- * The chain is keyed by DATABASE PATH, not global, because the resource being
28
- * protected is one SQLite file's write lock. Two runs against two different
29
- * data dirs (`AKM_DATA_DIR` isolation, a test sandbox, a second stash) share no
30
- * write lock and must not queue behind each other. Narrowing further — per run
31
- * — would be unsound: two runs in the SAME state.db still contend for that one
32
- * file's writer, which is precisely what the chain exists to avoid.
33
- *
34
- * Inside a `withWorkflowRunsConnection` scope the chain also becomes nearly
35
- * free: every write goes through the same handle, so each queued task is a
36
- * synchronous statement (or a synchronous `BEGIN IMMEDIATE … COMMIT`) that
37
- * settles in microseconds rather than a per-call open/migrate-preflight/close.
38
- * The queue depth stops gating unit completion in practice, without weakening
39
- * anything.
40
- *
41
- * Reads and gate evaluation stay OFF this queue — only writes serialize.
42
- *
43
- * A failed write rejects its own caller but never wedges its chain.
5
+ * Serialized writer queue for `workflow_run_units`. SQLite allows one writer
6
+ * per database file, so unit writes are chained per state.db path (an
7
+ * in-process promise chain suffices on Bun's single thread) instead of N
8
+ * connections racing for the lock. A unit's own reserve→finish order comes
9
+ * from program order, not the queue. Reads stay off it; a failed write rejects
10
+ * its caller without wedging the chain.
44
11
  */
45
12
  import { serializeByKey } from "../../core/concurrent.js";
46
13
  import { getStateDbPath } from "../../core/state-db.js";
47
- /**
48
- * One promise chain per database path ({@link serializeByKey}). Entries are
49
- * pruned when their chain drains, so a long-lived process that touches many
50
- * data dirs (the test harness swaps `AKM_DATA_DIR` per test) does not
51
- * accumulate one resolved promise per path forever.
52
- */
14
+ /** One promise chain per database path, pruned when it drains ({@link serializeByKey}). */
53
15
  const chains = new Map();
54
- /**
55
- * Enqueue a `workflow_run_units` write behind every write already queued for
56
- * the same state.db. The key is the current state.db path — the file whose one
57
- * write lock this exists to protect — and is not a caller's choice: a caller
58
- * writing somewhere else is not on this queue at all, and wants
59
- * {@link serializeByKey} with its own chain map.
60
- */
16
+ /** Enqueue a `workflow_run_units` write behind every write already queued for the current state.db. */
61
17
  export function enqueueUnitWrite(fn) {
62
18
  return serializeByKey(chains, getStateDbPath(), fn);
63
19
  }
@@ -2,102 +2,37 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * Git worktree lifecycle for `isolation: worktree` units (redesign addendum,
6
- * R2). Parallel file-mutating units on the agent/sdk runners each get a
7
- * fresh DETACHED worktree of the run's base repository under a run-scoped
8
- * tmp directory, so concurrent units can never trample each other's working
9
- * tree. Lifecycle (driven by the native executor per journaled attempt):
5
+ * Git worktree lifecycle for `isolation: worktree` units: each attempt of a
6
+ * file-mutating agent/sdk unit gets a fresh detached worktree of the run's base
7
+ * repository under `<tmp>/akm-worktrees/<runId>/<attemptId>`.
10
8
  *
11
- * 1. {@link assertGitWorkTree} — preflight, once per step: a non-git base
12
- * directory fails the step cleanly before anything dispatches.
13
- * 2. {@link createUnitWorktree} — `git worktree add --detach` into
14
- * `<tmp>/akm-worktrees/<runId>/<attemptId>`; the path is journaled on
15
- * the unit row (`workflow_run_units.worktree_path`, migration 004) and
16
- * passed to dispatch as the unit's cwd.
17
- * 3. {@link cleanupUnitWorktree} — after the unit finishes:
18
- * `git status --porcelain` CLEAN → the worktree is removed;
19
- * DIRTY → it is RETAINED (the caller logs the path) so uncollected work
20
- * is never destroyed.
21
- * 4. {@link sweepStaleWorktrees} — opportunistic, at most once per process:
22
- * an age-based GC of run roots and retained trees that outlived their run.
23
- * Age alone cannot see a unit that is still running in ANOTHER process, so
24
- * every live worktree carries a liveness lease (pid + host + path) in
25
- * git's own administrative directory for it, and the sweep skips a tree
26
- * whose lease holder is still running.
9
+ * 1. {@link assertGitWorkTree} — once per step, before dispatch.
10
+ * 2. {@link createUnitWorktree} — `git worktree add --detach`; the path is
11
+ * journaled on the unit row and becomes the unit's cwd.
12
+ * 3. {@link cleanupUnitWorktree} — a clean tree is removed, a dirty one kept.
13
+ * "Clean" ignores `.gitignore`d files (build outputs, `node_modules`),
14
+ * which the repo already declares disposable.
15
+ * 4. {@link sweepStaleWorktrees} — at most once per process, removes trees
16
+ * and run roots older than a week.
27
17
  *
28
- * What "uncollected work" means (the honest contract): the clean probe is
29
- * `git status --porcelain` WITHOUT `--ignored`, so it counts tracked-file
30
- * modifications and untracked *unignored* files, but NOT files the base repo's
31
- * own `.gitignore` matches (build outputs, caches, logs, dependency dirs such
32
- * as `node_modules`/`dist`). Those ignored files are DISPOSABLE BY DEFINITION
33
- * — the repository already declares them regenerable — so a worktree whose only
34
- * residue is ignored files probes clean and IS removed. This is deliberate:
35
- * adding `--ignored` would retain a worktree after essentially every unit that
36
- * ran a package install or a build (the ignored `node_modules`/`dist` tree),
37
- * blowing up disk under the run-scoped tmp root. Work a unit needs preserved
38
- * must therefore be tracked or untracked-unignored; anything the workflow
39
- * repo has chosen to `.gitignore` is treated as throwaway.
40
- *
41
- * Concurrency (bug 6). `git worktree add|prune|remove` mutate the base repo's
42
- * administrative state (`.git/worktrees/*`) under repo-level locks, so a map
43
- * step running N isolated units at once used to have N of them racing on the
44
- * same repository. Two invariants close that:
45
- *
46
- * • every repo-mutating operation runs inside {@link withRepoWorktreeLock},
47
- * a promise chain keyed by the resolved base repo path (`serializeByKey`
48
- * in `core/concurrent.ts`, shared with `unit-writer.ts` — Bun is
49
- * single-threaded, so an in-process chain is sufficient), so at most one
50
- * add/prune/remove per repository is ever in flight;
51
- * • those git calls are ASYNC ({@link runManagedSubprocess}) rather than
52
- * `spawnSync`, so a unit waiting on a git lock parks a promise instead of
53
- * wedging the whole event loop (and with it every other in-flight unit,
54
- * the lease heartbeat, and abort handling).
55
- *
56
- * The two sync git shell-outs that remain — {@link isGitAvailable} and
57
- * {@link assertGitWorkTree} — are read-only, take no repo lock, and run
58
- * BEFORE any unit dispatches (preflight / test gate), so they can never block
59
- * work that is already in flight.
60
- *
61
- * This module never throws — every operation returns a result object so the
62
- * executor maps failures onto its own step/unit failure vocabulary. The GC
63
- * sweep is the sole exception to "no logging here": it is fire-and-forget and
64
- * has no caller to report to, so it reports through `warn`.
18
+ * Every repo-mutating git call is async and serialized per base repository
19
+ * ({@link withRepoWorktreeLock}), so parallel units never race on
20
+ * `.git/worktrees`. Nothing here throws: results are objects the executor maps
21
+ * onto its failure vocabulary (the fire-and-forget GC sweep reports via `warn`).
65
22
  */
66
23
  import { spawnSync } from "node:child_process";
67
24
  import fsp from "node:fs/promises";
68
25
  import os from "node:os";
69
26
  import path from "node:path";
70
- import { isProcessAlive, isWithinAsync, safeRealpathAsync } from "../../core/common.js";
27
+ import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
71
28
  import { serializeByKey } from "../../core/concurrent.js";
72
29
  import { runManagedSubprocess } from "../../core/subprocess.js";
73
30
  import { warn } from "../../core/warn.js";
74
- /**
75
- * Timeout for every `git worktree add|prune|remove|status` call this module
76
- * makes. Was 30s; raised to 120s after #891 confirmed a real product gap:
77
- * these calls run under {@link withRepoWorktreeLock}, a per-process,
78
- * per-repository queue, so a machine also busy with OTHER git-heavy work
79
- * (other akm runs, other agents' worktrees, a loaded CI runner) can genuinely
80
- * push one `git worktree remove` past 30s without anything being stuck —
81
- * plain CPU/fork/IO contention. At 30s that showed up as `{ removed: false,
82
- * error: "... timed out after 30000ms" }` on a perfectly healthy op: a clean
83
- * worktree left retained, or a create failing a unit outright, purely because
84
- * the box was busy. 120s matches the same trade already made for
85
- * {@link GIT_PUSH_TIMEOUT_MS} (`core/write-source.ts`,
86
- * `sources/providers/git-stash.ts`) for the same class of administrative git
87
- * call under load; a call that is truly hung (not just slow) is still caught.
88
- */
31
+ /** Timeout for each worktree git call (#891: a busy machine can push a healthy one past 30s). */
89
32
  const GIT_TIMEOUT_MS = 120_000;
90
33
  /** Directory under `os.tmpdir()` that owns every run's worktree roots. */
91
34
  export const WORKTREES_DIR_NAME = "akm-worktrees";
92
- /**
93
- * Age after which an orphaned entry under the worktrees root is swept.
94
- *
95
- * Retained dirty worktrees are forensic state — deleting them is only
96
- * acceptable once they are far past any plausible investigation window. Seven
97
- * days is one full on-call rotation: long enough that a retained tree from a
98
- * failed run has been triaged (or abandoned), short enough that a tmpdir does
99
- * not accumulate whole repository checkouts indefinitely.
100
- */
35
+ /** Age after which an orphaned or retained worktree is swept: long enough to triage a failed run. */
101
36
  export const STALE_WORKTREE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
102
37
  function gitExitError(args, code, stderr, stdout) {
103
38
  const detail = (stderr || stdout || "").trim();
@@ -128,15 +63,7 @@ async function realGitExecutor(cwd, args) {
128
63
  return { ok: true, stdout: result.stdout };
129
64
  }
130
65
  let gitExecutor = realGitExecutor;
131
- /**
132
- * TEST-ONLY seam (#891): swap the executor every repo-mutating git call in
133
- * this module goes through. Lets a test prove those calls genuinely
134
- * interleave with other event-loop work — the property the old "count
135
- * setInterval ticks during real git calls" test asserted before it was
136
- * deleted for timing on the real scheduler instead of on behavior — using a
137
- * fake, deterministically-timed async git in place of the real subprocess.
138
- * Call with `undefined` to restore the real spawn-based executor.
139
- */
66
+ /** Test seam: swap the git executor for repo-mutating calls (`undefined` restores the real one). */
140
67
  export function setGitExecutorForTesting(executor) {
141
68
  gitExecutor = executor ?? realGitExecutor;
142
69
  }
@@ -191,13 +118,7 @@ export function assertGitWorkTree(dir) {
191
118
  // ── Per-repository serialization ────────────────────────────────────────────
192
119
  /** In-flight tail of each base repository's serialized git-worktree chain. */
193
120
  const repoOperationTails = new Map();
194
- /**
195
- * Base repos already pruned in this process, per run id. Granularity is
196
- * per-(repo, run), not per-repo: a run resuming against a repo another run
197
- * already pruned must still reap ITS own orphaned registrations. A run's whole
198
- * entry is dropped when its drained worktree root is removed
199
- * ({@link removeRunRootIfEmpty}), so the map never outgrows the live runs.
200
- */
121
+ /** Base repos already pruned in this process, per run id (dropped with the run's drained root). */
201
122
  const prunedRuns = new Map();
202
123
  /**
203
124
  * Serialize `fn` against every other repo-mutating worktree operation on the
@@ -237,27 +158,10 @@ async function pathExists(p) {
237
158
  return fsp.access(p).then(() => true, () => false);
238
159
  }
239
160
  /**
240
- * Create a fresh DETACHED worktree of `baseDir`'s repository at
241
- * `<tmp>/akm-worktrees/<runId>/<attemptId>` (detached HEAD — no branch is
242
- * minted, so parallel units cannot collide on branch names).
243
- *
244
- * A leftover directory at the attempt path (a RETAINED dirty worktree from a
245
- * prior invocation, or a crashed attempt's partial state) is handled with the
246
- * same never-destroy-unverified-work rule as {@link cleanupUnitWorktree}:
247
- * `git status --porcelain` CLEAN → removed; DIRTY or unverifiable (the probe
248
- * fails — e.g. a half-created directory that is no longer a valid worktree)
249
- * → moved aside to `<dest>.retained-<ts>` and reported via
250
- * `preservedLeftover` so the caller can log where the work went. Either way
251
- * `git worktree prune` clears the stale registration before re-creating.
252
- *
253
- * The whole body runs under {@link withRepoWorktreeLock}: the leftover probe,
254
- * the prune and the add form ONE critical section against the base repo's
255
- * administrative state, so a concurrent unit's prune can never land between
256
- * another unit's prune and its add.
257
- *
258
- * A successful add takes a liveness lease ({@link acquireWorktreeLease}) so the
259
- * GC sweep — in this process or another one — never collects the tree while the
260
- * unit is still running in it.
161
+ * Create a fresh detached worktree at `<tmp>/akm-worktrees/<runId>/<attemptId>`.
162
+ * A leftover at that path is removed when clean, otherwise moved aside to
163
+ * `<dest>.retained-<ts>` (`preservedLeftover`). Probe, prune, and add form one
164
+ * critical section under {@link withRepoWorktreeLock}.
261
165
  */
262
166
  export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
263
167
  // Opportunistic, at most once per process, never awaited — GC must never sit
@@ -274,7 +178,7 @@ export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
274
178
  if (status.ok && status.stdout.trim() === "") {
275
179
  // Async on purpose: a recursive delete of a whole leftover checkout
276
180
  // inside this critical section would otherwise block the event loop
277
- // (every other in-flight unit, the lease heartbeat, abort handling).
181
+ // (every other in-flight unit, abort handling).
278
182
  await fsp.rm(dest, { recursive: true, force: true });
279
183
  }
280
184
  else {
@@ -291,14 +195,8 @@ export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
291
195
  ...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
292
196
  };
293
197
  }
294
- // Prune only drops administrative entries whose worktree directory is
295
- // already gone; it never touches a live worktree. Two triggers, both
296
- // necessary, and never per-unit-attempt (which multiplied lock contention
297
- // without buying safety):
298
- // • a leftover was just removed/moved — its stale registration MUST go
299
- // before re-adding at the same path;
300
- // • first worktree of this (repo, run) — reaps registrations orphaned by
301
- // earlier runs whose roots were GC'd or deleted out from under git.
198
+ // Prune (never touches a live worktree) after a leftover was removed, and
199
+ // on the first worktree of this (repo, run) to reap orphaned registrations.
302
200
  const prunedRepos = prunedRuns.get(runId);
303
201
  if (leftoverHandled || !prunedRepos?.has(repoKey)) {
304
202
  if (prunedRepos)
@@ -315,47 +213,24 @@ export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
315
213
  ...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
316
214
  };
317
215
  }
318
- await acquireWorktreeLease(dest);
319
216
  return { ok: true, path: dest, ...(preservedLeftover !== undefined ? { preservedLeftover } : {}) };
320
217
  });
321
218
  }
322
219
  /**
323
- * Post-unit cleanup: remove the worktree when `git status --porcelain` shows
324
- * it clean; retain it (dirty: true) when the unit left uncommitted work —
325
- * the caller logs the retained path. Any git failure retains the worktree
326
- * too (never destroy a tree whose state could not be verified).
327
- *
328
- * The probe deliberately omits `--ignored`: a worktree whose only residue is
329
- * files matched by the base repo's `.gitignore` (build artifacts, caches,
330
- * logs, `node_modules`) probes clean and IS removed. Those files are disposable
331
- * by the repo's own declaration; retaining a worktree per build/install would
332
- * blow up disk. "Uncollected work" the caller preserves is therefore
333
- * tracked-or-untracked-unignored changes only (module doc).
334
- *
335
- * Only `git worktree remove` takes the base repo's lock; the status probe stays
336
- * OFF {@link withRepoWorktreeLock}. Since the probe now runs only when a
337
- * removal was refused, a dirty worktree costs one failed removal inside the
338
- * lock that it used to avoid — the trade that makes every CLEAN cleanup a
339
- * single git process.
220
+ * Post-unit cleanup: remove a clean worktree; retain (dirty: true) one with
221
+ * uncommitted work or whose state could not be verified. `.gitignore`d files do
222
+ * not count as work.
340
223
  */
341
224
  export async function cleanupUnitWorktree(baseDir, worktreePath) {
342
- // Try the removal FIRST and let it be the cleanliness check: `git worktree
343
- // remove` without `--force` already refuses a worktree carrying changes, on
344
- // the same terms as the probe (ignored files excluded either way). The clean
345
- // case — the overwhelmingly common one — is then ONE git process per unit
346
- // instead of two, which a wide fan-out pays per unit.
225
+ // The removal (without --force) is itself the cleanliness check: one git process per clean unit.
347
226
  const removed = await withRepoWorktreeLock(await safeRealpathAsync(baseDir), () => git(baseDir, ["worktree", "remove", worktreePath]));
348
227
  if (removed.ok) {
349
228
  await removeRunRootIfEmpty(worktreePath);
350
229
  return { removed: true, dirty: false };
351
230
  }
352
- // It refused, so the tree stays on disk — drop its lease, since no unit is
353
- // using it any more and the sweep must be free to collect it once it is
354
- // stale. (A successful removal took the whole admin directory, lease with it.)
355
- await releaseWorktreeLease(worktreePath);
356
- // Ask the probe WHY it refused rather than parsing git's message, whose
357
- // wording varies with version and locale — and which the caller's warn text
358
- // has never been written against.
231
+ // It refused, so the tree stays on disk. Ask the probe WHY it refused rather
232
+ // than parsing git's message, whose wording varies with version and locale —
233
+ // and which the caller's warn text has never been written against.
359
234
  const status = await git(worktreePath, ["status", "--porcelain"]);
360
235
  if (!status.ok) {
361
236
  return { removed: false, dirty: false, error: status.error };
@@ -365,100 +240,8 @@ export async function cleanupUnitWorktree(baseDir, worktreePath) {
365
240
  }
366
241
  return { removed: false, dirty: false, error: removed.error };
367
242
  }
368
- // ── Liveness leases ─────────────────────────────────────────────────────────
369
- /** Marker file, inside a worktree's git admin dir, naming the process using it. */
370
- const LEASE_FILE_NAME = "akm-lease";
371
- /**
372
- * Path of `p`'s git administrative directory (`<repo>/.git/worktrees/<name>`),
373
- * read from the `.git` FILE every linked worktree carries. Undefined when `p`
374
- * is not a readable linked worktree.
375
- */
376
- async function worktreeAdminDir(p) {
377
- let contents;
378
- try {
379
- contents = await fsp.readFile(path.join(p, ".git"), "utf8");
380
- }
381
- catch {
382
- return undefined;
383
- }
384
- const gitdir = /^gitdir:[ \t]*(\S.*)$/m.exec(contents)?.[1];
385
- return gitdir?.trim();
386
- }
387
- /**
388
- * Record this process as the user of `worktreePath`, so {@link
389
- * sweepStaleWorktrees} can tell a live worktree from an abandoned one.
390
- *
391
- * The marker lives in git's administrative directory for the worktree, never in
392
- * the checkout: an untracked file inside the tree would make it probe DIRTY (and
393
- * be retained forever), while git's own `worktree remove`/`prune` delete the
394
- * admin dir — lease included — with no extra bookkeeping here. Best effort: a
395
- * lease that cannot be written only leaves the tree collectible once stale,
396
- * which is the pre-lease behaviour.
397
- */
398
- async function acquireWorktreeLease(worktreePath) {
399
- const adminDir = await worktreeAdminDir(worktreePath);
400
- if (adminDir === undefined)
401
- return;
402
- const lease = {
403
- pid: process.pid,
404
- host: os.hostname(),
405
- path: await safeRealpathAsync(worktreePath),
406
- };
407
- try {
408
- await fsp.writeFile(path.join(adminDir, LEASE_FILE_NAME), JSON.stringify(lease));
409
- }
410
- catch {
411
- /* best effort — see above */
412
- }
413
- }
414
- /** Drop the lease of a worktree this process is done with but is not removing. */
415
- async function releaseWorktreeLease(worktreePath) {
416
- const adminDir = await worktreeAdminDir(worktreePath);
417
- if (adminDir === undefined)
418
- return;
419
- try {
420
- await fsp.rm(path.join(adminDir, LEASE_FILE_NAME), { force: true });
421
- }
422
- catch {
423
- /* best effort — a stale lease only delays the sweep by one run of it */
424
- }
425
- }
426
- /**
427
- * True when a still-running process holds `candidate`'s lease — the guard age
428
- * cannot provide. A unit that runs longer than the sweep threshold while
429
- * writing only inside subdirectories leaves the worktree ROOT's mtime at
430
- * creation time, so another akm process minting a worktree would otherwise
431
- * delete a tree that is still in use.
432
- *
433
- * A lease from a dead pid, from another host (where the pid means nothing), or
434
- * for a different path is NOT liveness: crashed runs and retained dirty trees
435
- * stay collectible, which is the whole point of the sweep.
436
- */
437
- async function isWorktreeLeaseLive(candidate) {
438
- const adminDir = await worktreeAdminDir(candidate);
439
- if (adminDir === undefined)
440
- return false;
441
- let lease;
442
- try {
443
- lease = JSON.parse(await fsp.readFile(path.join(adminDir, LEASE_FILE_NAME), "utf8"));
444
- }
445
- catch {
446
- return false;
447
- }
448
- if (lease.host !== os.hostname())
449
- return false;
450
- if (lease.path !== (await safeRealpathAsync(candidate)))
451
- return false;
452
- return isProcessAlive(lease.pid);
453
- }
454
243
  // ── Garbage collection ──────────────────────────────────────────────────────
455
- /**
456
- * Drop the run-scoped root once its last unit worktree is gone. `rmdir`
457
- * refuses a non-empty directory, so a run that retained a dirty worktree (or
458
- * a `.retained-<ts>` copy) keeps its root and its forensic contents; only a
459
- * fully drained root disappears. Never touches anything that is not a DIRECT
460
- * child of the worktrees root.
461
- */
244
+ /** Drop the run-scoped root once empty (`rmdir` keeps any retained tree). */
462
245
  async function removeRunRootIfEmpty(worktreePath) {
463
246
  const root = worktreesRoot();
464
247
  const runRoot = path.dirname(path.resolve(worktreePath));
@@ -480,25 +263,10 @@ async function removeRunRootIfEmpty(worktreePath) {
480
263
  prunedRuns.delete(path.basename(runRoot));
481
264
  }
482
265
  /**
483
- * Age-based GC of the worktrees root. Removes `<root>/<runId>/<entry>`
484
- * directories whose last activity is older than `maxAgeMs` — orphaned
485
- * worktrees from crashed runs AND deliberately retained dirty trees, because
486
- * the age threshold is exactly what makes discarding forensic state
487
- * acceptable. A run root is dropped once it is empty and itself stale (or
488
- * this sweep just emptied it), so a live run whose first worktree is mid-`add`
489
- * is never pulled out from under git.
490
- *
491
- * Safety invariants: it only ever descends two levels from `root`; entries
492
- * that are not real directories (symlinks included — `Dirent.isDirectory()`
493
- * reflects `lstat`) are skipped, never followed; a stale-looking candidate
494
- * whose {@link isWorktreeLeaseLive} lease holder is still running is skipped
495
- * (age alone cannot see a unit in flight in another process); and every
496
- * candidate is re-verified with {@link isWithin} against the resolved root
497
- * before removal.
498
- * Deleting a directory leaves its registration in whatever base repo minted
499
- * it; the next run's `git worktree prune` on that repo reaps it.
500
- *
501
- * Returns the paths removed. Never throws.
266
+ * Age-based GC of the worktrees root: removes `<root>/<runId>/<entry>`
267
+ * directories older than `maxAgeMs`, and empty stale run roots. Descends two
268
+ * levels only, never follows a symlink, and re-checks containment before each
269
+ * removal. Returns the paths removed; never throws.
502
270
  */
503
271
  export async function sweepStaleWorktrees(opts = {}) {
504
272
  const root = path.resolve(opts.root ?? worktreesRoot());
@@ -536,8 +304,6 @@ export async function sweepStaleWorktrees(opts = {}) {
536
304
  continue;
537
305
  if (now - (await lastActivityMs(candidate, entry.name)) < maxAgeMs)
538
306
  continue;
539
- if (await isWorktreeLeaseLive(candidate))
540
- continue;
541
307
  try {
542
308
  await fsp.rm(candidate, { recursive: true, force: true });
543
309
  removed.push(candidate);
@@ -2,21 +2,10 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * The freeze-time child-output reference check (P3b, spec docs/plans/specs/
6
- * p3b-child-executor.md §4.4, rows B-28…B-32).
7
- *
8
- * A parent step may read `steps.<child>.output(.<name>)*` where step
9
- * `<child>`'s frozen target is `kind: "child-workflow"`. Only the FIRST path
10
- * segment is a freeze-time concern: it must name one of the child's declared
11
- * `outputs:` names, or — when the child declares none — `runId` or `status`
12
- * (the default `workflowRunExportedResult` shape, `runtime/run-outputs.ts`).
13
- * A reference AT `steps.<child>.output` with no further segment always
14
- * accepts — it names the whole exported object. Anything deeper resolves (and,
15
- * if wrong, fails) at pre-attempt through the existing, unchanged resolver —
16
- * the value's shape past the first segment is unconstrained unless the
17
- * output declares a `schema:`.
18
- *
19
- * Pure over the frozen step list — no IO, no config.
5
+ * Freeze-time check of references into a child-workflow step's output: the
6
+ * first path segment after `steps.<child>.output` must name one of the child's
7
+ * declared `outputs:` (or `runId`/`status` when it declares none). Deeper
8
+ * segments resolve at run time.
20
9
  */
21
10
  import { UsageError } from "../../core/errors.js";
22
11
  import { formatReference, parseReference } from "../program/expressions.js";