akm-cli 0.9.31 → 0.10.261001-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 +409 -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 +94 -13
  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
@@ -53,8 +53,6 @@ const DEFAULT_MAX_SESSIONS_PER_RUN = 25;
53
53
  * but never less than 48h. The content-hash ledger makes the overlap free.
54
54
  */
55
55
  const DEFAULT_SINCE_FLOOR_MS = 48 * 60 * 60 * 1000;
56
- /** A per-session lock older than this belongs to a crashed holder. */
57
- const EXTRACT_SESSION_LOCK_STALE_MS = 5 * 60 * 1000;
58
56
  function resolveDefaultSinceMs(harnessName, now, opts) {
59
57
  const floor = now - DEFAULT_SINCE_FLOOR_MS;
60
58
  if (opts.skipTracking)
@@ -83,14 +81,14 @@ function extractSessionLockPath(harness, sessionId, stateDbPath) {
83
81
  return path.join(path.dirname(stateDbPath), "extract-locks", `extract-${safe}.lock`);
84
82
  }
85
83
  function extractSessionLockIsUnavailable(harness, sessionId, stateDbPath) {
86
- const probe = probeLock(extractSessionLockPath(harness, sessionId, stateDbPath), {
87
- staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS,
88
- });
84
+ const probe = probeLock(extractSessionLockPath(harness, sessionId, stateDbPath));
89
85
  return probe.state === "held" || probe.state === "inaccessible";
90
86
  }
91
87
  /**
92
88
  * Claim a session so a concurrent extract (a session-end hook racing the
93
- * hourly improve run) cannot process it twice. A stale lock is reclaimed; a
89
+ * hourly improve run) cannot process it twice. Only a lock whose holder is
90
+ * dead is reclaimed (no age window: a live extraction can run for many
91
+ * minutes, the #872 lesson in `core/run-lock.ts`); a
94
92
  * filesystem error proceeds, so locking never blocks extraction outright.
95
93
  */
96
94
  function acquireExtractSessionLock(lockPath) {
@@ -99,7 +97,7 @@ function acquireExtractSessionLock(lockPath) {
99
97
  let ownership = tryAcquireLockSync(lockPath, createLockPayload());
100
98
  if (ownership)
101
99
  return { proceed: true, ownership };
102
- const probe = probeLock(lockPath, { staleAfterMs: EXTRACT_SESSION_LOCK_STALE_MS });
100
+ const probe = probeLock(lockPath);
103
101
  if (probe.state === "held")
104
102
  return { proceed: false };
105
103
  if (probe.state === "stale" && !reclaimStaleLock(lockPath, probe))
@@ -3,16 +3,15 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import path from "node:path";
5
5
  import { defineCommand } from "citty";
6
- import { getParsedInvocation } from "../../cli/invocation.js";
7
- import { getStringArg, parsePositiveIntFlag } from "../../cli/parse-args.js";
8
- import { GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
6
+ import { findCittyTopLevelCommandIndex, getStringArg, hasSubcommand, parsePositiveIntFlag } from "../../cli/parse-args.js";
7
+ import { defineJsonCommand, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
9
8
  import { isFullRefInput, parseRefInput } from "../../core/asset/resolve-ref.js";
10
9
  import { loadConfig } from "../../core/config/config.js";
11
10
  import { ConfigError, UsageError } from "../../core/errors.js";
12
11
  import { resolveMutationTarget } from "../../core/mutation-target.js";
13
- import { getCacheDir } from "../../core/paths.js";
12
+ import { getRunLogDir } from "../../core/paths.js";
14
13
  import { redactSensitiveText } from "../../core/redaction.js";
15
- import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
14
+ import { clearLogFile, setLogFile } from "../../core/warn.js";
16
15
  import { resolveWriteTarget } from "../../core/write-source.js";
17
16
  import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
18
17
  import { defaultWhich } from "../../integrations/agent/detect.js";
@@ -35,37 +34,6 @@ let akmImproveForRun = akmImprove;
35
34
  export function _setAkmImproveForTests(fake) {
36
35
  akmImproveForRun = fake ?? akmImprove;
37
36
  }
38
- /**
39
- * `--auto-accept` (removed in 0.9): citty absorbs it silently, and
40
- * `--auto-accept 90` would leave `90` as the scope — a 0.8-era crontab would
41
- * match nothing and exit 0. Warn, and drop that positional.
42
- */
43
- function resolveScopeAfterRetiredAutoAccept(scopeArg) {
44
- const invocation = getParsedInvocation();
45
- const autoAcceptRaw = invocation.getFlagValue("--auto-accept");
46
- if (autoAcceptRaw === undefined && !invocation.hasFlag("--auto-accept"))
47
- return scopeArg;
48
- warn("[improve] --auto-accept was removed in 0.9 and is ignored; proposals always queue for review. " +
49
- "Replacement: `akm improve && akm proposal drain --promote --yes`, or a `triage` block with " +
50
- 'applyMode: "promote" in your strategy. It becomes a hard error in 0.10.');
51
- if (scopeArg !== undefined && scopeArg === autoAcceptRaw) {
52
- warn(`[improve] ignoring "${scopeArg}" as a scope — it is the removed --auto-accept flag's value.`);
53
- return undefined;
54
- }
55
- return scopeArg;
56
- }
57
- /** `akm improve canary` (removed in 0.9) would otherwise be a type scope matching nothing, exiting 0. */
58
- function rejectRetiredCanaryScope(scopeArg) {
59
- if (scopeArg !== "canary")
60
- return;
61
- throw new UsageError('"akm improve canary" was removed in 0.9; the collapse-detector canary set it managed no longer exists.', "INVALID_FLAG_VALUE");
62
- }
63
- /** `--target` (renamed `--bundle` in 0.9) would otherwise be absorbed and write to the default bundle. */
64
- function rejectRetiredImproveTargetFlag() {
65
- if (!getParsedInvocation().hasFlag("--target"))
66
- return;
67
- throw new UsageError("`akm improve --target` was renamed to `--bundle` in 0.9. Use `--bundle <name>` instead.", "INVALID_FLAG_VALUE");
68
- }
69
37
  /**
70
38
  * `--require-engines` (#957): fail before any side effect when an enabled
71
39
  * process cannot run, naming what each is missing — a scheduled run would
@@ -194,53 +162,91 @@ async function runShowPromptCli(refArg, parsedRef, taskArg, targetArg, resolvedP
194
162
  });
195
163
  }
196
164
  /** `akm improve report` (#944): the per-run LLM usage/routing report ("report" is no asset type). */
197
- function runImproveReportCli(args) {
198
- const runIdArg = getStringArg(args, "run");
199
- const sinceArg = getStringArg(args, "since");
200
- const result = runImproveReportQuery({ runId: runIdArg, since: sinceArg });
201
- output("improve-report", { ok: true, ...result });
202
- }
203
- /** `--run`/`--since` belong to `improve report`; elsewhere citty would silently ignore them. */
204
- function rejectReportOnlyFlags(args) {
205
- const flag = getStringArg(args, "run") !== undefined
206
- ? "--run"
207
- : getStringArg(args, "since") !== undefined
208
- ? "--since"
209
- : undefined;
210
- if (flag === undefined)
211
- return;
212
- throw new UsageError(`\`${flag}\` only applies to \`akm improve report\`. Use \`akm improve report ${flag} <value>\` instead.`, "INVALID_FLAG_VALUE");
213
- }
165
+ const improveReportCommand = defineJsonCommand({
166
+ meta: {
167
+ name: "report",
168
+ description: "Show the LLM usage/routing report for the most recent improve run, one run, or a window of runs.",
169
+ },
170
+ args: {
171
+ run: {
172
+ type: "string",
173
+ description: "Show the report for one specific improve_runs row instead of the most recent run. Mutually exclusive with --since.",
174
+ },
175
+ since: {
176
+ type: "string",
177
+ description: 'Aggregate the report over every real run started since <window> (a duration like "24h"/"7d", or an ISO timestamp) instead of showing one run. Mutually exclusive with --run.',
178
+ },
179
+ },
180
+ run({ args }) {
181
+ const result = runImproveReportQuery({ runId: getStringArg(args, "run"), since: getStringArg(args, "since") });
182
+ output("improve-report", { ok: true, ...result });
183
+ },
184
+ });
214
185
  /**
215
186
  * `akm improve judge`: reflect's quality judge on one revision, read as
216
187
  * `{"source", "candidate", "feedback"}` JSON from stdin, with the engine the
217
188
  * strategy's reflect quality gate names. It writes nothing.
218
189
  */
219
- async function runImproveJudgeCli(strategyName) {
220
- const input = process.stdin.isTTY
221
- ? {}
222
- : JSON.parse((await readStdin()).toString("utf8"));
223
- const { source, candidate, feedback, ref } = input;
224
- if (typeof source !== "string" || typeof candidate !== "string") {
225
- throw new UsageError('`akm improve judge` reads {"source": "...", "candidate": "...", "feedback": "...", "ref": "..."} JSON from stdin.', "MISSING_REQUIRED_ARGUMENT");
226
- }
227
- const config = loadConfig();
228
- const judge = resolveQualityGateJudge(config, resolveImproveStrategy(strategyName, config).config, "reflect");
229
- if (!judge) {
230
- throw new ConfigError("`akm improve judge` judges with the reflect quality gate's engine. Set processes.reflect.qualityGate.engine.", "INVALID_CONFIG_FILE");
231
- }
232
- const notes = typeof feedback === "string" && feedback.trim() !== "" ? [feedback.trim()] : [];
233
- const verdict = await runReflectQualityJudge(config, candidate, source, notes, undefined, {
234
- runnerSelectionFrozen: true,
235
- llmRunner: judge,
236
- ...(typeof ref === "string" && ref ? { ref } : {}),
237
- });
238
- output("improve-judge", { engine: judge.engine, ...verdict });
239
- }
190
+ const improveJudgeCommand = defineJsonCommand({
191
+ meta: {
192
+ name: "judge",
193
+ description: "Run reflect's quality judge on one revision, read as {source, candidate, feedback, ref} JSON from stdin. Writes nothing.",
194
+ },
195
+ args: {
196
+ strategy: {
197
+ type: "string",
198
+ description: "Named improve strategy whose reflect quality gate names the judge engine.",
199
+ },
200
+ },
201
+ async run({ args }) {
202
+ const input = process.stdin.isTTY
203
+ ? {}
204
+ : JSON.parse((await readStdin()).toString("utf8"));
205
+ const { source, candidate, feedback, ref } = input;
206
+ if (typeof source !== "string" || typeof candidate !== "string") {
207
+ throw new UsageError('`akm improve judge` reads {"source": "...", "candidate": "...", "feedback": "...", "ref": "..."} JSON from stdin.', "MISSING_REQUIRED_ARGUMENT");
208
+ }
209
+ const config = loadConfig();
210
+ const judge = resolveQualityGateJudge(config, resolveImproveStrategy(getStringArg(args, "strategy"), config).config, "reflect");
211
+ if (!judge) {
212
+ throw new ConfigError("`akm improve judge` judges with the reflect quality gate's engine. Set processes.reflect.qualityGate.engine.", "INVALID_CONFIG_FILE");
213
+ }
214
+ const notes = typeof feedback === "string" && feedback.trim() !== "" ? [feedback.trim()] : [];
215
+ const verdict = await runReflectQualityJudge(config, candidate, source, notes, undefined, {
216
+ runnerSelectionFrozen: true,
217
+ llmRunner: judge,
218
+ ...(typeof ref === "string" && ref ? { ref } : {}),
219
+ });
220
+ output("improve-judge", { engine: judge.engine, ...verdict });
221
+ },
222
+ });
223
+ /** The subcommands `setup` withheld for this run, restored by `cleanup` (one command runs per process; in-process tests run many). */
224
+ let subCommandsWithheld = false;
225
+ const IMPROVE_SUBCOMMANDS = { report: improveReportCommand, judge: improveJudgeCommand };
240
226
  export const improveCommand = defineCommand({
241
227
  meta: {
242
228
  name: "improve",
243
- description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when the selected strategy enables consolidate.",
229
+ description: "Analyze existing AKM assets and generate improvement proposals; also consolidates memories when the selected strategy enables consolidate. `improve report` and `improve judge` are subcommands.",
230
+ },
231
+ subCommands: IMPROVE_SUBCOMMANDS,
232
+ // `improve` takes a scope positional (an asset type or ref), and citty reads
233
+ // the first positional of a command with subcommands as a subcommand name,
234
+ // failing with an unknown command for a scope. So the subcommands stay
235
+ // registered only for an invocation that names one; any other, scope or none,
236
+ // is the improve run itself.
237
+ setup(context) {
238
+ const index = findCittyTopLevelCommandIndex(context.rawArgs, improveCommand.args);
239
+ const token = index >= 0 ? context.rawArgs[index] : undefined;
240
+ if (token !== undefined && token in IMPROVE_SUBCOMMANDS)
241
+ return;
242
+ subCommandsWithheld = true;
243
+ context.cmd.subCommands = undefined;
244
+ },
245
+ cleanup(context) {
246
+ if (!subCommandsWithheld)
247
+ return;
248
+ context.cmd.subCommands = IMPROVE_SUBCOMMANDS;
249
+ subCommandsWithheld = false;
244
250
  },
245
251
  // Declared explicitly: otherwise citty takes `--format`'s value as the scope.
246
252
  args: {
@@ -252,11 +258,6 @@ export const improveCommand = defineCommand({
252
258
  },
253
259
  task: { type: "string", description: "Add extra guidance for this improvement pass" },
254
260
  "dry-run": { type: "boolean", description: "Show planned actions without writing", default: false },
255
- plan: {
256
- type: "boolean",
257
- description: "Alias for --dry-run (#947). Sets the exact same internal flag; use it when previewing resolved process -> engine -> model routing (plan.processes) rather than checking what would write.",
258
- default: false,
259
- },
260
261
  bundle: {
261
262
  type: "string",
262
263
  description: "Bundle to improve and write proposals to (default: defaultWriteTarget, else the working bundle); only its assets are planned",
@@ -268,7 +269,7 @@ export const improveCommand = defineCommand({
268
269
  },
269
270
  "require-feedback-signal": {
270
271
  type: "boolean",
271
- description: "Turn the proactive/high-salience fallback lanes off (they only select and score assets; a rewrite needs negative feedback)",
272
+ description: "Turn the proactive-maintenance lane off for this run, so only assets with recent feedback are planned",
272
273
  default: false,
273
274
  },
274
275
  "json-to-stdout": {
@@ -291,52 +292,36 @@ export const improveCommand = defineCommand({
291
292
  description: "Print the composed reflect prompt for one asset ref and exit — no lock, index write, or engine dispatch (#952). Requires a fully-qualified asset ref as the scope positional (e.g. `akm improve lessons/my-lesson --show-prompt`). JSON/yaml format carries the prompt as a `prompt` field; text format prints it directly.",
292
293
  default: false,
293
294
  },
294
- run: {
295
- type: "string",
296
- description: 'Only with the "report" scope (`akm improve report --run <id>`): show the LLM usage/routing report for one specific improve_runs row instead of the most recent run. Mutually exclusive with --since.',
297
- },
298
- since: {
299
- type: "string",
300
- description: 'Only with the "report" scope (`akm improve report --since <window>`): aggregate the LLM usage/routing report over every real run started since <window> (a duration like "24h"/"7d", or an ISO timestamp) instead of showing one run. Mutually exclusive with --run.',
301
- },
302
295
  strategy: {
303
296
  type: "string",
304
- description: "Named improve strategy from improve.strategies or built-in strategies (catchup, consolidate, default, proactive-maintenance, quick, reflect-distill, thorough). Controls which sub-processes run and which asset types are processed.",
297
+ description: "Named improve strategy from improve.strategies or built-in strategies (consolidate, default). Controls which sub-processes run and which asset types are processed.",
305
298
  },
306
299
  sync: {
307
300
  type: "boolean",
308
- description: "Commit (and optionally push) the git-backed primary bundle when the run finishes. Use --no-sync to disable. Default: on for git-backed bundles (per profile config).",
301
+ description: "Sync the git-backed primary bundle like `akm sync` (fetch and merge the upstream, commit, push) before the run, and commit (and optionally push) when it finishes. Use --no-sync to disable. Default: on for git-backed bundles (per profile config).",
309
302
  },
310
303
  push: {
311
304
  type: "boolean",
312
- description: "Push after the end-of-run sync commit when writable + remote configured. Use --no-push to commit only. Default: per profile config (true).",
305
+ description: "Push after the pre-run and end-of-run sync commits when writable + remote configured. Use --no-push to commit only. Default: per profile config (true).",
313
306
  },
314
307
  },
315
308
  async run({ args }) {
316
309
  await runWithJsonErrors(async () => {
317
- if (getStringArg(args, "scope") === "report") {
318
- runImproveReportCli(args);
310
+ // citty runs this body after a subcommand it dispatched too.
311
+ if (hasSubcommand(args, new Set(Object.keys(IMPROVE_SUBCOMMANDS))))
319
312
  return;
320
- }
321
- rejectReportOnlyFlags(args);
322
- if (getStringArg(args, "scope") === "judge") {
323
- await runImproveJudgeCli(getStringArg(args, "strategy"));
324
- return;
325
- }
326
- rejectRetiredImproveTargetFlag();
327
313
  const jsonToStdout = args["json-to-stdout"];
328
314
  const targetArg = getStringArg(args, "bundle");
329
315
  const taskArg = getStringArg(args, "task");
330
- // `--plan` is an alias for `--dry-run`; `--show-prompt` is read-only too.
331
- const dryRun = args["dry-run"] || args.plan || args["show-prompt"];
316
+ // `--show-prompt` is read-only too.
317
+ const dryRun = args["dry-run"] || args["show-prompt"];
332
318
  const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
333
319
  const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
334
320
  const requireFeedbackSignal = args["require-feedback-signal"];
335
321
  const skipIfLocked = args["skip-if-locked"];
336
322
  const strategyArg = getStringArg(args, "strategy");
337
323
  const effectiveConfig = loadConfig();
338
- const scopeArg = resolveScopeAfterRetiredAutoAccept(getStringArg(args, "scope"));
339
- rejectRetiredCanaryScope(scopeArg);
324
+ const scopeArg = getStringArg(args, "scope");
340
325
  const scopeRef = scopeArg && isFullRefInput(scopeArg) ? parseRefInput(scopeArg) : undefined;
341
326
  const writeTarget = dryRun
342
327
  ? undefined
@@ -369,7 +354,7 @@ export const improveCommand = defineCommand({
369
354
  if (pushFlag !== undefined)
370
355
  syncOverride.push = pushFlag;
371
356
  if (!dryRun) {
372
- const improveLogFile = path.join(getCacheDir(), "logs", "improve", `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
357
+ const improveLogFile = path.join(getRunLogDir("improve"), `${new Date().toISOString().replace(/[:.]/g, "-")}.log`);
373
358
  setLogFile(improveLogFile);
374
359
  }
375
360
  const startedAtMs = Date.now();
@@ -419,6 +404,7 @@ export const improveCommand = defineCommand({
419
404
  ...(skipIfLocked ? { skipIfLocked } : {}),
420
405
  ...(strategyArg !== undefined ? { strategy: strategyArg } : {}),
421
406
  ...(engineProbe !== undefined ? { engineProbe } : {}),
407
+ onBudgetHardKill: () => persistTerminated("budget_exhausted"),
422
408
  ...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
423
409
  }),
424
410
  }, {
@@ -1,17 +1,12 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- import catchup from "../../assets/improve-strategies/catchup.json" with { type: "json" };
5
4
  import consolidate from "../../assets/improve-strategies/consolidate.json" with { type: "json" };
6
5
  import defaultStrategy from "../../assets/improve-strategies/default.json" with { type: "json" };
7
- import proactiveMaintenance from "../../assets/improve-strategies/proactive-maintenance.json" with { type: "json" };
8
- import quick from "../../assets/improve-strategies/quick.json" with { type: "json" };
9
- import reflectDistill from "../../assets/improve-strategies/reflect-distill.json" with { type: "json" };
10
- import thorough from "../../assets/improve-strategies/thorough.json" with { type: "json" };
11
6
  import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
12
7
  import { ImproveProfileConfigSchema } from "../../core/config/config-schema.js";
13
8
  import { deepMergeConfig } from "../../core/config/deep-merge.js";
14
- import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_ENGINE_PROCESSES, IMPROVE_PROCESS_NAMES, } from "../../core/config/engine-semantics.js";
9
+ import { BUILTIN_IMPROVE_STRATEGY_NAMES, IMPROVE_ENGINE_PROCESSES, IMPROVE_PROCESS_NAMES, REMOVED_IMPROVE_STRATEGY_NAMES, } from "../../core/config/engine-semantics.js";
15
10
  import { ConfigError } from "../../core/errors.js";
16
11
  import { describeLlmCredentialAvailability, } from "../../integrations/agent/engine-resolution.js";
17
12
  import { runnerLlmConnection } from "../../integrations/agent/runner.js";
@@ -69,12 +64,7 @@ export function isStrategyFilteredForAllPasses(ref, strategy) {
69
64
  }
70
65
  const BUILTIN_STRATEGIES = {
71
66
  default: defaultStrategy,
72
- quick,
73
- thorough,
74
67
  consolidate,
75
- catchup,
76
- "reflect-distill": reflectDistill,
77
- "proactive-maintenance": proactiveMaintenance,
78
68
  };
79
69
  if (BUILTIN_IMPROVE_STRATEGY_NAMES.some((name) => !(name in BUILTIN_STRATEGIES))) {
80
70
  throw new Error("Built-in improve strategy names are out of sync with their assets");
@@ -105,6 +95,11 @@ export function resolveImproveStrategy(name, config) {
105
95
  // shape this refusal exists to reject, not a way around it.
106
96
  "Choose a different strategy. `graph-refresh` cannot be redefined under `improve.strategies` — it always refuses.");
107
97
  }
98
+ if (!(selectedName in BUILTIN_STRATEGIES) &&
99
+ !userStrategies[selectedName] &&
100
+ REMOVED_IMPROVE_STRATEGY_NAMES.includes(selectedName)) {
101
+ throw new ConfigError(`Improve strategy "${selectedName}" was removed in 0.10. Use "default" (or "consolidate"), or define "${selectedName}" under improve.strategies to keep your own version.`, "UNKNOWN_IMPROVE_STRATEGY", `Run \`akm improve --strategy default\`, or add \`improve.strategies.${selectedName}\` to your config; a block of that name is a user-defined strategy that inherits \`default\`.`);
102
+ }
108
103
  if (!(selectedName in BUILTIN_STRATEGIES) && !userStrategies[selectedName]) {
109
104
  const valid = [...new Set([...Object.keys(BUILTIN_STRATEGIES), ...Object.keys(userStrategies)])].sort();
110
105
  throw new ConfigError(`Improve strategy "${selectedName}" not found. Valid strategies: ${valid.join(", ")}.`, "UNKNOWN_IMPROVE_STRATEGY");
@@ -304,7 +299,8 @@ function buildImprovePlan(strategy, config, options) {
304
299
  });
305
300
  }
306
301
  if (engineUnavailable.length > 0 &&
307
- !Object.values(processes).some((process) => process.enabled) &&
302
+ // proactiveMaintenance uses no engine: it is a selector, so it alone does not keep a run alive.
303
+ !IMPROVE_ENGINE_PROCESSES.some((name) => processes[name].enabled) &&
308
304
  (!options.allowAllDisabled || anyEngineNotConfigured)) {
309
305
  const names = engineUnavailable.map((item) => `"${item.process}"`).join(", ");
310
306
  throw new ConfigError(`No improve process can run: ${names} ${engineUnavailable.length === 1 ? "requires" : "require"} an engine that is not configured. Set defaults.llmEngine, or the per-process engine key named for each.`, "LLM_NOT_CONFIGURED");
@@ -28,7 +28,7 @@ import { resolveEntryContentDir, resolveSourceEntries } from "../../indexer/sear
28
28
  import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
29
29
  import { installLlmUsagePersistence } from "../../llm/usage-persist.js";
30
30
  import { withLlmStage } from "../../llm/usage-telemetry.js";
31
- import { isGitBackedStash, listGitChangedPaths, resolveWritableOverride, saveGitStash, } from "../../sources/providers/git.js";
31
+ import { GitStashPushError, isGitBackedStash, listGitChangedPaths, resolveWritableOverride, saveGitStash, } from "../../sources/providers/git.js";
32
32
  import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
33
33
  import { getEntryCount } from "../../storage/repositories/index-entries-repository.js";
34
34
  import { openSqliteReadSnapshot, SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
@@ -70,7 +70,8 @@ export function renderSyncCommitMessage(template, result, nowMs) {
70
70
  export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
71
71
  /**
72
72
  * Abort the run at the budget; force-exit (0 — budget exhaustion is a normal
73
- * scheduled-task outcome) only if the drain overruns the grace period.
73
+ * scheduled-task outcome) only if the drain overruns the grace period, after
74
+ * `onHardKill` has persisted what the killed run left behind.
74
75
  * Returns an idempotent disposer.
75
76
  */
76
77
  export function armBudgetWatchdog(budgetMs, controller, deps) {
@@ -81,7 +82,15 @@ export function armBudgetWatchdog(budgetMs, controller, deps) {
81
82
  let hardKillTimer;
82
83
  const budgetTimer = setTimeoutFn(() => {
83
84
  controller.abort("improve budget exhausted");
84
- hardKillTimer = setTimeoutFn(() => exitFn(0), hardKillGraceMs);
85
+ hardKillTimer = setTimeoutFn(() => {
86
+ try {
87
+ deps?.onHardKill?.();
88
+ }
89
+ catch {
90
+ // Best-effort: the forced exit must still happen.
91
+ }
92
+ exitFn(0);
93
+ }, hardKillGraceMs);
85
94
  hardKillTimer.unref?.();
86
95
  }, budgetMs);
87
96
  return () => {
@@ -101,6 +110,7 @@ export async function akmImprove(options = {}) {
101
110
  // Set by the usage sink when any engine call terminates, prepass included.
102
111
  let firstEngineResponseSeen = false;
103
112
  let initialGitPaths = new Set();
113
+ let preRunSync;
104
114
  // The write-provenance journal (#652) spans exactly the window this run holds the lock.
105
115
  let journal;
106
116
  const closeJournal = () => {
@@ -155,7 +165,20 @@ export async function akmImprove(options = {}) {
155
165
  });
156
166
  if (!options.dryRun) {
157
167
  const remainingBudget = budgetAbortController.signal.remainingBudgetMs;
158
- clearBudgetTimer = armBudgetWatchdog(Math.max(1, remainingBudget ?? budgetMs), budgetAbortController);
168
+ clearBudgetTimer = armBudgetWatchdog(Math.max(1, remainingBudget ?? budgetMs), budgetAbortController, {
169
+ // The forced exit skips every finally and the CLI catch: leave a terminated
170
+ // run record and bank the files this run wrote (journal is in memory) first.
171
+ // Local commit only — a push to a dead remote must not hang the exit.
172
+ onHardKill: () => {
173
+ try {
174
+ options.onBudgetHardKill?.();
175
+ }
176
+ catch {
177
+ // best-effort
178
+ }
179
+ commitStashBatch({ scope, plannedRefs: collected?.plannedRefs ?? [], runId: options.runId }, { push: false });
180
+ },
181
+ });
159
182
  }
160
183
  try {
161
184
  if (!options.dryRun) {
@@ -174,6 +197,9 @@ export async function akmImprove(options = {}) {
174
197
  });
175
198
  exitBackstop = releaseRunLock;
176
199
  process.on("exit", exitBackstop);
200
+ // Pull in what another host pushed before anything reads the bundle (#1091). This also commits
201
+ // pending managed-path changes, as `akm sync` does, so they are not mistaken for this run's writes.
202
+ preRunSync = syncBeforeRun(setup, eventsCtx);
177
203
  initialGitPaths =
178
204
  syncRepoDir && isGitBackedStash(syncRepoDir) ? new Set(listGitChangedPaths(syncRepoDir)) : new Set();
179
205
  journal = beginWriteProvenance();
@@ -246,6 +272,8 @@ export async function akmImprove(options = {}) {
246
272
  if (writtenPaths.length > 0)
247
273
  result.writtenPaths = writtenPaths;
248
274
  result.sync = commitStashBatch(result);
275
+ if (preRunSync)
276
+ result.preRunSync = preRunSync;
249
277
  return result;
250
278
  }
251
279
  catch (err) {
@@ -654,7 +682,7 @@ function buildResultExecutionPlan(run, preparation, collected, dryRun) {
654
682
  enabled: scope.mode !== "ref" && resolvedPlan.processes.triage.enabled,
655
683
  configuredMode: configuredTriage?.applyMode ?? "queue",
656
684
  mode: improveProfile.processes?.triage?.applyMode ?? "queue",
657
- maxAcceptsPerRun: configuredTriage?.maxAcceptsPerRun ?? 25,
685
+ maxAcceptsPerRun: configuredTriage?.maxAcceptsPerRun ?? 200,
658
686
  },
659
687
  });
660
688
  }
@@ -674,7 +702,7 @@ async function runTriagePrePass(run) {
674
702
  ...(options.target ? { target: options.target } : {}),
675
703
  config: options.config,
676
704
  applyMode: triageConfig?.applyMode ?? "queue",
677
- maxAccepts: triageConfig?.maxAcceptsPerRun ?? 25,
705
+ maxAccepts: triageConfig?.maxAcceptsPerRun ?? 200,
678
706
  dryRun: false,
679
707
  excludeIds: new Set(),
680
708
  judgment: resolvedPlan.triageJudgment,
@@ -712,60 +740,114 @@ export function resolveSyncPathSet(input) {
712
740
  unattributed: changedPaths.filter((p) => !attributed.has(p) && !initialPaths.has(p) && inScope(p)).sort(),
713
741
  };
714
742
  }
743
+ /**
744
+ * One `saveGitStash` call for the run's write-target bundle, shared by the
745
+ * pre-run sync and the auto-sync commit: never throws, records a `stash_synced`
746
+ * event, and returns undefined when the bundle is not git-backed, there is no
747
+ * primary stash, or sync is disabled (`--no-sync` / `sync.enabled: false`).
748
+ * `scopePaths` runs inside the guard; leaving `paths` undefined means what
749
+ * `akm sync` stages (the managed paths).
750
+ */
751
+ function runStashSync(run, eventsCtx, phase, input) {
752
+ const { writeTarget, primaryStashDir, effectiveSync, options, config, improveProfile } = run;
753
+ const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
754
+ if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
755
+ return undefined;
756
+ }
757
+ const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
758
+ const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(config);
759
+ const push = input.push ?? options.sync?.push ?? improveProfile.sync?.push ?? true;
760
+ const record = (metadata) => appendEvent({ eventType: "stash_synced", metadata: { phase, ...metadata } }, eventsCtx);
761
+ try {
762
+ const { paths, unattributed } = input.scopePaths({
763
+ repoDir,
764
+ assetRoot: writeTarget?.source.path ?? primaryStashDir,
765
+ });
766
+ const syncResult = saveGitStashFn(undefined, input.message, writableOverride, {
767
+ push,
768
+ repoDir,
769
+ ...(paths ? { paths } : {}),
770
+ });
771
+ record({
772
+ committed: syncResult.committed,
773
+ pushed: syncResult.pushed,
774
+ skipped: syncResult.skipped,
775
+ reason: syncResult.reason ?? null,
776
+ ...(paths ? { attributed: paths.length, unattributed: unattributed ?? 0 } : {}),
777
+ });
778
+ return {
779
+ committed: syncResult.committed,
780
+ pushed: syncResult.pushed,
781
+ skipped: syncResult.skipped,
782
+ ...(syncResult.reason !== undefined ? { reason: syncResult.reason } : {}),
783
+ };
784
+ }
785
+ catch (syncErr) {
786
+ const reason = errMessage(syncErr);
787
+ warn(`improve: stash sync failed (non-fatal): ${reason}`);
788
+ // A push that failed after the commit landed still left the commit in the repo.
789
+ const committed = syncErr instanceof GitStashPushError;
790
+ record({ committed, pushed: false, skipped: true, reason });
791
+ return { committed, pushed: false, skipped: true, reason };
792
+ }
793
+ }
794
+ /**
795
+ * The pre-run sync (#1091): the same fetch / fast-forward-or-merge / commit /
796
+ * push `akm sync` runs for the write-target bundle, before the index and plan
797
+ * stages so they see what another host pushed. A conflict or fetch failure is
798
+ * a warning, never an abort: the run continues on the local state.
799
+ */
800
+ function syncBeforeRun(run, eventsCtx) {
801
+ const outcome = runStashSync(run, eventsCtx, "pre-run", {
802
+ message: "akm improve pre-run sync",
803
+ scopePaths: ({ repoDir, assetRoot }) => {
804
+ // A bundle whose content sits in a subdirectory of the repo stages only that subdirectory, as
805
+ // `akm sync` does for a content root; a root-level bundle leaves `paths` to the managed-path default.
806
+ const assetPrefix = path.relative(repoDir, assetRoot).replaceAll(path.sep, "/");
807
+ if (!assetPrefix)
808
+ return {};
809
+ return resolveSyncPathSet({
810
+ repoDir,
811
+ assetPrefix,
812
+ changedPaths: listGitChangedPaths(repoDir),
813
+ initialPaths: new Set(),
814
+ writtenPaths: [],
815
+ provenance: false,
816
+ });
817
+ },
818
+ });
819
+ if (outcome?.reason)
820
+ warn(`[improve] pre-run sync: ${outcome.reason}; continuing on the local state`);
821
+ return outcome;
822
+ }
715
823
  /**
716
824
  * The auto-sync commit (#662), used at end of run and from the crash path:
717
825
  * idempotent and never throws. The getters read bindings reassigned after this
718
826
  * factory runs.
719
827
  */
720
828
  function makeCommitStashBatch(deps) {
721
- const { writeTarget, primaryStashDir, effectiveSync, options, config, improveProfile } = deps.run;
722
- return (messageContext) => {
723
- const eventsCtx = deps.getEventsCtx();
829
+ const { effectiveSync } = deps.run;
830
+ return (messageContext, override) => {
724
831
  const writeJournal = deps.getWriteJournal();
725
- const repoDir = writeTarget?.source.repoPath ?? primaryStashDir;
726
- if (!primaryStashDir || !repoDir || effectiveSync.enabled === false || !isGitBackedStash(repoDir)) {
727
- return undefined;
728
- }
729
- const saveGitStashFn = options.saveGitStashFn ?? saveGitStash;
730
- const writableOverride = writeTarget ? resolveWritable(writeTarget.config) : resolveWritableOverride(config);
731
- const push = options.sync?.push ?? improveProfile.sync?.push ?? true;
732
832
  const message = renderSyncCommitMessage(effectiveSync.message ?? "akm improve auto-sync", messageContext, Date.now());
733
- const record = (metadata) => appendEvent({ eventType: "stash_synced", metadata }, eventsCtx);
734
- try {
735
- const assetRoot = writeTarget?.source.path ?? primaryStashDir;
736
- const { paths, unattributed } = resolveSyncPathSet({
737
- repoDir,
738
- assetPrefix: path.relative(repoDir, assetRoot).replaceAll(path.sep, "/"),
739
- changedPaths: listGitChangedPaths(repoDir),
740
- initialPaths: deps.getInitialGitPaths(),
741
- writtenPaths: writeJournal?.writtenPaths() ?? [],
742
- provenance: writeJournal !== undefined,
743
- });
744
- if (unattributed.length > 0) {
745
- warnVerbose(`[improve] auto-sync left ${unattributed.length} path(s) uncommitted — not written by this run: ${unattributed.join(", ")}`);
746
- }
747
- const syncResult = saveGitStashFn(undefined, message, writableOverride, { push, repoDir, paths });
748
- record({
749
- committed: syncResult.committed,
750
- pushed: syncResult.pushed,
751
- skipped: syncResult.skipped,
752
- reason: syncResult.reason ?? null,
753
- attributed: paths.length,
754
- unattributed: unattributed.length,
755
- });
756
- return {
757
- committed: syncResult.committed,
758
- pushed: syncResult.pushed,
759
- skipped: syncResult.skipped,
760
- ...(syncResult.reason !== undefined ? { reason: syncResult.reason } : {}),
761
- };
762
- }
763
- catch (syncErr) {
764
- const reason = errMessage(syncErr);
765
- warn(`improve: stash sync failed (non-fatal): ${reason}`);
766
- record({ committed: false, pushed: false, skipped: true, reason });
767
- return { committed: false, pushed: false, skipped: true, reason };
768
- }
833
+ return runStashSync(deps.run, deps.getEventsCtx(), "end-of-run", {
834
+ message,
835
+ push: override?.push,
836
+ scopePaths: ({ repoDir, assetRoot }) => {
837
+ const { paths, unattributed } = resolveSyncPathSet({
838
+ repoDir,
839
+ assetPrefix: path.relative(repoDir, assetRoot).replaceAll(path.sep, "/"),
840
+ changedPaths: listGitChangedPaths(repoDir),
841
+ initialPaths: deps.getInitialGitPaths(),
842
+ writtenPaths: writeJournal?.writtenPaths() ?? [],
843
+ provenance: writeJournal !== undefined,
844
+ });
845
+ if (unattributed.length > 0) {
846
+ warnVerbose(`[improve] auto-sync left ${unattributed.length} path(s) uncommitted — not written by this run: ${unattributed.join(", ")}`);
847
+ }
848
+ return { paths, unattributed: unattributed.length };
849
+ },
850
+ });
769
851
  };
770
852
  }
771
853
  /**