akm-cli 0.9.31 → 0.10.261002-beta

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 (270) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/STABILITY.md +99 -57
  3. package/dist/api-node.mjs +20 -0
  4. package/dist/api.d.ts +23 -0
  5. package/dist/api.js +45 -0
  6. package/dist/assets/hints/cli-hints-full.md +15 -29
  7. package/dist/assets/hints/cli-hints-short.md +2 -2
  8. package/dist/assets/improve-strategies/default.json +3 -3
  9. package/dist/assets/prompts/consolidate-pair.md +9 -7
  10. package/dist/assets/stash-skeleton/README.md +1 -1
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
  12. package/dist/cli/shared.js +31 -48
  13. package/dist/cli/unknown-flags.js +6 -43
  14. package/dist/cli.js +8 -34
  15. package/dist/commands/agent/contribute-cli.js +1 -1
  16. package/dist/commands/completions.js +1 -2
  17. package/dist/commands/config-cli.js +5 -28
  18. package/dist/commands/env/env-cli.js +5 -5
  19. package/dist/commands/env/secret-cli.js +2 -2
  20. package/dist/commands/feedback-cli.js +75 -49
  21. package/dist/commands/health/checks.js +41 -16
  22. package/dist/commands/health/plugin-staleness.js +52 -22
  23. package/dist/commands/improve/autonomy-gate.js +5 -3
  24. package/dist/commands/improve/consolidate/chunking.js +3 -10
  25. package/dist/commands/improve/consolidate/continuity-check.js +4 -4
  26. package/dist/commands/improve/consolidate/pair-pass.js +29 -10
  27. package/dist/commands/improve/consolidate.js +10 -65
  28. package/dist/commands/improve/distill/lesson-update.js +91 -0
  29. package/dist/commands/improve/distill-guards.js +1 -43
  30. package/dist/commands/improve/distill.js +148 -21
  31. package/dist/commands/improve/eligibility.js +14 -1
  32. package/dist/commands/improve/extract-cli.js +1 -1
  33. package/dist/commands/improve/extract-prompt.js +17 -7
  34. package/dist/commands/improve/extract.js +5 -7
  35. package/dist/commands/improve/improve-cli.js +92 -106
  36. package/dist/commands/improve/improve-strategies.js +8 -12
  37. package/dist/commands/improve/improve.js +134 -52
  38. package/dist/commands/improve/loop-stages.js +18 -5
  39. package/dist/commands/improve/planner.js +0 -1
  40. package/dist/commands/improve/preparation.js +40 -99
  41. package/dist/commands/improve/proactive-maintenance.js +2 -2
  42. package/dist/commands/improve/reflect-noise.js +3 -55
  43. package/dist/commands/improve/reflect.js +29 -34
  44. package/dist/commands/improve/retrieval-queries.js +37 -0
  45. package/dist/commands/improve/retrieval-scope.js +2 -2
  46. package/dist/commands/improve/salience.js +2 -9
  47. package/dist/commands/improve/session-asset.js +4 -1
  48. package/dist/commands/improve/stage.js +23 -12
  49. package/dist/commands/lint/index.js +5 -20
  50. package/dist/commands/observability-cli.js +3 -9
  51. package/dist/commands/proposal/drain.js +43 -6
  52. package/dist/commands/proposal/proposal-cli.js +11 -31
  53. package/dist/commands/proposal/repository.js +20 -14
  54. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -3
  55. package/dist/commands/read/knowledge.js +3 -3
  56. package/dist/commands/read/remember-cli.js +0 -13
  57. package/dist/commands/read/search-cli.js +29 -97
  58. package/dist/commands/read/search.js +0 -11
  59. package/dist/commands/read/show.js +0 -19
  60. package/dist/commands/sources/plugin-upgrade.js +164 -101
  61. package/dist/commands/sources/sources-cli.js +1 -15
  62. package/dist/commands/sources/stash-cli.js +6 -14
  63. package/dist/commands/tasks/tasks-cli.js +4 -115
  64. package/dist/commands/workflow/plan.js +11 -33
  65. package/dist/commands/workflow-cli.js +2 -7
  66. package/dist/core/adapter/adapter-ids.js +1 -2
  67. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  68. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  69. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  70. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  71. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  72. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  73. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  74. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  75. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  76. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  77. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  78. package/dist/core/adapter/recognize-match.js +3 -4
  79. package/dist/core/common.js +18 -2
  80. package/dist/core/config/config-schema.js +12 -15
  81. package/dist/core/config/config-types.js +6 -15
  82. package/dist/core/config/config-walker.js +6 -23
  83. package/dist/core/config/config.js +17 -80
  84. package/dist/core/config/engine-semantics.js +7 -3
  85. package/dist/core/config/schema/engines.js +7 -7
  86. package/dist/core/config/schema/improve-processes.js +10 -63
  87. package/dist/core/config/schema/improve.js +2 -21
  88. package/dist/core/config/schema/index-config.js +6 -39
  89. package/dist/core/env-secret-ref.js +2 -45
  90. package/dist/core/errors.js +1 -2
  91. package/dist/core/extra-params.js +2 -86
  92. package/dist/core/file-lock.js +2 -6
  93. package/dist/core/improve-result.js +13 -1
  94. package/dist/core/mutation-target.js +2 -2
  95. package/dist/core/paths.js +4 -0
  96. package/dist/core/recognition-util.js +4 -5
  97. package/dist/core/run-lock.js +1 -1
  98. package/dist/core/subprocess.js +49 -0
  99. package/dist/core/warn.js +21 -4
  100. package/dist/core/write-source.js +3 -3
  101. package/dist/execution/target-ref.js +1 -2
  102. package/dist/indexer/indexer.js +15 -6
  103. package/dist/indexer/passes/memory-inference.js +15 -6
  104. package/dist/indexer/search/db-search.js +49 -36
  105. package/dist/indexer/search/fts-query.js +0 -19
  106. package/dist/indexer/search/ranking.js +29 -1
  107. package/dist/indexer/usage/usage-events.js +21 -16
  108. package/dist/integrations/agent/engine-resolution.js +5 -2
  109. package/dist/integrations/agent/index.js +1 -1
  110. package/dist/integrations/agent/profiles.js +41 -98
  111. package/dist/integrations/agent/request-lowering.js +3 -3
  112. package/dist/integrations/harnesses/aider/index.js +5 -0
  113. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  114. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  115. package/dist/integrations/harnesses/claude/index.js +5 -0
  116. package/dist/integrations/harnesses/codex/index.js +1 -0
  117. package/dist/integrations/harnesses/copilot/index.js +1 -0
  118. package/dist/integrations/harnesses/gemini/index.js +1 -0
  119. package/dist/integrations/harnesses/index.js +18 -39
  120. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  121. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  122. package/dist/integrations/harnesses/opencode/index.js +22 -0
  123. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  124. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  125. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  126. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  127. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  128. package/dist/integrations/harnesses/opencode/version.js +98 -0
  129. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  130. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  131. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  132. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +254 -0
  133. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  134. package/dist/integrations/harnesses/openhands/index.js +1 -0
  135. package/dist/integrations/harnesses/pi/index.js +1 -0
  136. package/dist/integrations/harnesses/shared.js +2 -0
  137. package/dist/integrations/harnesses/types.js +2 -0
  138. package/dist/llm/client.js +41 -11
  139. package/dist/output/cli-hints.js +1 -1
  140. package/dist/output/context.js +10 -19
  141. package/dist/output/format-exempt.js +5 -8
  142. package/dist/output/shapes/helpers.js +1 -22
  143. package/dist/output/shapes/passthrough.js +0 -1
  144. package/dist/output/shapes.js +3 -31
  145. package/dist/output/stdout.js +5 -1
  146. package/dist/output/text/command-format.js +6 -1
  147. package/dist/output/text/workflow-format.js +4 -22
  148. package/dist/scripts/akm-migrate-node.js +15745 -16711
  149. package/dist/scripts/akm-migrate.js +16318 -17280
  150. package/dist/setup/detect.js +10 -4
  151. package/dist/setup/harness-config-import.js +5 -14
  152. package/dist/setup/setup.js +1 -1
  153. package/dist/setup/steps/tasks.js +12 -13
  154. package/dist/sources/providers/git-stash.js +113 -16
  155. package/dist/storage/repositories/index-utility-repository.js +2 -32
  156. package/dist/storage/repositories/index-vec-repository.js +4 -0
  157. package/dist/storage/repositories/task-history-repository.js +0 -5
  158. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  159. package/dist/tasks/embedded.js +2 -1
  160. package/dist/tasks/run/run-command-task.js +9 -1
  161. package/dist/tasks/run/run-workflow-task.js +4 -3
  162. package/dist/tasks/run/task-history.js +0 -2
  163. package/dist/tasks/run/task-log.js +38 -1
  164. package/dist/tasks/schedule-gates.js +39 -0
  165. package/dist/tasks/scheduler-binding.js +0 -16
  166. package/dist/tasks/scheduler-sync.js +3 -97
  167. package/dist/tasks/source/bounded-document.js +1 -1
  168. package/dist/tasks/source/task-source-v4.js +2 -12
  169. package/dist/tasks/task-run-reserved-flags.js +2 -31
  170. package/dist/workflows/authoring/authoring.js +6 -6
  171. package/dist/workflows/compile.js +8 -65
  172. package/dist/workflows/exec/child-workflow.js +22 -3
  173. package/dist/workflows/exec/native-executor.js +12 -147
  174. package/dist/workflows/exec/run-workflow.js +31 -103
  175. package/dist/workflows/exec/step-work.js +9 -178
  176. package/dist/workflows/exec/unit-dispatch.js +0 -10
  177. package/dist/workflows/freeze/environment.js +3 -44
  178. package/dist/workflows/freeze/freeze.js +8 -41
  179. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  180. package/dist/workflows/freeze/targets/command.js +2 -16
  181. package/dist/workflows/freeze/targets/shell.js +0 -2
  182. package/dist/workflows/freeze/task-bindings.js +2 -28
  183. package/dist/workflows/ir/params.js +12 -2
  184. package/dist/workflows/parser.js +82 -232
  185. package/dist/workflows/plan.js +1 -1
  186. package/dist/workflows/renderer.js +10 -19
  187. package/dist/workflows/runtime/run-plan.js +11 -53
  188. package/dist/workflows/runtime/runs.js +4 -27
  189. package/dist/workflows/source-files.js +12 -16
  190. package/dist/workflows/source-semantics.js +1 -93
  191. package/docs/README.md +1 -1
  192. package/docs/agents/README.md +3 -3
  193. package/docs/agents/agent-install.md +1 -1
  194. package/docs/architecture/0.10-process-rules.md +827 -0
  195. package/docs/architecture/adapters.md +17 -31
  196. package/docs/architecture/architecture.md +4 -5
  197. package/docs/architecture/improvement.md +115 -76
  198. package/docs/architecture/internals/improve-workflow.md +22 -16
  199. package/docs/architecture/internals/indexing.md +1 -1
  200. package/docs/architecture/internals/search.md +1 -2
  201. package/docs/architecture/internals/storage-locations.md +6 -6
  202. package/docs/architecture/persisted-data-compat.md +13 -4
  203. package/docs/architecture/specs/0.9.0-decisions.md +6 -2
  204. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
  205. package/docs/architecture/specs/workflow-format-unification.md +1 -6
  206. package/docs/architecture/testing/manual-testing-checklist.md +21 -30
  207. package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
  208. package/docs/architecture/testing/testing-workflow.md +1 -1
  209. package/docs/architecture/workflow-engine.md +5 -56
  210. package/docs/guides/author-workflows.md +39 -69
  211. package/docs/guides/bundles.md +3 -1
  212. package/docs/guides/capture-knowledge.md +1 -1
  213. package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
  214. package/docs/guides/discover-and-load.md +4 -4
  215. package/docs/guides/getting-started.md +1 -1
  216. package/docs/guides/improve-the-library.md +10 -9
  217. package/docs/guides/recipes/headless-install.md +2 -2
  218. package/docs/guides/run-workflows.md +1 -1
  219. package/docs/guides/scheduling.md +9 -1
  220. package/docs/guides/use-with-any-agent.md +49 -4
  221. package/docs/integration/bundling-akm.md +7 -6
  222. package/docs/maintainers/curate-workmap.md +1 -2
  223. package/docs/maintainers/release-checklist.md +33 -1
  224. package/docs/migration/README.md +2 -0
  225. package/docs/migration/release-notes/0.10.0.md +201 -0
  226. package/docs/migration/release-notes/README.md +1 -0
  227. package/docs/migration/v0.9-to-v0.10.md +507 -0
  228. package/docs/posts/akm-command-tour.md +3 -3
  229. package/docs/product-surface.md +1 -1
  230. package/docs/reference/README.md +1 -0
  231. package/docs/reference/api.md +65 -0
  232. package/docs/reference/asset-types.md +3 -5
  233. package/docs/reference/bundle-types.md +2 -4
  234. package/docs/reference/cli.md +160 -188
  235. package/docs/reference/configuration.md +101 -20
  236. package/docs/reference/data-and-telemetry.md +2 -2
  237. package/docs/reference/env-and-secrets.md +2 -2
  238. package/docs/reference/supported-formats.md +3 -4
  239. package/docs/reference/tasks.md +12 -19
  240. package/docs/reference/website-sources.md +1 -1
  241. package/docs/reference/workflow-schema.md +89 -317
  242. package/docs/reference/workflows.md +20 -48
  243. package/package.json +12 -2
  244. package/schemas/akm-config.json +2 -243
  245. package/schemas/akm-task.json +2 -3
  246. package/schemas/akm-workflow.json +47 -84
  247. package/dist/assets/improve-strategies/catchup.json +0 -41
  248. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  249. package/dist/assets/improve-strategies/quick.json +0 -17
  250. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  251. package/dist/assets/improve-strategies/thorough.json +0 -49
  252. package/dist/assets/prompts/retrieval-relevance-judge.md +0 -6
  253. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  254. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  255. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  256. package/dist/cli/retired-commands.js +0 -118
  257. package/dist/commands/improve/retrieval-gate.js +0 -132
  258. package/dist/commands/tasks/validate.js +0 -176
  259. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  260. package/dist/indexer/feedback/utility-policy.js +0 -75
  261. package/dist/integrations/harnesses/ids.js +0 -28
  262. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  263. package/dist/tasks/source/task-to-v4.js +0 -863
  264. package/dist/workflows/exec/worktree.js +0 -372
  265. package/dist/workflows/freeze/child-output-references.js +0 -83
  266. package/dist/workflows/freeze/identity.js +0 -22
  267. package/dist/workflows/freeze/targets/script.js +0 -50
  268. package/dist/workflows/freeze/targets/task.js +0 -124
  269. package/dist/workflows/github-yaml.js +0 -506
  270. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -1,372 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
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>`.
8
- *
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.
17
- *
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`).
22
- */
23
- import { spawnSync } from "node:child_process";
24
- import fsp from "node:fs/promises";
25
- import os from "node:os";
26
- import path from "node:path";
27
- import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
28
- import { serializeByKey } from "../../core/concurrent.js";
29
- import { runManagedSubprocess } from "../../core/subprocess.js";
30
- import { warn } from "../../core/warn.js";
31
- /** Timeout for each worktree git call (#891: a busy machine can push a healthy one past 30s). */
32
- const GIT_TIMEOUT_MS = 120_000;
33
- /** Directory under `os.tmpdir()` that owns every run's worktree roots. */
34
- export const WORKTREES_DIR_NAME = "akm-worktrees";
35
- /** Age after which an orphaned or retained worktree is swept: long enough to triage a failed run. */
36
- export const STALE_WORKTREE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
37
- function gitExitError(args, code, stderr, stdout) {
38
- const detail = (stderr || stdout || "").trim();
39
- return `git ${args.join(" ")} exited ${code}${detail ? `: ${detail}` : ""}`;
40
- }
41
- /**
42
- * Run one git command asynchronously via the real `git` binary; `ok` = exit
43
- * 0. Never throws (spawn errors and the timeout → ok: false).
44
- */
45
- async function realGitExecutor(cwd, args) {
46
- const result = await runManagedSubprocess(["git", "-C", cwd, ...args], {
47
- capture: true,
48
- timeoutMs: GIT_TIMEOUT_MS,
49
- });
50
- if (result.spawnError) {
51
- return { ok: false, stdout: "", error: `git ${args[0]} failed to spawn: ${result.spawnError.message}` };
52
- }
53
- if (result.timedOut) {
54
- return { ok: false, stdout: result.stdout, error: `git ${args.join(" ")} timed out after ${GIT_TIMEOUT_MS}ms` };
55
- }
56
- if (result.exitCode !== 0) {
57
- return {
58
- ok: false,
59
- stdout: result.stdout,
60
- error: gitExitError(args, result.exitCode, result.stderr, result.stdout),
61
- };
62
- }
63
- return { ok: true, stdout: result.stdout };
64
- }
65
- let gitExecutor = realGitExecutor;
66
- /** Test seam: swap the git executor for repo-mutating calls (`undefined` restores the real one). */
67
- export function setGitExecutorForTesting(executor) {
68
- gitExecutor = executor ?? realGitExecutor;
69
- }
70
- /**
71
- * Run one git command asynchronously; `ok` = exit 0. Never throws. Async so a
72
- * git lock wait parks a promise instead of blocking the event loop;
73
- * repo-mutating callers must hold {@link withRepoWorktreeLock}.
74
- */
75
- async function git(cwd, args) {
76
- return gitExecutor(cwd, args);
77
- }
78
- /**
79
- * Synchronous git for the two read-only probes that run before any unit is in
80
- * flight ({@link isGitAvailable}, {@link assertGitWorkTree}). They take no
81
- * repository lock, so blocking here cannot stall another unit's git call.
82
- */
83
- function gitSync(cwd, args) {
84
- const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: GIT_TIMEOUT_MS });
85
- if (result.error) {
86
- return { ok: false, stdout: "", error: `git ${args[0]} failed to spawn: ${result.error.message}` };
87
- }
88
- if (result.status !== 0) {
89
- return {
90
- ok: false,
91
- stdout: result.stdout ?? "",
92
- error: gitExitError(args, result.status, result.stderr ?? "", result.stdout ?? ""),
93
- };
94
- }
95
- return { ok: true, stdout: result.stdout ?? "" };
96
- }
97
- /** True when a usable `git` binary is on PATH (tests skip gracefully without one). */
98
- export function isGitAvailable() {
99
- const result = spawnSync("git", ["--version"], { encoding: "utf8", timeout: 5_000 });
100
- return !result.error && result.status === 0;
101
- }
102
- /**
103
- * Preflight for worktree isolation: `dir` must be inside a git work tree.
104
- * Returns an error message (for a clean step failure) or undefined when ok.
105
- * A missing git binary reports as the same clean failure — a workflow that
106
- * declares isolation cannot run without git.
107
- */
108
- export function assertGitWorkTree(dir) {
109
- const result = gitSync(dir, ["rev-parse", "--is-inside-work-tree"]);
110
- if (!result.ok) {
111
- return `"${dir}" is not a git repository (isolation: worktree requires one): ${result.error}`;
112
- }
113
- if (result.stdout.trim() !== "true") {
114
- return `"${dir}" is not inside a git work tree (isolation: worktree requires one).`;
115
- }
116
- return undefined;
117
- }
118
- // ── Per-repository serialization ────────────────────────────────────────────
119
- /** In-flight tail of each base repository's serialized git-worktree chain. */
120
- const repoOperationTails = new Map();
121
- /** Base repos already pruned in this process, per run id (dropped with the run's drained root). */
122
- const prunedRuns = new Map();
123
- /**
124
- * Serialize `fn` against every other repo-mutating worktree operation on the
125
- * same base repository ({@link serializeByKey}). Keyed by the RESOLVED repo
126
- * path so two spellings of one repo (symlinked tmpdir, relative cwd) share a
127
- * chain. A failure rejects its own caller but never wedges the chain.
128
- */
129
- function withRepoWorktreeLock(repoKey, fn) {
130
- return serializeByKey(repoOperationTails, repoKey, fn);
131
- }
132
- /** Journal-safe directory name for a unit attempt id (ids carry `:` / `~`). */
133
- function sanitizeAttemptId(attemptId) {
134
- return attemptId.replace(/[^A-Za-z0-9._-]/g, "-");
135
- }
136
- /** Parent directory of every run's worktree root (`<tmp>/akm-worktrees`). */
137
- export function worktreesRoot() {
138
- return path.join(os.tmpdir(), WORKTREES_DIR_NAME);
139
- }
140
- /** Run-scoped parent directory for all of one run's unit worktrees. */
141
- export function runWorktreeRoot(runId) {
142
- return path.join(worktreesRoot(), runId);
143
- }
144
- /**
145
- * Move a leftover attempt directory aside to `<dest>.retained-<ts>[-n]`
146
- * (never overwriting an earlier retained copy). Throws on fs errors — the
147
- * caller maps them onto its result object.
148
- */
149
- async function moveLeftoverAside(dest) {
150
- const base = `${dest}.retained-${Date.now()}`;
151
- let aside = base;
152
- for (let n = 1; await pathExists(aside); n++)
153
- aside = `${base}-${n}`;
154
- await fsp.rename(dest, aside);
155
- return aside;
156
- }
157
- async function pathExists(p) {
158
- return fsp.access(p).then(() => true, () => false);
159
- }
160
- /**
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}.
165
- */
166
- export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
167
- // Opportunistic, at most once per process, never awaited — GC must never sit
168
- // on the dispatch path.
169
- sweepStaleWorktreesOnce();
170
- const repoKey = await safeRealpathAsync(baseDir);
171
- const dest = path.join(runWorktreeRoot(runId), sanitizeAttemptId(attemptId));
172
- return withRepoWorktreeLock(repoKey, async () => {
173
- let preservedLeftover;
174
- let leftoverHandled = false;
175
- try {
176
- if (await pathExists(dest)) {
177
- const status = await git(dest, ["status", "--porcelain"]);
178
- if (status.ok && status.stdout.trim() === "") {
179
- // Async on purpose: a recursive delete of a whole leftover checkout
180
- // inside this critical section would otherwise block the event loop
181
- // (every other in-flight unit, abort handling).
182
- await fsp.rm(dest, { recursive: true, force: true });
183
- }
184
- else {
185
- preservedLeftover = await moveLeftoverAside(dest);
186
- }
187
- leftoverHandled = true;
188
- }
189
- await fsp.mkdir(path.dirname(dest), { recursive: true });
190
- }
191
- catch (err) {
192
- return {
193
- ok: false,
194
- error: `could not prepare worktree directory ${dest}: ${message(err)}`,
195
- ...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
196
- };
197
- }
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.
200
- const prunedRepos = prunedRuns.get(runId);
201
- if (leftoverHandled || !prunedRepos?.has(repoKey)) {
202
- if (prunedRepos)
203
- prunedRepos.add(repoKey);
204
- else
205
- prunedRuns.set(runId, new Set([repoKey]));
206
- await git(baseDir, ["worktree", "prune"]);
207
- }
208
- const added = await git(baseDir, ["worktree", "add", "--detach", dest, ...(commitOid ? [commitOid] : [])]);
209
- if (!added.ok) {
210
- return {
211
- ok: false,
212
- error: `could not create isolation worktree at ${dest}: ${added.error}`,
213
- ...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
214
- };
215
- }
216
- return { ok: true, path: dest, ...(preservedLeftover !== undefined ? { preservedLeftover } : {}) };
217
- });
218
- }
219
- /**
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.
223
- */
224
- export async function cleanupUnitWorktree(baseDir, worktreePath) {
225
- // The removal (without --force) is itself the cleanliness check: one git process per clean unit.
226
- const removed = await withRepoWorktreeLock(await safeRealpathAsync(baseDir), () => git(baseDir, ["worktree", "remove", worktreePath]));
227
- if (removed.ok) {
228
- await removeRunRootIfEmpty(worktreePath);
229
- return { removed: true, dirty: false };
230
- }
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.
234
- const status = await git(worktreePath, ["status", "--porcelain"]);
235
- if (!status.ok) {
236
- return { removed: false, dirty: false, error: status.error };
237
- }
238
- if (status.stdout.trim() !== "") {
239
- return { removed: false, dirty: true };
240
- }
241
- return { removed: false, dirty: false, error: removed.error };
242
- }
243
- // ── Garbage collection ──────────────────────────────────────────────────────
244
- /** Drop the run-scoped root once empty (`rmdir` keeps any retained tree). */
245
- async function removeRunRootIfEmpty(worktreePath) {
246
- const root = worktreesRoot();
247
- const runRoot = path.dirname(path.resolve(worktreePath));
248
- if (!(await isWithinAsync(runRoot, root)))
249
- return;
250
- if ((await safeRealpathAsync(path.dirname(runRoot))) !== (await safeRealpathAsync(root)))
251
- return;
252
- try {
253
- await fsp.rmdir(runRoot);
254
- }
255
- catch {
256
- // ENOTEMPTY (retained work) / ENOENT (already gone) — both fine.
257
- return;
258
- }
259
- // The run's worktrees are fully drained — drop its prune bookkeeping so
260
- // `prunedRuns` never outgrows the live runs. (A later worktree of the same
261
- // run simply prunes once more; the guard is an optimization, not a
262
- // correctness gate.)
263
- prunedRuns.delete(path.basename(runRoot));
264
- }
265
- /**
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.
270
- */
271
- export async function sweepStaleWorktrees(opts = {}) {
272
- const root = path.resolve(opts.root ?? worktreesRoot());
273
- const removed = [];
274
- if (path.basename(root) !== WORKTREES_DIR_NAME)
275
- return removed;
276
- const maxAgeMs = opts.maxAgeMs ?? STALE_WORKTREE_MAX_AGE_MS;
277
- const now = opts.now ?? Date.now();
278
- let runRoots;
279
- try {
280
- runRoots = await fsp.readdir(root, { withFileTypes: true, encoding: "utf8" });
281
- }
282
- catch {
283
- return removed; // No root yet (or unreadable) — nothing to sweep.
284
- }
285
- for (const runRootEntry of runRoots) {
286
- if (!runRootEntry.isDirectory())
287
- continue;
288
- const runRoot = path.join(root, runRootEntry.name);
289
- if (!(await isWithinAsync(runRoot, root)))
290
- continue;
291
- let entries;
292
- try {
293
- entries = await fsp.readdir(runRoot, { withFileTypes: true, encoding: "utf8" });
294
- }
295
- catch {
296
- continue;
297
- }
298
- let emptiedHere = false;
299
- for (const entry of entries) {
300
- if (!entry.isDirectory())
301
- continue;
302
- const candidate = path.join(runRoot, entry.name);
303
- if (!(await isWithinAsync(candidate, root)))
304
- continue;
305
- if (now - (await lastActivityMs(candidate, entry.name)) < maxAgeMs)
306
- continue;
307
- try {
308
- await fsp.rm(candidate, { recursive: true, force: true });
309
- removed.push(candidate);
310
- emptiedHere = true;
311
- }
312
- catch {
313
- /* leave it for the next sweep */
314
- }
315
- }
316
- try {
317
- if ((await fsp.readdir(runRoot)).length > 0)
318
- continue;
319
- if (!emptiedHere && now - (await lastActivityMs(runRoot, runRootEntry.name)) < maxAgeMs)
320
- continue;
321
- await fsp.rmdir(runRoot);
322
- removed.push(runRoot);
323
- }
324
- catch {
325
- /* leave it for the next sweep */
326
- }
327
- }
328
- return removed;
329
- }
330
- /**
331
- * Newest evidence of activity for `p`: its mtime, or the timestamp embedded in
332
- * a `.retained-<ts>[-n]` name when that is newer. Taking the max is the
333
- * conservative direction — a sweep never deletes something that looks recent
334
- * by either measure. An unstattable entry reports as "now" so it survives.
335
- */
336
- async function lastActivityMs(p, name) {
337
- let mtimeMs;
338
- try {
339
- mtimeMs = (await fsp.stat(p)).mtimeMs;
340
- }
341
- catch {
342
- return Date.now();
343
- }
344
- const stamped = /\.retained-(\d{10,})(?:-\d+)?$/.exec(name);
345
- return stamped ? Math.max(mtimeMs, Number(stamped[1])) : mtimeMs;
346
- }
347
- let sweepStarted = false;
348
- /**
349
- * Kick off the GC sweep at most once per process, fire-and-forget. Called from
350
- * {@link createUnitWorktree} so the cost is paid by a run that is already
351
- * doing worktree work, and never awaited so dispatch does not wait on it.
352
- */
353
- function sweepStaleWorktreesOnce() {
354
- if (sweepStarted)
355
- return;
356
- sweepStarted = true;
357
- void sweepStaleWorktrees()
358
- .then((removed) => {
359
- if (removed.length === 0)
360
- return;
361
- const shown = removed.slice(0, 10).join(", ");
362
- const rest = removed.length > 10 ? ` (+${removed.length - 10} more)` : "";
363
- warn(`Workflow worktree GC: removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"} ` +
364
- `older than ${STALE_WORKTREE_MAX_AGE_MS / (24 * 60 * 60 * 1000)}d under ${worktreesRoot()}: ${shown}${rest}`);
365
- })
366
- .catch(() => {
367
- // GC is best-effort observability; a failed sweep never affects a run.
368
- });
369
- }
370
- function message(err) {
371
- return err instanceof Error ? err.message : String(err);
372
- }
@@ -1,83 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
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.
9
- */
10
- import { UsageError } from "../../core/errors.js";
11
- import { formatReference, parseReference } from "../program/expressions.js";
12
- /** The frozen target a step's root unit (or map template) dispatches, if any. */
13
- function stepFrozenTarget(step) {
14
- const root = step?.root;
15
- if (!root)
16
- return undefined;
17
- const unit = root.kind === "map" ? root.template : root;
18
- return unit.frozenTarget;
19
- }
20
- /** Every reference string a step's `inputs[]`, `map.over`, `route.input`, and reference-kind `inputBindings[].from` carry. */
21
- function collectReferenceSites(step) {
22
- const sites = [];
23
- const push = (reference) => {
24
- sites.push({ stepId: step.stepId, reference });
25
- };
26
- if (step.route)
27
- push(step.route.input);
28
- const root = step.root;
29
- if (root) {
30
- const unit = root.kind === "map" ? root.template : root;
31
- if (root.kind === "map")
32
- push(root.over);
33
- for (const reference of unit.inputs ?? [])
34
- push(reference);
35
- const bindings = unit.frozenTarget.inputBindings ?? [];
36
- for (const binding of bindings) {
37
- if (binding.kind === "reference")
38
- push(binding.from);
39
- }
40
- }
41
- return sites;
42
- }
43
- /** The names a `child-workflow` target's own exported result carries at its first segment. */
44
- function acceptedFirstSegments(target) {
45
- const declared = target.frozenPlan.outputs;
46
- return declared ? Object.keys(declared) : ["runId", "status"];
47
- }
48
- function exportsDescription(target) {
49
- const declared = target.frozenPlan.outputs;
50
- return declared ? `outputs: ${Object.keys(declared).join(", ")}` : "only {runId, status} — it declares no `outputs:`";
51
- }
52
- function checkSite(site, stepsById) {
53
- const parsed = parseReference(site.reference);
54
- if (!parsed.ok || parsed.expr.kind !== "stepOutput")
55
- return;
56
- const targetStep = stepsById.get(parsed.expr.stepId);
57
- const frozenTarget = stepFrozenTarget(targetStep);
58
- if (!frozenTarget || frozenTarget.kind !== "child-workflow")
59
- return;
60
- const first = parsed.expr.path[0];
61
- if (first === undefined)
62
- return; // bare steps.<child>.output — the whole exported object, always accepted.
63
- const accepted = acceptedFirstSegments(frozenTarget);
64
- if (typeof first === "string" && accepted.includes(first))
65
- return;
66
- throw new UsageError(`Workflow step ${site.stepId} reads "${formatReference(parsed.expr)}", but child workflow ${frozenTarget.ref} ` +
67
- `exports ${exportsDescription(frozenTarget)}. Declare the output in the child's \`outputs:\` frontmatter, or ` +
68
- `reference one of the names above.`, "COMPOSITION_INVALID", `Fix the reference to name one of: ${accepted.join(", ")} — or add the missing name to ${frozenTarget.ref}'s ` +
69
- "own `outputs:` frontmatter.");
70
- }
71
- /**
72
- * Assert every reference into a `child-workflow`-targeted step's output
73
- * names an output the child actually exports. Throws `UsageError`
74
- * (`COMPOSITION_INVALID`) on the first violation found.
75
- */
76
- export function assertChildOutputReferences(steps) {
77
- const stepsById = new Map(steps.map((step) => [step.stepId, step]));
78
- for (const step of steps) {
79
- for (const site of collectReferenceSites(step)) {
80
- checkSite(site, stepsById);
81
- }
82
- }
83
- }
@@ -1,22 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { spawnSync } from "node:child_process";
5
- import { UsageError } from "../../core/errors.js";
6
- export function scriptExecutable(interpreter) {
7
- if (interpreter === "bun" || interpreter === "bun-standalone")
8
- return process.execPath;
9
- if (interpreter === "kotlin")
10
- return "kotlin";
11
- return interpreter;
12
- }
13
- export function gitIdentity(unit, root) {
14
- if (unit.isolation !== "worktree")
15
- return {};
16
- const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
17
- const oid = result.status === 0 ? result.stdout.trim() : "";
18
- if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(oid)) {
19
- throw new UsageError(`Worktree-isolated workflow root ${root} has no immutable Git HEAD OID.`, "WORKFLOW_SOURCE_INVALID");
20
- }
21
- return { gitCommitOid: oid };
22
- }
@@ -1,50 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import fs from "node:fs";
5
- import { prepareScriptTarget } from "../../../tasks/prepare/prepare-script-target.js";
6
- import { freezeEnvironment, resolveOwnedAsset } from "../environment.js";
7
- import { gitIdentity, scriptExecutable } from "../identity.js";
8
- import { freezeExecSpec, } from "../step-values.js";
9
- export async function directScript(source, baseUnit, refInput, context) {
10
- const owned = await resolveOwnedAsset(refInput, "script", context);
11
- const captured = prepareScriptTarget({
12
- ref: owned.ref,
13
- file: owned.file,
14
- bundleRoot: owned.root,
15
- readFile: () => fs.readFileSync(owned.file),
16
- });
17
- return scriptResult(source, baseUnit, {
18
- sourceRef: captured.ref,
19
- interpreter: captured.interpreter,
20
- extension: captured.extension,
21
- bytesBase64: captured.bytesBase64,
22
- byteLength: captured.byteLength,
23
- sha256: captured.sha256,
24
- cwdIdentity: captured.cwdIdentity,
25
- }, context, []);
26
- }
27
- export function scriptResult(source, baseUnit, prepared, context, literals) {
28
- const authoredExec = { command: [scriptExecutable(prepared.interpreter), "<frozen-script>"] };
29
- const exec = freezeExecSpec(source, authoredExec, context);
30
- const environment = Object.freeze([...literals, ...freezeEnvironment(source, authoredExec, context)]);
31
- const target = Object.freeze({
32
- kind: "script",
33
- ref: prepared.sourceRef,
34
- contentHash: prepared.sha256,
35
- exec,
36
- interpreter: prepared.interpreter,
37
- extension: prepared.extension,
38
- bytesBase64: prepared.bytesBase64,
39
- byteLength: prepared.byteLength,
40
- cwdIdentity: prepared.cwdIdentity,
41
- materialization: "ephemeral-0700-delete",
42
- ...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
43
- });
44
- return {
45
- target,
46
- environment,
47
- unit: { ...baseUnit, exec: authoredExec },
48
- instructions: source.instructions ?? `Run script ${prepared.sourceRef}.`,
49
- };
50
- }
@@ -1,124 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { parseBundleRef } from "../../../core/asset/asset-ref.js";
7
- import { UsageError } from "../../../core/errors.js";
8
- import { prepareTaskV3Execution } from "../../../tasks/prepare/prepare.js";
9
- import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
10
- import { projectTaskSourceV4 } from "../../../tasks/source/project-v4.js";
11
- import { workflowShellCommand } from "../../source-semantics.js";
12
- import { freezeEnvironment, resolveOwnedAsset, workflowExecutionSource } from "../environment.js";
13
- import { gitIdentity } from "../identity.js";
14
- import { declaredParamNames, earlierStepIds, freezeExecSpec, } from "../step-values.js";
15
- import { freezeTaskInputBindings } from "../task-bindings.js";
16
- import { childWorkflowDispatch } from "./child-workflow.js";
17
- import { commandResult } from "./command.js";
18
- import { scriptResult } from "./script.js";
19
- /** The rejection for a `with:` on a task that declares no inputs (or does not resolve). */
20
- function noDeclaredInputsError(stepId, ref) {
21
- return new UsageError(`Workflow step ${stepId} cannot pass with: to task target ${ref}; ${ref} declares no inputs.`, "COMPOSITION_INVALID");
22
- }
23
- /**
24
- * Resolve and parse the composed task. An authored `with:` whose target does
25
- * not even resolve cannot be proven a valid binding surface, so it is refused
26
- * the same no-declared-inputs way; a genuine defect in the resolved task's own
27
- * source propagates unchanged.
28
- */
29
- async function resolveTaskForComposition(source, refInput, context) {
30
- let owned;
31
- let yaml;
32
- try {
33
- owned = await resolveOwnedAsset(refInput, "task", context);
34
- yaml = fs.readFileSync(owned.file, "utf8");
35
- }
36
- catch (cause) {
37
- if (source.with !== undefined)
38
- throw noDeclaredInputsError(source.id, refInput);
39
- throw cause;
40
- }
41
- const parsed = parseTaskSource({ yaml, filePath: owned.file, workspaceRoot: owned.root });
42
- return { owned, task: projectTaskSourceV4(parsed.v4), contract: parsed.v4.inputs };
43
- }
44
- export async function taskDispatch(source, baseUnit, refInput, context) {
45
- const { owned, task, contract } = await resolveTaskForComposition(source, refInput, context);
46
- // Any authored `with:` (even `{}`) on a task that declares no inputs is COMPOSITION_INVALID.
47
- if (source.with !== undefined && contract === undefined) {
48
- throw noDeclaredInputsError(source.id, refInput);
49
- }
50
- // This step's own `with:` against this task's own declared inputs (none → no bindings).
51
- const bindings = freezeTaskInputBindings({
52
- stepId: source.id,
53
- targetRef: refInput,
54
- with: source.with,
55
- contract: contract ?? {},
56
- earlierStepIds: earlierStepIds(context.plan, source.id),
57
- declaredParamNames: declaredParamNames(context.plan),
58
- });
59
- const prepared = await prepareTaskV3Execution(task, {
60
- taskId: parseBundleRef(owned.ref).conceptId.slice("tasks/".length),
61
- taskRef: owned.ref,
62
- bundleName: owned.bundle,
63
- bundleRoot: owned.root,
64
- config: context.config,
65
- commandSourceLoader: (ref, kind) => workflowExecutionSource(ref, kind, context),
66
- resolveAsset: async ({ ref, type }) => {
67
- const target = await resolveOwnedAsset(ref, type, context);
68
- return { file: target.file, bundleRoot: target.root };
69
- },
70
- });
71
- if (prepared.kind === "workflow") {
72
- // A task whose own target is a workflow composes it as a child. This
73
- // task's effective inputs, already classified against its own contract,
74
- // are re-bound against the child's `params:` — never round-tripped through
75
- // the `with:` grammar, which would reinterpret a literal shaped like
76
- // `{from: ...}` as a reference.
77
- return childWorkflowDispatch({
78
- source,
79
- baseUnit,
80
- childRefInput: prepared.ref,
81
- context,
82
- via: "task",
83
- taskRef: prepared.taskRef,
84
- authoredInputs: { kind: "bindings", value: bindings },
85
- });
86
- }
87
- const taskLiterals = Object.entries(prepared.environment).map(([name, value]) => Object.freeze({ kind: "literal", name, value }));
88
- if (prepared.kind === "command") {
89
- return withInputBindings(commandResult(source, baseUnit, prepared.invocation, context, taskLiterals), bindings);
90
- }
91
- if (prepared.kind === "shell") {
92
- const authoredExec = {
93
- command: workflowShellCommand(prepared.shell, prepared.command),
94
- ...(prepared.cwdIdentity.realCwd !== prepared.cwdIdentity.realRoot
95
- ? { cwd: path.relative(prepared.cwdIdentity.realRoot, prepared.cwdIdentity.realCwd) }
96
- : {}),
97
- };
98
- const exec = freezeExecSpec(source, authoredExec, context);
99
- const environment = Object.freeze([...taskLiterals, ...freezeEnvironment(source, authoredExec, context)]);
100
- const target = Object.freeze({
101
- kind: "shell",
102
- contentHash: "",
103
- exec,
104
- cwdIdentity: prepared.cwdIdentity,
105
- ...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
106
- });
107
- return withInputBindings({
108
- target,
109
- environment,
110
- unit: { ...baseUnit, exec: authoredExec },
111
- instructions: source.instructions ?? `Run task ${owned.ref}.`,
112
- }, bindings);
113
- }
114
- return withInputBindings(scriptResult(source, baseUnit, prepared, context, taskLiterals), bindings);
115
- }
116
- /** Attach the frozen inputBindings to whichever target shape taskDispatch produced. Absent, never [], when empty. */
117
- function withInputBindings(resolved, bindings) {
118
- if (bindings.length === 0)
119
- return resolved;
120
- return {
121
- ...resolved,
122
- target: Object.freeze({ ...resolved.target, inputBindings: bindings }),
123
- };
124
- }