akm-cli 0.9.0-rc.0 → 0.9.0-rc.2

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 (262) hide show
  1. package/CHANGELOG.md +339 -2
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  11. package/dist/assets/stash-skeleton/README.md +28 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  24. package/dist/assets/tasks/core/backup.yml +1 -0
  25. package/dist/assets/tasks/core/extract.yml +1 -0
  26. package/dist/assets/tasks/core/improve.yml +1 -0
  27. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  28. package/dist/assets/tasks/core/sync.yml +1 -0
  29. package/dist/assets/tasks/core/version-check.yml +1 -0
  30. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  31. package/dist/assets/templates/html/health.html +5 -1
  32. package/dist/cli/config-migrate.js +31 -138
  33. package/dist/cli/config-validate.js +10 -8
  34. package/dist/cli.js +48 -14
  35. package/dist/commands/agent/agent-dispatch.js +17 -16
  36. package/dist/commands/agent/agent-support.js +0 -24
  37. package/dist/commands/agent/contribute-cli.js +5 -15
  38. package/dist/commands/backup-cli.js +54 -0
  39. package/dist/commands/config-cli.js +45 -159
  40. package/dist/commands/env/env-binding.js +95 -0
  41. package/dist/commands/env/env-cli.js +8 -65
  42. package/dist/commands/env/secret.js +8 -5
  43. package/dist/commands/health/checks.js +130 -83
  44. package/dist/commands/health/html-report.js +4 -0
  45. package/dist/commands/health/improve-metrics.js +30 -32
  46. package/dist/commands/health/llm-usage.js +19 -19
  47. package/dist/commands/health/md-report.js +4 -0
  48. package/dist/commands/health/metrics.js +2 -1
  49. package/dist/commands/health/surfaces.js +5 -4
  50. package/dist/commands/health.js +1 -1
  51. package/dist/commands/improve/consolidate/chunking.js +2 -2
  52. package/dist/commands/improve/consolidate.js +28 -25
  53. package/dist/commands/improve/distill/promote-memory.js +5 -12
  54. package/dist/commands/improve/distill/quality-gate.js +5 -7
  55. package/dist/commands/improve/distill.js +16 -5
  56. package/dist/commands/improve/eligibility.js +22 -12
  57. package/dist/commands/improve/extract-cli.js +47 -19
  58. package/dist/commands/improve/extract.js +110 -62
  59. package/dist/commands/improve/improve-cli.js +38 -16
  60. package/dist/commands/improve/improve-result-file.js +30 -24
  61. package/dist/commands/improve/improve-strategies.js +137 -0
  62. package/dist/commands/improve/improve.js +60 -30
  63. package/dist/commands/improve/locks.js +66 -45
  64. package/dist/commands/improve/loop-stages.js +75 -33
  65. package/dist/commands/improve/memory/memory-belief.js +79 -7
  66. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  67. package/dist/commands/improve/preparation.js +71 -73
  68. package/dist/commands/improve/procedural.js +3 -2
  69. package/dist/commands/improve/recombine.js +2 -1
  70. package/dist/commands/improve/reflect.js +119 -214
  71. package/dist/commands/improve/shared.js +11 -5
  72. package/dist/commands/lint/base-linter.js +152 -42
  73. package/dist/commands/mv-cli.js +809 -0
  74. package/dist/commands/proposal/proposal-cli.js +18 -8
  75. package/dist/commands/proposal/propose.js +64 -69
  76. package/dist/commands/read/knowledge.js +436 -4
  77. package/dist/commands/read/remember-cli.js +39 -2
  78. package/dist/commands/read/search-cli.js +6 -1
  79. package/dist/commands/registry-cli.js +29 -14
  80. package/dist/commands/remember.js +2 -0
  81. package/dist/commands/sources/init.js +13 -14
  82. package/dist/commands/sources/migration-help.js +7 -4
  83. package/dist/commands/sources/schema-repair.js +2 -4
  84. package/dist/commands/sources/source-add.js +62 -73
  85. package/dist/commands/sources/source-manage.js +50 -46
  86. package/dist/commands/sources/stash-cli.js +41 -4
  87. package/dist/commands/tasks/default-tasks.js +12 -12
  88. package/dist/commands/tasks/tasks-cli.js +7 -3
  89. package/dist/commands/tasks/tasks.js +113 -18
  90. package/dist/commands/wiki-cli.js +9 -10
  91. package/dist/commands/workflow-cli.js +276 -12
  92. package/dist/core/asset/asset-spec.js +58 -1
  93. package/dist/core/asset/frontmatter.js +12 -2
  94. package/dist/core/common.js +5 -3
  95. package/dist/core/config/config-io.js +28 -17
  96. package/dist/core/config/config-schema.js +379 -66
  97. package/dist/core/config/config-types.js +3 -3
  98. package/dist/core/config/config-version.js +29 -0
  99. package/dist/core/config/config-walker.js +98 -27
  100. package/dist/core/config/config.js +132 -266
  101. package/dist/core/config/deep-merge.js +41 -0
  102. package/dist/core/config/engine-semantics.js +32 -0
  103. package/dist/core/errors.js +2 -2
  104. package/dist/core/extra-params.js +61 -0
  105. package/dist/core/file-lock.js +201 -56
  106. package/dist/core/improve-result.js +178 -0
  107. package/dist/core/json-schema.js +142 -0
  108. package/dist/core/maintenance-barrier.js +119 -0
  109. package/dist/core/migration-backup.js +416 -0
  110. package/dist/core/paths.js +3 -0
  111. package/dist/core/redaction.js +358 -0
  112. package/dist/core/state/migrations.js +17 -2
  113. package/dist/core/state-db.js +44 -1
  114. package/dist/indexer/db/db.js +118 -2
  115. package/dist/indexer/graph/graph-extraction.js +28 -16
  116. package/dist/indexer/index-writer-lock.js +31 -24
  117. package/dist/indexer/index-written-assets.js +15 -6
  118. package/dist/indexer/indexer.js +47 -2
  119. package/dist/indexer/passes/memory-inference.js +10 -6
  120. package/dist/indexer/passes/metadata.js +250 -0
  121. package/dist/indexer/search/db-search.js +111 -44
  122. package/dist/indexer/search/fts-query.js +41 -0
  123. package/dist/indexer/search/ranking-contributors.js +48 -0
  124. package/dist/indexer/search/ranking.js +36 -23
  125. package/dist/indexer/search/search-fields.js +11 -1
  126. package/dist/indexer/walk/matchers.js +39 -0
  127. package/dist/integrations/agent/builder-shared.js +7 -0
  128. package/dist/integrations/agent/builders.js +5 -50
  129. package/dist/integrations/agent/config.js +3 -143
  130. package/dist/integrations/agent/detect.js +17 -2
  131. package/dist/integrations/agent/engine-resolution.js +202 -0
  132. package/dist/integrations/agent/index.js +1 -2
  133. package/dist/integrations/agent/model-aliases.js +16 -2
  134. package/dist/integrations/agent/profiles.js +36 -62
  135. package/dist/integrations/agent/runner-dispatch.js +91 -4
  136. package/dist/integrations/agent/runner.js +76 -207
  137. package/dist/integrations/agent/spawn.js +141 -20
  138. package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
  139. package/dist/integrations/harnesses/aider/index.js +57 -0
  140. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  141. package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
  142. package/dist/integrations/harnesses/amazonq/index.js +58 -0
  143. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  144. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  145. package/dist/integrations/harnesses/claude/index.js +25 -25
  146. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  147. package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
  148. package/dist/integrations/harnesses/codex/index.js +62 -0
  149. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  150. package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
  151. package/dist/integrations/harnesses/copilot/index.js +59 -0
  152. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  153. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  154. package/dist/integrations/harnesses/gemini/index.js +59 -0
  155. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  156. package/dist/integrations/harnesses/index.js +27 -28
  157. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  158. package/dist/integrations/harnesses/opencode/index.js +15 -22
  159. package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
  160. package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
  161. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  162. package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
  163. package/dist/integrations/harnesses/openhands/index.js +57 -0
  164. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  165. package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
  166. package/dist/integrations/harnesses/pi/index.js +57 -0
  167. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  168. package/dist/integrations/harnesses/types.js +8 -32
  169. package/dist/integrations/lockfile.js +32 -21
  170. package/dist/integrations/session-logs/index.js +24 -11
  171. package/dist/llm/client.js +48 -14
  172. package/dist/llm/feature-gate.js +15 -47
  173. package/dist/llm/graph-extract.js +1 -1
  174. package/dist/llm/index-passes.js +8 -42
  175. package/dist/llm/memory-infer-impl.js +1 -1
  176. package/dist/llm/usage-persist.js +4 -0
  177. package/dist/llm/usage-telemetry.js +35 -5
  178. package/dist/output/renderers.js +3 -2
  179. package/dist/output/shapes/helpers.js +2 -1
  180. package/dist/output/shapes/passthrough.js +6 -0
  181. package/dist/output/text/helpers.js +215 -2
  182. package/dist/output/text/workflow.js +3 -1
  183. package/dist/schemas/akm-config.json +16638 -0
  184. package/dist/schemas/akm-task.json +87 -0
  185. package/dist/schemas/akm-workflow.json +372 -0
  186. package/dist/scripts/migrate-storage.js +10944 -8801
  187. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
  188. package/dist/setup/detected-engines.js +142 -0
  189. package/dist/setup/engine-config.js +89 -0
  190. package/dist/setup/setup.js +236 -132
  191. package/dist/setup/steps/connection.js +61 -32
  192. package/dist/setup/steps/platforms.js +4 -4
  193. package/dist/setup/steps.js +3 -2
  194. package/dist/storage/database.js +13 -1
  195. package/dist/storage/engines/sqlite-migrations.js +1 -0
  196. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  197. package/dist/storage/repositories/task-history-repository.js +78 -0
  198. package/dist/storage/repositories/workflow-runs-repository.js +190 -1
  199. package/dist/tasks/parser.js +138 -52
  200. package/dist/tasks/runner.js +71 -75
  201. package/dist/tasks/schema.js +1 -1
  202. package/dist/tasks/validator.js +11 -6
  203. package/dist/text-import-hook.mjs +1 -1
  204. package/dist/wiki/wiki.js +9 -8
  205. package/dist/workflows/authoring/authoring.js +123 -10
  206. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  207. package/dist/workflows/cli.js +4 -0
  208. package/dist/workflows/concurrency-policy.js +15 -0
  209. package/dist/workflows/db.js +200 -13
  210. package/dist/workflows/exec/brief.js +478 -0
  211. package/dist/workflows/exec/frozen-judge.js +47 -0
  212. package/dist/workflows/exec/native-executor.js +1034 -0
  213. package/dist/workflows/exec/param-secrets.js +115 -0
  214. package/dist/workflows/exec/report.js +1355 -0
  215. package/dist/workflows/exec/run-workflow.js +609 -0
  216. package/dist/workflows/exec/scheduler.js +71 -0
  217. package/dist/workflows/exec/step-work.js +1212 -0
  218. package/dist/workflows/exec/unit-writer.js +23 -0
  219. package/dist/workflows/exec/watch.js +116 -0
  220. package/dist/workflows/exec/worktree.js +171 -0
  221. package/dist/workflows/ir/compile.js +375 -0
  222. package/dist/workflows/ir/freeze.js +243 -0
  223. package/dist/workflows/ir/params.js +54 -0
  224. package/dist/workflows/ir/plan-hash.js +68 -0
  225. package/dist/workflows/ir/schema.js +545 -0
  226. package/dist/workflows/parser.js +10 -1
  227. package/dist/workflows/program/expressions.js +369 -0
  228. package/dist/workflows/program/parser.js +869 -0
  229. package/dist/workflows/program/project.js +104 -0
  230. package/dist/workflows/program/schema.js +54 -0
  231. package/dist/workflows/renderer.js +82 -5
  232. package/dist/workflows/resource-limits.js +20 -0
  233. package/dist/workflows/runtime/agent-identity.js +59 -14
  234. package/dist/workflows/runtime/plan-classifier.js +187 -0
  235. package/dist/workflows/runtime/runs.js +246 -69
  236. package/dist/workflows/runtime/unit-checkin.js +45 -0
  237. package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
  238. package/dist/workflows/validate-summary.js +24 -3
  239. package/dist/workflows/validator.js +26 -1
  240. package/docs/data-and-telemetry.md +4 -3
  241. package/docs/migration/release-notes/0.6.0.md +1 -1
  242. package/docs/migration/release-notes/0.7.0.md +5 -4
  243. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  244. package/docs/migration/v0.8-to-v0.9.md +401 -0
  245. package/package.json +4 -2
  246. package/schemas/akm-config.json +16638 -0
  247. package/schemas/akm-task.json +87 -0
  248. package/schemas/akm-workflow.json +372 -0
  249. package/dist/commands/improve/improve-profiles.js +0 -168
  250. package/dist/core/config/config-migration.js +0 -602
  251. package/dist/core/deep-merge.js +0 -38
  252. package/dist/llm/call-ai.js +0 -62
  253. package/dist/setup/legacy-config.js +0 -106
  254. package/docs/README.md +0 -104
  255. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  256. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  257. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  258. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  259. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  260. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  261. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  262. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -0,0 +1,23 @@
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
+ * Serialized writer queue for `workflow_run_units` (orchestration plan,
6
+ * *Persistence changes*).
7
+ *
8
+ * `withWorkflowRunsRepo` opens a fresh SQLite connection per call, so N
9
+ * parallel units completing at once would contend on SQLite's single writer
10
+ * and burn the 30 s busy_timeout. Bun is single-threaded, so a promise-chained
11
+ * in-process queue is sufficient: every unit write is appended to one chain
12
+ * and executes strictly in enqueue order. Reads and gate evaluation stay OFF
13
+ * this queue — only writes serialize.
14
+ *
15
+ * A failed write rejects its own caller but never wedges the chain.
16
+ */
17
+ let tail = Promise.resolve();
18
+ export function enqueueUnitWrite(fn) {
19
+ const run = tail.then(() => fn());
20
+ // Keep the chain alive regardless of individual outcomes.
21
+ tail = run.then(() => undefined, () => undefined);
22
+ return run;
23
+ }
@@ -0,0 +1,116 @@
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
+ * `akm workflow watch <run>` — run-scoped event tail (redesign addendum R2).
6
+ *
7
+ * Prints the run's `workflow_*` / `workflow_unit_*` events from the state.db
8
+ * `events` table (matched on `metadata.runId`) as NDJSON — one
9
+ * {@link EventEnvelope} per line — and exits. With `stream: true` it keeps
10
+ * polling from the last seen event id (monotonic rowid cursor, so concurrent
11
+ * writers can never cause skips) and exits when the run reaches a terminal
12
+ * status.
13
+ *
14
+ * Design constraints (pinned R2 decisions):
15
+ * - FOREGROUND poll loop only — no daemon, no background process, no
16
+ * `tailEvents` subscription held past the terminal status.
17
+ * - Reads go through the existing events repository (via
18
+ * {@link readEvents}); this module issues no SQL of its own.
19
+ * - "Terminal" means any non-`active` run status (`completed`, `failed`,
20
+ * `blocked`): in all three the engine has stopped driving, so no further
21
+ * events arrive until a human resumes the run. The status is read BEFORE
22
+ * each drain, so events written before the status flip are always
23
+ * emitted before the loop exits. The engine commits the status flip
24
+ * (workflow.db transaction) BEFORE it appends the terminal
25
+ * `workflow_step_completed` / `workflow_finished` events to state.db, so
26
+ * after first observing a terminal status the loop keeps performing
27
+ * grace polls (sleep + drain) until one drains nothing new — the
28
+ * terminal events landing in that commit→append window are never
29
+ * dropped.
30
+ * - Event lines are the raw envelopes (ids/status metadata only — event
31
+ * emitters never journal workflow-authored content, 07 P1-B rule).
32
+ */
33
+ import { NotFoundError } from "../../core/errors.js";
34
+ import { readEvents } from "../../core/events.js";
35
+ import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
36
+ /** Default `--stream` poll interval (ms). */
37
+ export const DEFAULT_WATCH_INTERVAL_MS = 1000;
38
+ /**
39
+ * True when `event` belongs to workflow run `runId`: the event type is in the
40
+ * `workflow_*` family (which includes `workflow_unit_*`) AND the metadata
41
+ * carries a matching `runId`. Events of other families that happen to carry
42
+ * a `runId` (e.g. `llm_usage`) are excluded by design.
43
+ */
44
+ export function isWorkflowRunEvent(event, runId) {
45
+ if (!event.eventType.startsWith("workflow_"))
46
+ return false;
47
+ return event.metadata?.runId === runId;
48
+ }
49
+ /** Default status reader — repository lookup; a missing run is a structured not-found. */
50
+ async function readRunStatus(runId) {
51
+ return withWorkflowRunsRepo((repo) => {
52
+ const row = repo.getRunById(runId);
53
+ if (!row) {
54
+ throw new NotFoundError(`Workflow run "${runId}" not found.`, "WORKFLOW_NOT_FOUND", "Run `akm workflow list --active` to see runs.");
55
+ }
56
+ return row.status;
57
+ });
58
+ }
59
+ /**
60
+ * Watch one workflow run's events. Emits each matching event envelope as a
61
+ * single NDJSON line via `emit`, then returns a summary. See the module doc
62
+ * for the backlog/stream/terminal-status contract.
63
+ */
64
+ export async function watchWorkflowRun(options) {
65
+ const intervalMs = options.intervalMs ?? DEFAULT_WATCH_INTERVAL_MS;
66
+ const emit = options.emit ?? ((line) => process.stdout.write(`${line}\n`));
67
+ const read = options.readEventsFn ?? ((readOptions) => readEvents(readOptions));
68
+ const getRunStatus = options.getRunStatus ?? readRunStatus;
69
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
70
+ // Existence check first so an unknown run id is a structured error, not an
71
+ // empty NDJSON stream.
72
+ let status = await getRunStatus(options.runId);
73
+ let eventCount = 0;
74
+ let cursor = 0;
75
+ const drain = () => {
76
+ const { events, nextOffset } = read({ sinceOffset: cursor });
77
+ cursor = nextOffset;
78
+ for (const event of events) {
79
+ if (!isWorkflowRunEvent(event, options.runId))
80
+ continue;
81
+ emit(JSON.stringify(event));
82
+ eventCount++;
83
+ }
84
+ };
85
+ // Backlog: everything already journaled for this run.
86
+ drain();
87
+ if (options.stream === true) {
88
+ // Foreground poll loop (NO daemon). Status is re-read BEFORE each drain
89
+ // so events written before a terminal flip are emitted before exit.
90
+ while (status === "active") {
91
+ await sleep(intervalMs);
92
+ status = await getRunStatus(options.runId);
93
+ drain();
94
+ }
95
+ // Terminal grace polls: completeWorkflowStep commits the run-status flip
96
+ // (workflow.db) BEFORE appending workflow_step_completed /
97
+ // workflow_finished to state.db, so the drain that first observed the
98
+ // terminal status can predate those events. Keep sleeping + draining
99
+ // until an idle poll (a drain that emits nothing new for this run) —
100
+ // the engine has stopped driving, so this converges after the terminal
101
+ // events land (already-terminal runs pay exactly one idle poll).
102
+ let emittedBefore;
103
+ do {
104
+ emittedBefore = eventCount;
105
+ await sleep(intervalMs);
106
+ drain();
107
+ } while (eventCount > emittedBefore);
108
+ }
109
+ return {
110
+ runId: options.runId,
111
+ status,
112
+ eventCount,
113
+ lastEventId: cursor,
114
+ streamed: options.stream === true,
115
+ };
116
+ }
@@ -0,0 +1,171 @@
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 (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):
10
+ *
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
+ *
22
+ * What "uncollected work" means (the honest contract): the clean probe is
23
+ * `git status --porcelain` WITHOUT `--ignored`, so it counts tracked-file
24
+ * modifications and untracked *unignored* files, but NOT files the base repo's
25
+ * own `.gitignore` matches (build outputs, caches, logs, dependency dirs such
26
+ * as `node_modules`/`dist`). Those ignored files are DISPOSABLE BY DEFINITION
27
+ * — the repository already declares them regenerable — so a worktree whose only
28
+ * residue is ignored files probes clean and IS removed. This is deliberate:
29
+ * adding `--ignored` would retain a worktree after essentially every unit that
30
+ * ran a package install or a build (the ignored `node_modules`/`dist` tree),
31
+ * blowing up disk under the run-scoped tmp root. Work a unit needs preserved
32
+ * must therefore be tracked or untracked-unignored; anything the workflow
33
+ * repo has chosen to `.gitignore` is treated as throwaway.
34
+ *
35
+ * All git invocations are `spawnSync` (the repo-wide pattern for git
36
+ * shell-outs) with explicit timeouts; this module never throws — every
37
+ * operation returns a result object so the executor maps failures onto its
38
+ * own step/unit failure vocabulary.
39
+ */
40
+ import { spawnSync } from "node:child_process";
41
+ import fs from "node:fs";
42
+ import os from "node:os";
43
+ import path from "node:path";
44
+ const GIT_TIMEOUT_MS = 30_000;
45
+ /** Run one git command; `ok` = exit 0. Never throws (spawn errors → ok: false). */
46
+ function git(cwd, args) {
47
+ const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: GIT_TIMEOUT_MS });
48
+ if (result.error) {
49
+ return { ok: false, stdout: "", error: `git ${args[0]} failed to spawn: ${result.error.message}` };
50
+ }
51
+ if (result.status !== 0) {
52
+ const detail = (result.stderr || result.stdout || "").trim();
53
+ return {
54
+ ok: false,
55
+ stdout: result.stdout ?? "",
56
+ error: `git ${args.join(" ")} exited ${result.status}${detail ? `: ${detail}` : ""}`,
57
+ };
58
+ }
59
+ return { ok: true, stdout: result.stdout ?? "" };
60
+ }
61
+ /** True when a usable `git` binary is on PATH (tests skip gracefully without one). */
62
+ export function isGitAvailable() {
63
+ const result = spawnSync("git", ["--version"], { encoding: "utf8", timeout: 5_000 });
64
+ return !result.error && result.status === 0;
65
+ }
66
+ /**
67
+ * Preflight for worktree isolation: `dir` must be inside a git work tree.
68
+ * Returns an error message (for a clean step failure) or undefined when ok.
69
+ * A missing git binary reports as the same clean failure — a workflow that
70
+ * declares isolation cannot run without git.
71
+ */
72
+ export function assertGitWorkTree(dir) {
73
+ const result = git(dir, ["rev-parse", "--is-inside-work-tree"]);
74
+ if (!result.ok) {
75
+ return `"${dir}" is not a git repository (isolation: worktree requires one): ${result.error}`;
76
+ }
77
+ if (result.stdout.trim() !== "true") {
78
+ return `"${dir}" is not inside a git work tree (isolation: worktree requires one).`;
79
+ }
80
+ return undefined;
81
+ }
82
+ /** Journal-safe directory name for a unit attempt id (ids carry `:` / `~`). */
83
+ function sanitizeAttemptId(attemptId) {
84
+ return attemptId.replace(/[^A-Za-z0-9._-]/g, "-");
85
+ }
86
+ /** Run-scoped parent directory for all of one run's unit worktrees. */
87
+ export function runWorktreeRoot(runId) {
88
+ return path.join(os.tmpdir(), "akm-worktrees", runId);
89
+ }
90
+ /**
91
+ * Move a leftover attempt directory aside to `<dest>.retained-<ts>[-n]`
92
+ * (never overwriting an earlier retained copy). Throws on fs errors — the
93
+ * caller maps them onto its result object.
94
+ */
95
+ function moveLeftoverAside(dest) {
96
+ const base = `${dest}.retained-${Date.now()}`;
97
+ let aside = base;
98
+ for (let n = 1; fs.existsSync(aside); n++)
99
+ aside = `${base}-${n}`;
100
+ fs.renameSync(dest, aside);
101
+ return aside;
102
+ }
103
+ /**
104
+ * Create a fresh DETACHED worktree of `baseDir`'s repository at
105
+ * `<tmp>/akm-worktrees/<runId>/<attemptId>` (detached HEAD — no branch is
106
+ * minted, so parallel units cannot collide on branch names).
107
+ *
108
+ * A leftover directory at the attempt path (a RETAINED dirty worktree from a
109
+ * prior invocation, or a crashed attempt's partial state) is handled with the
110
+ * same never-destroy-unverified-work rule as {@link cleanupUnitWorktree}:
111
+ * `git status --porcelain` CLEAN → removed; DIRTY or unverifiable (the probe
112
+ * fails — e.g. a half-created directory that is no longer a valid worktree)
113
+ * → moved aside to `<dest>.retained-<ts>` and reported via
114
+ * `preservedLeftover` so the caller can log where the work went. Either way
115
+ * `git worktree prune` clears the stale registration before re-creating.
116
+ */
117
+ export function createUnitWorktree(baseDir, runId, attemptId) {
118
+ const dest = path.join(runWorktreeRoot(runId), sanitizeAttemptId(attemptId));
119
+ let preservedLeftover;
120
+ try {
121
+ if (fs.existsSync(dest)) {
122
+ const status = git(dest, ["status", "--porcelain"]);
123
+ if (status.ok && status.stdout.trim() === "") {
124
+ fs.rmSync(dest, { recursive: true, force: true });
125
+ }
126
+ else {
127
+ preservedLeftover = moveLeftoverAside(dest);
128
+ }
129
+ git(baseDir, ["worktree", "prune"]);
130
+ }
131
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
132
+ }
133
+ catch (err) {
134
+ return { ok: false, error: `could not prepare worktree directory ${dest}: ${message(err)}` };
135
+ }
136
+ const added = git(baseDir, ["worktree", "add", "--detach", dest]);
137
+ if (!added.ok) {
138
+ return { ok: false, error: `could not create isolation worktree at ${dest}: ${added.error}` };
139
+ }
140
+ return { ok: true, path: dest, ...(preservedLeftover !== undefined ? { preservedLeftover } : {}) };
141
+ }
142
+ /**
143
+ * Post-unit cleanup: remove the worktree when `git status --porcelain` shows
144
+ * it clean; retain it (dirty: true) when the unit left uncommitted work —
145
+ * the caller logs the retained path. Any git failure retains the worktree
146
+ * too (never destroy a tree whose state could not be verified).
147
+ *
148
+ * The probe deliberately omits `--ignored`: a worktree whose only residue is
149
+ * files matched by the base repo's `.gitignore` (build artifacts, caches,
150
+ * logs, `node_modules`) probes clean and IS removed. Those files are disposable
151
+ * by the repo's own declaration; retaining a worktree per build/install would
152
+ * blow up disk. "Uncollected work" the caller preserves is therefore
153
+ * tracked-or-untracked-unignored changes only (module doc).
154
+ */
155
+ export function cleanupUnitWorktree(baseDir, worktreePath) {
156
+ const status = git(worktreePath, ["status", "--porcelain"]);
157
+ if (!status.ok) {
158
+ return { removed: false, dirty: false, error: status.error };
159
+ }
160
+ if (status.stdout.trim() !== "") {
161
+ return { removed: false, dirty: true };
162
+ }
163
+ const removed = git(baseDir, ["worktree", "remove", worktreePath]);
164
+ if (!removed.ok) {
165
+ return { removed: false, dirty: false, error: removed.error };
166
+ }
167
+ return { removed: true, dirty: false };
168
+ }
169
+ function message(err) {
170
+ return err instanceof Error ? err.message : String(err);
171
+ }