akm-cli 0.9.0-rc.4 → 0.9.0-rc.6

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 (438) hide show
  1. package/CHANGELOG.md +122 -26
  2. package/README.md +7 -7
  3. package/SECURITY.md +11 -10
  4. package/dist/akm +2 -2
  5. package/dist/akm-migrate-storage +2 -2
  6. package/dist/assets/hints/cli-hints-full.md +60 -73
  7. package/dist/assets/hints/cli-hints-short.md +8 -12
  8. package/dist/assets/improve-strategies/default.json +5 -7
  9. package/dist/assets/improve-strategies/frequent.json +2 -2
  10. package/dist/assets/improve-strategies/proactive-maintenance.json +2 -2
  11. package/dist/assets/improve-strategies/quick.json +1 -1
  12. package/dist/assets/improve-strategies/reflect-distill.json +3 -3
  13. package/dist/assets/improve-strategies/thorough.json +1 -1
  14. package/dist/assets/prompts/consolidate-system.md +5 -5
  15. package/dist/assets/prompts/extract-session.md +2 -6
  16. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  17. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  18. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  19. package/dist/assets/stash-skeleton/README.md +10 -11
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +1 -1
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +1 -1
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +1 -1
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -2
  24. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -2
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +1 -1
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -2
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +1 -1
  28. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +18 -18
  29. package/dist/assets/stash-skeleton/facts/conventions/domains.md +8 -9
  30. package/dist/assets/stash-skeleton/facts/conventions/organization.md +13 -15
  31. package/dist/assets/tasks/core/extract.yml +2 -2
  32. package/dist/assets/tasks/core/improve.yml +1 -1
  33. package/dist/assets/templates/html/health.html +2 -3
  34. package/dist/cli/config-migrate.js +1328 -101
  35. package/dist/cli/invocation.js +255 -0
  36. package/dist/cli/parse-args.js +20 -83
  37. package/dist/cli/shared.js +15 -37
  38. package/dist/cli.js +18 -49
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/contribute-cli.js +5 -4
  41. package/dist/commands/bundle/bundle-cli.js +68 -0
  42. package/dist/commands/bundle/bundle.js +219 -0
  43. package/dist/commands/completions.js +2 -2
  44. package/dist/commands/config-cli.js +6 -3
  45. package/dist/commands/env/env-binding.js +13 -9
  46. package/dist/commands/env/env-cli.js +49 -48
  47. package/dist/commands/env/secret-cli.js +24 -10
  48. package/dist/commands/events.js +2 -2
  49. package/dist/commands/feedback-cli.js +59 -33
  50. package/dist/commands/graph/graph.js +6 -4
  51. package/dist/commands/health/advisories.js +4 -4
  52. package/dist/commands/health/html-report.js +190 -593
  53. package/dist/commands/health/improve-metrics.js +240 -188
  54. package/dist/commands/health/metrics.js +9 -17
  55. package/dist/commands/health/report-view-model.js +509 -0
  56. package/dist/commands/health/surfaces.js +6 -3
  57. package/dist/commands/health/types-checks.js +4 -0
  58. package/dist/commands/health/types-improve.js +22 -0
  59. package/dist/commands/health/types-metrics.js +4 -0
  60. package/dist/commands/health/types-result.js +7 -0
  61. package/dist/commands/health/types-runs.js +4 -0
  62. package/dist/commands/health/types-session-log.js +4 -0
  63. package/dist/commands/health/types-windows.js +4 -0
  64. package/dist/commands/health/types.js +26 -21
  65. package/dist/commands/health/windows.js +1 -2
  66. package/dist/commands/health.js +219 -161
  67. package/dist/commands/improve/collapse-detector.js +25 -14
  68. package/dist/commands/improve/consolidate/chunking.js +7 -5
  69. package/dist/commands/improve/consolidate.js +640 -740
  70. package/dist/commands/improve/content-hash.js +39 -0
  71. package/dist/commands/improve/distill/content-repair.js +4 -10
  72. package/dist/commands/improve/distill/promote-memory.js +79 -49
  73. package/dist/commands/improve/distill/quality-gate.js +113 -35
  74. package/dist/commands/improve/distill-promotion-policy.js +24 -885
  75. package/dist/commands/improve/distill.js +565 -337
  76. package/dist/commands/improve/eligibility.js +75 -36
  77. package/dist/commands/improve/extract-cli.js +6 -6
  78. package/dist/commands/improve/extract-prompt.js +5 -34
  79. package/dist/commands/improve/extract.js +526 -357
  80. package/dist/commands/improve/feedback-valence.js +2 -12
  81. package/dist/commands/improve/improve-cli.js +11 -20
  82. package/dist/commands/improve/improve-result-file.js +7 -6
  83. package/dist/commands/improve/improve-run-types.js +4 -0
  84. package/dist/commands/improve/improve-strategies.js +3 -9
  85. package/dist/commands/improve/improve.js +822 -727
  86. package/dist/commands/improve/locks.js +27 -95
  87. package/dist/commands/improve/loop-stages.js +941 -963
  88. package/dist/commands/improve/memory/derived-ref.js +122 -0
  89. package/dist/commands/improve/memory/memory-contradiction-detect.js +35 -38
  90. package/dist/commands/improve/memory/memory-improve.js +24 -37
  91. package/dist/commands/improve/outcome-loop.js +13 -33
  92. package/dist/commands/improve/preparation.js +889 -691
  93. package/dist/commands/improve/proactive-maintenance.js +37 -9
  94. package/dist/commands/improve/proposal-envelope.js +31 -0
  95. package/dist/commands/improve/reflect.js +974 -588
  96. package/dist/commands/improve/run-context.js +119 -0
  97. package/dist/commands/improve/salience.js +8 -4
  98. package/dist/commands/improve/session-asset.js +7 -3
  99. package/dist/commands/improve/shared.js +14 -40
  100. package/dist/commands/improve/source-identity.js +39 -26
  101. package/dist/commands/improve/triage.js +20 -17
  102. package/dist/commands/lint/base-linter.js +288 -295
  103. package/dist/commands/lint/index.js +132 -19
  104. package/dist/commands/mv-cli.js +257 -234
  105. package/dist/commands/observability-cli.js +2 -1
  106. package/dist/commands/proposal/diff-format.js +50 -0
  107. package/dist/commands/proposal/drain-policies.js +0 -6
  108. package/dist/commands/proposal/drain.js +17 -16
  109. package/dist/commands/proposal/proposal-cli.js +40 -77
  110. package/dist/commands/proposal/proposal-types.js +56 -0
  111. package/dist/commands/proposal/proposal.js +39 -1
  112. package/dist/commands/proposal/propose.js +41 -19
  113. package/dist/commands/proposal/repository.js +566 -486
  114. package/dist/commands/proposal/validators/proposal-quality-validators.js +22 -6
  115. package/dist/commands/proposal/validators/proposal-validators.js +6 -5
  116. package/dist/commands/proposal/validators/proposals.js +5 -4
  117. package/dist/commands/read/curate.js +50 -28
  118. package/dist/commands/read/knowledge.js +25 -16
  119. package/dist/commands/read/remember-cli.js +12 -4
  120. package/dist/commands/read/search-cli.js +33 -33
  121. package/dist/commands/read/search.js +68 -55
  122. package/dist/commands/read/show.js +54 -135
  123. package/dist/commands/remember.js +7 -5
  124. package/dist/commands/sources/add-cli.js +10 -24
  125. package/dist/commands/sources/bundle-config-ops.js +58 -0
  126. package/dist/commands/sources/history.js +13 -8
  127. package/dist/commands/sources/info.js +10 -11
  128. package/dist/commands/sources/init.js +12 -21
  129. package/dist/commands/sources/installed-stashes.js +151 -138
  130. package/dist/commands/sources/schema-repair.js +5 -3
  131. package/dist/commands/sources/self-update.js +9 -7
  132. package/dist/commands/sources/source-add.js +116 -154
  133. package/dist/commands/sources/source-clone.js +33 -14
  134. package/dist/commands/sources/source-manage.js +43 -32
  135. package/dist/commands/sources/sources-cli.js +12 -7
  136. package/dist/commands/sources/stash-cli.js +12 -5
  137. package/dist/commands/sources/stash-skeleton.js +1 -1
  138. package/dist/commands/tasks/default-tasks.js +13 -13
  139. package/dist/commands/tasks/tasks-cli.js +3 -3
  140. package/dist/commands/tasks/tasks.js +71 -31
  141. package/dist/commands/workflow-cli.js +22 -26
  142. package/dist/core/action-contributors.js +1 -1
  143. package/dist/core/activation-policy.js +49 -0
  144. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  145. package/dist/core/adapter/adapters/akm-adapter.js +509 -0
  146. package/dist/core/adapter/adapters/akm-lint.js +381 -0
  147. package/dist/core/adapter/adapters/akm-metadata.js +416 -0
  148. package/dist/core/adapter/adapters/akm-task-adapter.js +150 -0
  149. package/dist/core/adapter/adapters/akm-workflow-adapter.js +196 -0
  150. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  151. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  152. package/dist/core/adapter/adapters/generic-files-adapter.js +117 -0
  153. package/dist/core/adapter/adapters/index.js +80 -0
  154. package/dist/core/adapter/adapters/llm-wiki-adapter.js +413 -0
  155. package/dist/core/adapter/adapters/okf-adapter.js +223 -0
  156. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  157. package/dist/core/adapter/adapters/shared.js +286 -0
  158. package/dist/core/adapter/adapters/tool-dir-shared.js +212 -0
  159. package/dist/core/adapter/adapters/website-snapshot-adapter.js +154 -0
  160. package/dist/core/adapter/bundle-adapter.js +4 -0
  161. package/dist/core/adapter/recognize-match.js +42 -0
  162. package/dist/core/adapter/registry.js +56 -0
  163. package/dist/core/adapter/types.js +4 -0
  164. package/dist/core/asset/asset-placement.js +229 -0
  165. package/dist/core/asset/asset-ref.js +107 -78
  166. package/dist/core/asset/asset-serialize.js +20 -0
  167. package/dist/core/asset/frontmatter.js +1 -1
  168. package/dist/core/asset/resolve-ref.js +223 -0
  169. package/dist/core/asset/stash-meta.js +1 -1
  170. package/dist/core/common.js +141 -49
  171. package/dist/core/config/config-io.js +0 -108
  172. package/dist/core/config/config-schema.js +94 -1039
  173. package/dist/core/config/config-sources.js +115 -36
  174. package/dist/core/config/config-types.js +16 -11
  175. package/dist/core/config/config-walker.js +4 -2
  176. package/dist/core/config/config.js +21 -26
  177. package/dist/core/config/engine-semantics.js +0 -4
  178. package/dist/core/config/schema/embedding.js +38 -0
  179. package/dist/core/config/schema/engines.js +116 -0
  180. package/dist/core/config/schema/feedback.js +31 -0
  181. package/dist/core/config/schema/improve-processes.js +395 -0
  182. package/dist/core/config/schema/improve.js +76 -0
  183. package/dist/core/config/schema/index-config.js +183 -0
  184. package/dist/core/config/schema/output.js +15 -0
  185. package/dist/core/config/schema/primitives.js +94 -0
  186. package/dist/core/config/schema/search.js +30 -0
  187. package/dist/core/config/schema/setup.js +27 -0
  188. package/dist/core/config/schema/sources-bundles.js +164 -0
  189. package/dist/core/config/schema/workflow.js +29 -0
  190. package/dist/core/env-secret-ref.js +113 -8
  191. package/dist/core/errors.js +1 -1
  192. package/dist/core/events-types.js +4 -0
  193. package/dist/core/extra-params.js +1 -0
  194. package/dist/core/file-change.js +16 -0
  195. package/dist/core/fs-txn.js +316 -0
  196. package/dist/core/git-message.js +59 -0
  197. package/dist/core/improve-result.js +9 -1
  198. package/dist/core/logs-db.js +1 -1
  199. package/dist/core/maintenance-barrier.js +16 -0
  200. package/dist/core/migration-backup.js +332 -160
  201. package/dist/core/paths.js +3 -6
  202. package/dist/core/platform.js +10 -0
  203. package/dist/core/recognition-util.js +130 -0
  204. package/dist/core/redaction.js +2 -2
  205. package/dist/core/standards/resolve-standards-context.js +47 -64
  206. package/dist/core/standards/resolve-type-conventions.js +3 -3
  207. package/dist/core/state/migrations.js +201 -8
  208. package/dist/core/state-db.js +38 -2
  209. package/dist/core/subprocess.js +303 -0
  210. package/dist/core/time.js +20 -0
  211. package/dist/core/type-presentation.js +130 -0
  212. package/dist/core/write-source.js +39 -67
  213. package/dist/indexer/bundle-identity-guard.js +91 -0
  214. package/dist/indexer/db/graph-db.js +1 -1
  215. package/dist/indexer/db/llm-cache.js +1 -1
  216. package/dist/indexer/ensure-index.js +29 -9
  217. package/dist/indexer/graph/graph-boost.js +28 -24
  218. package/dist/indexer/graph/graph-extraction.js +5 -6
  219. package/dist/indexer/graph/graph-types.js +4 -0
  220. package/dist/indexer/index-written-assets.js +44 -17
  221. package/dist/indexer/indexer.js +289 -153
  222. package/dist/indexer/init.js +18 -25
  223. package/dist/indexer/installations.js +224 -0
  224. package/dist/indexer/passes/dir-staleness.js +2 -1
  225. package/dist/indexer/passes/memory-inference.js +8 -4
  226. package/dist/indexer/passes/metadata.js +96 -243
  227. package/dist/indexer/scan/doc-to-entry.js +123 -0
  228. package/dist/indexer/scan/drain-dir.js +144 -0
  229. package/dist/indexer/search/db-search.js +171 -99
  230. package/dist/indexer/search/fts-query.js +1 -1
  231. package/dist/indexer/search/ranking-contributors.js +57 -7
  232. package/dist/indexer/search/ranking-types.js +4 -0
  233. package/dist/indexer/search/ranking.js +31 -6
  234. package/dist/indexer/search/search-attribution.js +67 -0
  235. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  236. package/dist/indexer/search/search-source.js +109 -52
  237. package/dist/indexer/search/semantic-status.js +4 -1
  238. package/dist/indexer/usage/unmigrated-vaults-guard.js +2 -1
  239. package/dist/indexer/usage/usage-events.js +72 -9
  240. package/dist/indexer/walk/file-context.js +1 -44
  241. package/dist/indexer/walk/matchers.js +16 -32
  242. package/dist/indexer/walk/path-resolver.js +6 -5
  243. package/dist/indexer/walk/walker.js +4 -2
  244. package/dist/integrations/agent/engine-resolution.js +22 -0
  245. package/dist/integrations/agent/model-aliases.js +1 -1
  246. package/dist/integrations/agent/prompts.js +35 -11
  247. package/dist/integrations/agent/spawn.js +41 -270
  248. package/dist/integrations/harnesses/aider/agent-builder.js +2 -2
  249. package/dist/integrations/harnesses/aider/index.js +2 -11
  250. package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -9
  251. package/dist/integrations/harnesses/amazonq/index.js +3 -16
  252. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  253. package/dist/integrations/harnesses/claude/index.js +1 -12
  254. package/dist/integrations/harnesses/claude/session-log.js +27 -65
  255. package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
  256. package/dist/integrations/harnesses/codex/index.js +2 -12
  257. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  258. package/dist/integrations/harnesses/copilot/index.js +1 -12
  259. package/dist/integrations/harnesses/gemini/agent-builder.js +1 -3
  260. package/dist/integrations/harnesses/gemini/index.js +1 -12
  261. package/dist/integrations/harnesses/ids.js +24 -0
  262. package/dist/integrations/harnesses/index.js +27 -2
  263. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  264. package/dist/integrations/harnesses/opencode/index.js +1 -12
  265. package/dist/integrations/harnesses/opencode/session-log.js +67 -110
  266. package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -11
  267. package/dist/integrations/harnesses/openhands/agent-builder.js +3 -3
  268. package/dist/integrations/harnesses/openhands/index.js +2 -11
  269. package/dist/integrations/harnesses/pi/agent-builder.js +3 -11
  270. package/dist/integrations/harnesses/pi/index.js +3 -15
  271. package/dist/integrations/harnesses/shared.js +17 -0
  272. package/dist/integrations/harnesses/types.js +37 -2
  273. package/dist/integrations/lockfile.js +36 -0
  274. package/dist/integrations/session-logs/index.js +21 -12
  275. package/dist/integrations/session-logs/provider-base.js +113 -0
  276. package/dist/llm/client.js +7 -5
  277. package/dist/llm/embedders/deterministic.js +2 -2
  278. package/dist/llm/embedders/remote.js +8 -4
  279. package/dist/llm/graph-extract.js +7 -2
  280. package/dist/llm/metadata-enhance.js +43 -26
  281. package/dist/llm/structured-call.js +15 -7
  282. package/dist/migrate/legacy/config-source-migration.js +223 -0
  283. package/dist/migrate/legacy/content-migration.js +305 -0
  284. package/dist/migrate/legacy/legacy-layout.js +779 -0
  285. package/dist/migrate/legacy/legacy-paths.js +25 -0
  286. package/dist/migrate/legacy/legacy-stash-json.js +72 -0
  287. package/dist/migrate/legacy/proposal-fs-import.js +168 -0
  288. package/dist/migrate/legacy/task-target-ref-migration.js +272 -0
  289. package/dist/migrate/legacy/three-db-cutover.js +840 -0
  290. package/dist/migrate/legacy/workflow-migrations-bodies.js +52 -0
  291. package/dist/migrate/legacy/workflow-migrations-frozen.js +21 -0
  292. package/dist/migrate/legacy-ref-grammar.js +209 -0
  293. package/dist/output/command-registry.js +27 -0
  294. package/dist/output/html-render.js +11 -16
  295. package/dist/output/renderers.js +32 -276
  296. package/dist/output/shapes/passthrough.js +3 -9
  297. package/dist/output/shapes/registry.js +12 -6
  298. package/dist/output/text/command-format.js +547 -0
  299. package/dist/output/text/helpers.js +15 -1377
  300. package/dist/output/text/proposal-format.js +230 -0
  301. package/dist/output/text/registry.js +12 -6
  302. package/dist/output/text/show-directives.js +107 -0
  303. package/dist/output/text/show-format.js +103 -0
  304. package/dist/output/text/workflow-format.js +345 -0
  305. package/dist/output/text.js +1 -3
  306. package/dist/registry/build-index.js +13 -17
  307. package/dist/registry/providers/static-index.js +2 -2
  308. package/dist/registry/resolve.js +4 -90
  309. package/dist/registry/semver.js +93 -0
  310. package/dist/runtime.js +90 -0
  311. package/dist/schemas/akm-config.json +2956 -14890
  312. package/dist/schemas/akm-task.json +2 -2
  313. package/dist/schemas/akm-workflow.json +2 -2
  314. package/dist/scripts/migrate-storage.js +2312 -19642
  315. package/dist/setup/detect.js +5 -7
  316. package/dist/setup/engine-config.js +2 -1
  317. package/dist/setup/registry-stash-loader.js +1 -1
  318. package/dist/setup/semantic-assets.js +12 -9
  319. package/dist/setup/setup.js +109 -39
  320. package/dist/setup/steps/connection-shared.js +120 -0
  321. package/dist/setup/steps/connection.js +50 -274
  322. package/dist/setup/steps/platforms.js +1 -0
  323. package/dist/setup/steps/sources.js +13 -6
  324. package/dist/setup/steps/stashdir.js +5 -2
  325. package/dist/sources/freshness.js +39 -0
  326. package/dist/sources/providers/git-provider.js +29 -33
  327. package/dist/sources/providers/git-stash.js +10 -5
  328. package/dist/sources/providers/provider-utils.js +40 -18
  329. package/dist/sources/providers/website.js +1 -1
  330. package/dist/sources/resolve.js +5 -5
  331. package/dist/sources/snapshot-fetchers/types.js +4 -0
  332. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +105 -39
  333. package/dist/storage/database.js +47 -3
  334. package/dist/storage/engines/sqlite-migrations.js +34 -16
  335. package/dist/storage/locations.js +1 -2
  336. package/dist/storage/repositories/improve-runs-repository.js +0 -20
  337. package/dist/storage/repositories/index-connection.js +80 -0
  338. package/dist/storage/repositories/index-db.js +4 -3
  339. package/dist/storage/repositories/index-entries-repository.js +952 -0
  340. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +15 -2
  341. package/dist/storage/repositories/index-entry-types.js +4 -0
  342. package/dist/storage/repositories/index-fts-repository.js +164 -0
  343. package/dist/storage/repositories/index-llm-cache-repository.js +109 -0
  344. package/dist/storage/repositories/index-meta-repository.js +50 -0
  345. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +241 -100
  346. package/dist/storage/repositories/index-sql.js +12 -0
  347. package/dist/storage/repositories/index-utility-repository.js +369 -0
  348. package/dist/storage/repositories/index-vec-repository.js +245 -0
  349. package/dist/storage/repositories/proposals-repository.js +41 -50
  350. package/dist/storage/repositories/registry-cache.js +1 -1
  351. package/dist/storage/repositories/workflow-runs-repository.js +17 -11
  352. package/dist/tasks/backends/cron.js +5 -5
  353. package/dist/tasks/backends/exec-utils.js +21 -0
  354. package/dist/tasks/backends/index.js +6 -9
  355. package/dist/tasks/backends/launchd.js +16 -17
  356. package/dist/tasks/backends/schtasks.js +33 -41
  357. package/dist/tasks/backends/types.js +4 -0
  358. package/dist/tasks/parser.js +25 -5
  359. package/dist/tasks/runner.js +33 -39
  360. package/dist/tasks/validator.js +5 -6
  361. package/dist/workflows/authoring/authoring.js +8 -11
  362. package/dist/workflows/exec/brief.js +10 -7
  363. package/dist/workflows/exec/report.js +261 -156
  364. package/dist/workflows/exec/run-workflow.js +2 -1
  365. package/dist/workflows/exec/step-work.js +3 -9
  366. package/dist/workflows/program/parser.js +4 -60
  367. package/dist/workflows/renderer.js +11 -60
  368. package/dist/workflows/runtime/checkin.js +1 -1
  369. package/dist/workflows/runtime/plan-classifier.js +7 -4
  370. package/dist/workflows/runtime/runs.js +11 -10
  371. package/dist/workflows/runtime/unit-checkin.js +1 -1
  372. package/dist/workflows/runtime/unit-phases.js +20 -0
  373. package/dist/workflows/runtime/workflow-asset-loader.js +27 -16
  374. package/dist/workflows/validate-summary.js +2 -2
  375. package/dist/workflows/validator.js +18 -2
  376. package/docs/migration/release-notes/0.9.0.md +45 -3
  377. package/docs/migration/v0.8-to-v0.9.md +466 -262
  378. package/docs/{data-and-telemetry.md → reference/data-and-telemetry.md} +61 -17
  379. package/package.json +10 -9
  380. package/schemas/akm-config.json +2956 -14890
  381. package/schemas/akm-task.json +2 -2
  382. package/schemas/akm-workflow.json +2 -2
  383. package/dist/assets/help/help-accept.md +0 -12
  384. package/dist/assets/help/help-improve.md +0 -84
  385. package/dist/assets/help/help-proposals.md +0 -17
  386. package/dist/assets/help/help-propose.md +0 -17
  387. package/dist/assets/help/help-reject.md +0 -11
  388. package/dist/assets/improve-strategies/recombine-only.json +0 -23
  389. package/dist/assets/improve-strategies/synthesize.json +0 -17
  390. package/dist/assets/prompts/procedural-system.md +0 -44
  391. package/dist/assets/prompts/recombine-system.md +0 -40
  392. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  393. package/dist/assets/templates/html/default.html +0 -78
  394. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  395. package/dist/assets/wiki/index-template.md +0 -12
  396. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  397. package/dist/assets/wiki/log-template.md +0 -8
  398. package/dist/assets/wiki/schema-template.md +0 -61
  399. package/dist/commands/improve/calibration.js +0 -161
  400. package/dist/commands/improve/dedup.js +0 -482
  401. package/dist/commands/improve/hot-probation.js +0 -45
  402. package/dist/commands/improve/improve-auto-accept.js +0 -262
  403. package/dist/commands/improve/procedural.js +0 -403
  404. package/dist/commands/improve/recombine.js +0 -843
  405. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  406. package/dist/commands/lint/agent-linter.js +0 -44
  407. package/dist/commands/lint/command-linter.js +0 -44
  408. package/dist/commands/lint/default-linter.js +0 -16
  409. package/dist/commands/lint/fact-linter.js +0 -39
  410. package/dist/commands/lint/knowledge-linter.js +0 -16
  411. package/dist/commands/lint/memory-linter.js +0 -61
  412. package/dist/commands/lint/registry.js +0 -41
  413. package/dist/commands/lint/skill-linter.js +0 -45
  414. package/dist/commands/lint/task-linter.js +0 -50
  415. package/dist/commands/lint/workflow-linter.js +0 -81
  416. package/dist/commands/proposal/legacy-import.js +0 -115
  417. package/dist/commands/wiki-cli.js +0 -291
  418. package/dist/core/asset/asset-registry.js +0 -76
  419. package/dist/core/asset/asset-spec.js +0 -316
  420. package/dist/core/eval/rank-metrics.js +0 -113
  421. package/dist/core/ripgrep/install.js +0 -163
  422. package/dist/core/ripgrep/resolve.js +0 -81
  423. package/dist/indexer/db/db.js +0 -1585
  424. package/dist/indexer/manifest.js +0 -170
  425. package/dist/indexer/passes/metadata-contributors.js +0 -31
  426. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -25
  427. package/dist/output/text/wiki.js +0 -16
  428. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -18228
  429. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  430. package/dist/storage/repositories/consolidation-repository.js +0 -38
  431. package/dist/storage/repositories/recombine-repository.js +0 -213
  432. package/dist/wiki/wiki-templates.js +0 -15
  433. package/dist/wiki/wiki.js +0 -1013
  434. package/dist/workflows/db.js +0 -413
  435. package/docs/README.md +0 -103
  436. package/docs/migration/release-notes/0.9.0-beta.60.md +0 -19
  437. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  438. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -3,19 +3,43 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import fs from "node:fs";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { MAX_CONFIG_FILE_BYTES, MAX_LOCAL_METADATA_BYTES, readTextFileWithLimit, writeFileAtomic, } from "../core/common.js";
8
9
  import { parseAndValidateConfigText, resetConfigCache, sanitizeConfigForWrite, } from "../core/config/config.js";
9
- import { backupExistingConfig, withConfigLock, writeConfigAtomic } from "../core/config/config-io.js";
10
+ import { backupExistingConfig, parseConfigText, readConfigText, withConfigLock, writeConfigAtomic, } from "../core/config/config-io.js";
10
11
  import { ConfigError } from "../core/errors.js";
11
12
  import { withMaintenanceStartBarrier } from "../core/maintenance-barrier.js";
12
13
  import { assertNoArtifactReplacementBlockers, ensureMigrationBackupWithConfigLockHeld, fingerprintMigrationGeneration, getMigrationApplyJournalPath, getMigrationBackupDir, getMigrationBackupRoot, getMigrationRestoreJournalPath, inspectMigrationState, MIGRATION_BACKUP_VERSION, recoverInterruptedRestoreWithLocksHeld, restoreMigrationBackupWithLocksHeld, sameMigrationGeneration, verifyMigrationBackup, } from "../core/migration-backup.js";
13
- import { getConfigPath, getStateDbPathInDataDir, getWorkflowDbPath } from "../core/paths.js";
14
+ import { getConfigPath, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
14
15
  import { runMigrations as runStateMigrations } from "../core/state/migrations.js";
15
- import { openDatabase } from "../storage/database.js";
16
- import { runMigrations as runWorkflowMigrations } from "../workflows/db.js";
16
+ import { mergeLockEntriesSync } from "../integrations/lockfile.js";
17
+ import { migrateConfigSourcesToBundles, migratedLockEntries } from "../migrate/legacy/config-source-migration.js";
18
+ import { runContentMigration } from "../migrate/legacy/content-migration.js";
19
+ import { getLegacyWorkflowDbPath } from "../migrate/legacy/legacy-paths.js";
20
+ import { importLegacyProposalsIntoState } from "../migrate/legacy/proposal-fs-import.js";
21
+ import { applyTaskTargetRefMigration, planTaskTargetRefMigration } from "../migrate/legacy/task-target-ref-migration.js";
22
+ import { buildCutoverRefMap, cutoverMergeCommitted, deleteWorkflowDb, loadCutoverRefMap, migratePilotTreatmentFiles, quarantineIndexDb, runThreeDbCutover, } from "../migrate/legacy/three-db-cutover.js";
23
+ import { FROZEN_WORKFLOW_MIGRATIONS } from "../migrate/legacy/workflow-migrations-bodies.js";
24
+ import { requestGc } from "../runtime.js";
25
+ import { openDatabaseFinalizing } from "../storage/database.js";
26
+ import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
17
27
  import { EXIT_CODES } from "./shared.js";
18
28
  const MANUAL_GUIDANCE = "Provide a complete operator-prepared 0.9 config with --config. AKM does not guess profile-to-engine mappings.";
29
+ const APPLY_PHASE_ORDER = [
30
+ "prepared",
31
+ "state-converting",
32
+ "state-collapsing",
33
+ "state-applied",
34
+ "workflow-applied",
35
+ "cutover-applied",
36
+ "config-applied",
37
+ "tasks-prepared",
38
+ "tasks-applied",
39
+ "pilot-prepared",
40
+ "pilot-applied",
41
+ "committed",
42
+ ];
19
43
  function isFileFingerprint(value) {
20
44
  if (value === null)
21
45
  return true;
@@ -55,10 +79,57 @@ function isGenerationFingerprint(value) {
55
79
  function sameArtifactFingerprint(left, right) {
56
80
  return JSON.stringify(left) === JSON.stringify(right);
57
81
  }
82
+ /**
83
+ * Collapse state.db to a SINGLE FILE (DELETE journal) for the rest of the
84
+ * apply. A WAL-mode state.db carries `-wal`/`-shm` sidecars that the migration
85
+ * generation fingerprint tracks; a later read-only inspect (or a rolled-back
86
+ * cutover transaction) mutates them, which would trip the "state changed
87
+ * outside the journaled transition" rollback guard and REFUSE the fail-closed
88
+ * restore. In single-file mode a rolled-back transaction leaves state.db
89
+ * byte-identical, so the cutover's fail-closed rollback works. The runtime
90
+ * restores WAL on its next openStateDatabase.
91
+ *
92
+ * The resulting journal_mode is AUTHORITATIVE, not best-effort (issue #720):
93
+ * if state.db could not leave WAL mode — another PROCESS holds it open
94
+ * (same-process zombie closes are prevented by openDatabaseFinalizing's
95
+ * finalize-on-close guard) — the later cutover's rolled-back transaction
96
+ * would mutate `-wal` and the restore would be silently refused. Fail EARLY
97
+ * instead: journal.phase is still "state-converting" at the call site, so the
98
+ * outer catch restores config+state+workflow from the backup with a clear
99
+ * retry message.
100
+ */
101
+ function collapseStateDbToSingleFile(db) {
102
+ const attempt = () => {
103
+ try {
104
+ const checkpoint = db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
105
+ if (checkpoint?.busy === 0)
106
+ crashInMutationGapForTests("state-checkpoint");
107
+ db.exec("PRAGMA journal_mode = DELETE");
108
+ }
109
+ catch {
110
+ // Already single-file / nothing to checkpoint, or blocked — the
111
+ // read-back below is the authoritative signal either way.
112
+ }
113
+ return String(db.prepare("PRAGMA journal_mode").get()?.journal_mode ?? "").toLowerCase();
114
+ };
115
+ let journalMode = attempt();
116
+ if (journalMode === "wal") {
117
+ // A zombie-closed sibling connection ANYWHERE in this process (a close()
118
+ // with unfinalized prepare() statements outside the openDatabaseFinalizing
119
+ // set) also blocks the switch until GC finalizes it. Force a collection
120
+ // and retry once before concluding another PROCESS holds the database.
121
+ requestGc();
122
+ journalMode = attempt();
123
+ }
124
+ if (journalMode === "wal") {
125
+ throw new ConfigError("Cannot convert state.db out of WAL mode for migration — another akm process is holding it open. " +
126
+ "Close other akm processes and re-run `akm migrate apply`.", "INVALID_CONFIG_FILE");
127
+ }
128
+ }
58
129
  function hasGenerationMarker(dbPath, operationId, phase) {
59
130
  if (!fs.existsSync(dbPath))
60
131
  return false;
61
- const db = openDatabase(dbPath, { readonly: true });
132
+ const db = openDatabaseFinalizing(dbPath, { readonly: true });
62
133
  try {
63
134
  if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='akm_migration_generation'").get()) {
64
135
  return false;
@@ -66,12 +137,495 @@ function hasGenerationMarker(dbPath, operationId, phase) {
66
137
  const rows = db
67
138
  .prepare("SELECT operation_id, phase FROM akm_migration_generation WHERE singleton=1 LIMIT 2")
68
139
  .all();
69
- return rows.length === 1 && rows[0].operation_id === operationId && rows[0].phase === phase;
140
+ return rows.length === 1 && rows[0]?.operation_id === operationId && rows[0]?.phase === phase;
141
+ }
142
+ finally {
143
+ db.close();
144
+ }
145
+ }
146
+ function hasGenerationMarkerFromSnapshot(dbPath, operationId, phase) {
147
+ if (!fs.existsSync(dbPath))
148
+ return false;
149
+ const snapshot = createSqliteReadSnapshot(dbPath, "akm-migration-marker-");
150
+ try {
151
+ return hasGenerationMarker(snapshot.databasePath, operationId, phase);
152
+ }
153
+ finally {
154
+ snapshot.cleanup();
155
+ }
156
+ }
157
+ const STATE_SNAPSHOT_BUFFER_BYTES = 1024 * 1024;
158
+ class MigrationSnapshotChangedError extends ConfigError {
159
+ }
160
+ let migrationSnapshotHookForTests;
161
+ export function _setMigrationSnapshotHookForTests(hook) {
162
+ migrationSnapshotHookForTests = hook;
163
+ }
164
+ function updateCanonicalValue(hash, type, value) {
165
+ hash.update(`${Buffer.byteLength(type)}:`);
166
+ hash.update(type);
167
+ if (value === null) {
168
+ hash.update("-1:");
169
+ return;
170
+ }
171
+ const byteLength = typeof value === "string" ? Buffer.byteLength(value) : value.byteLength;
172
+ hash.update(`${byteLength}:`);
173
+ hash.update(value);
174
+ }
175
+ export function canonicalStateGenerationSha256(db) {
176
+ const quoteIdentifier = (value) => `"${value.replaceAll('"', '""')}"`;
177
+ const hash = createHash("sha256");
178
+ for (const [name, query] of [
179
+ ["application_id", "SELECT CAST(application_id AS TEXT) AS value FROM pragma_application_id"],
180
+ ["encoding", "SELECT encoding AS value FROM pragma_encoding"],
181
+ ["user_version", "SELECT CAST(user_version AS TEXT) AS value FROM pragma_user_version"],
182
+ ]) {
183
+ const row = db.prepare(query).get();
184
+ if (!row)
185
+ throw new ConfigError(`Cannot fingerprint SQLite PRAGMA ${name}.`, "INVALID_CONFIG_FILE");
186
+ updateCanonicalValue(hash, "pragma-name", name);
187
+ updateCanonicalValue(hash, "pragma-value", row.value);
188
+ }
189
+ const schemaRows = db
190
+ .prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name <> 'akm_migration_generation' AND tbl_name <> 'akm_migration_generation' ORDER BY type COLLATE BINARY, name COLLATE BINARY, tbl_name COLLATE BINARY, sql COLLATE BINARY")
191
+ .iterate();
192
+ for (const row of schemaRows) {
193
+ updateCanonicalValue(hash, "schema-type", row.type);
194
+ updateCanonicalValue(hash, "schema-name", row.name);
195
+ updateCanonicalValue(hash, "schema-table", row.tbl_name);
196
+ updateCanonicalValue(hash, "schema-sql", row.sql);
197
+ }
198
+ const firstTable = db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name <> 'akm_migration_generation' AND tbl_name <> 'akm_migration_generation' ORDER BY name COLLATE BINARY LIMIT 1");
199
+ const nextTable = db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name <> 'akm_migration_generation' AND tbl_name <> 'akm_migration_generation' AND name COLLATE BINARY > ? ORDER BY name COLLATE BINARY LIMIT 1");
200
+ let tableRow = firstTable.get();
201
+ while (tableRow) {
202
+ const table = tableRow.name;
203
+ updateCanonicalValue(hash, "table", table);
204
+ const columns = [];
205
+ const columnRows = db
206
+ .prepare('SELECT CAST(cid AS TEXT) AS cid, name, type, CAST("notnull" AS TEXT) AS not_null, dflt_value, CAST(pk AS TEXT) AS pk, CAST(hidden AS TEXT) AS hidden FROM pragma_table_xinfo(?) ORDER BY cid')
207
+ .iterate(table);
208
+ for (const column of columnRows) {
209
+ columns.push(column.name);
210
+ updateCanonicalValue(hash, "column-cid", column.cid);
211
+ updateCanonicalValue(hash, "column-name", column.name);
212
+ updateCanonicalValue(hash, "column-type", column.type);
213
+ updateCanonicalValue(hash, "column-notnull", column.not_null);
214
+ updateCanonicalValue(hash, "column-default", column.dflt_value);
215
+ updateCanonicalValue(hash, "column-pk", column.pk);
216
+ updateCanonicalValue(hash, "column-hidden", column.hidden);
217
+ }
218
+ const projections = [];
219
+ const ordering = [];
220
+ const tableFlags = db
221
+ .prepare("SELECT CAST(wr AS TEXT) AS without_rowid FROM pragma_table_list WHERE schema='main' AND name=?")
222
+ .get(table);
223
+ if (!tableFlags) {
224
+ throw new ConfigError(`Cannot determine whether SQLite table ${JSON.stringify(table)} has a rowid.`, "INVALID_CONFIG_FILE");
225
+ }
226
+ const hasImplicitRowid = tableFlags.without_rowid === "0";
227
+ const declaredNames = new Set(columns.map((column) => column.toLowerCase()));
228
+ const rowidAlias = hasImplicitRowid
229
+ ? ["rowid", "_rowid_", "oid"].find((alias) => !declaredNames.has(alias))
230
+ : undefined;
231
+ if (hasImplicitRowid && !rowidAlias) {
232
+ throw new ConfigError(`Cannot fingerprint SQLite table ${JSON.stringify(table)} because all implicit rowid aliases are shadowed.`, "INVALID_CONFIG_FILE");
233
+ }
234
+ if (rowidAlias) {
235
+ const rowidTypeAlias = quoteIdentifier("__akm_implicit_rowid_type");
236
+ const rowidValueAlias = quoteIdentifier("__akm_implicit_rowid_value");
237
+ projections.push(`'integer' AS ${rowidTypeAlias}`, `CAST(${quoteIdentifier(rowidAlias)} AS TEXT) AS ${rowidValueAlias}`);
238
+ ordering.push(`${rowidTypeAlias} COLLATE BINARY`, `${rowidValueAlias} COLLATE BINARY`);
239
+ }
240
+ for (const [index, column] of columns.entries()) {
241
+ const quotedColumn = quoteIdentifier(column);
242
+ const typeAlias = quoteIdentifier(`__akm_type_${index}`);
243
+ const valueAlias = quoteIdentifier(`__akm_value_${index}`);
244
+ projections.push(`typeof(${quotedColumn}) AS ${typeAlias}`, `CASE typeof(${quotedColumn}) WHEN 'integer' THEN CAST(${quotedColumn} AS TEXT) WHEN 'real' THEN printf('%!.17g', ${quotedColumn}) WHEN 'text' THEN CAST(${quotedColumn} AS BLOB) WHEN 'blob' THEN ${quotedColumn} ELSE NULL END AS ${valueAlias}`);
245
+ ordering.push(`${typeAlias} COLLATE BINARY`, `${valueAlias} COLLATE BINARY`);
246
+ }
247
+ const rowStatement = db.prepare(`SELECT ${projections.join(", ")} FROM ${quoteIdentifier(table)} ORDER BY ${ordering.join(", ")}`);
248
+ for (const row of rowStatement.iterate()) {
249
+ updateCanonicalValue(hash, "row", "");
250
+ if (rowidAlias) {
251
+ const rowidType = row.__akm_implicit_rowid_type;
252
+ const rowidValue = row.__akm_implicit_rowid_value;
253
+ if (rowidType !== "integer" || typeof rowidValue !== "string") {
254
+ throw new ConfigError("Cannot fingerprint an exact SQLite implicit rowid.", "INVALID_CONFIG_FILE");
255
+ }
256
+ updateCanonicalValue(hash, rowidType, rowidValue);
257
+ }
258
+ for (const [index] of columns.entries()) {
259
+ const type = row[`__akm_type_${index}`];
260
+ const value = row[`__akm_value_${index}`];
261
+ if (typeof type !== "string" ||
262
+ (value !== null && typeof value !== "string" && !(value instanceof Uint8Array))) {
263
+ throw new ConfigError("Cannot fingerprint an unsupported SQLite value.", "INVALID_CONFIG_FILE");
264
+ }
265
+ updateCanonicalValue(hash, type, value);
266
+ }
267
+ }
268
+ tableRow = nextTable.get(table);
269
+ }
270
+ return hash.digest("hex");
271
+ }
272
+ function migrationSnapshotChangedError() {
273
+ return new MigrationSnapshotChangedError("A SQLite migration artifact changed while creating a private status snapshot; retry when writes are idle.", "INVALID_CONFIG_FILE");
274
+ }
275
+ function isSnapshotPathRaceError(error) {
276
+ const code = error.code;
277
+ return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "EISDIR";
278
+ }
279
+ function sameOpenStateSnapshotStat(left, right) {
280
+ return (left.dev === right.dev &&
281
+ left.ino === right.ino &&
282
+ left.size === right.size &&
283
+ left.mtimeNs === right.mtimeNs &&
284
+ left.ctimeNs === right.ctimeNs);
285
+ }
286
+ function assertOpenStateSnapshotIdentity(source) {
287
+ let stat;
288
+ try {
289
+ stat = fs.lstatSync(source.path, { bigint: true });
290
+ }
291
+ catch (error) {
292
+ if (isSnapshotPathRaceError(error))
293
+ throw migrationSnapshotChangedError();
294
+ throw error;
295
+ }
296
+ if (!stat.isFile() || stat.dev !== source.dev || stat.ino !== source.ino)
297
+ throw migrationSnapshotChangedError();
298
+ }
299
+ function assertStateSnapshotPathAbsent(filePath) {
300
+ try {
301
+ fs.lstatSync(filePath);
302
+ }
303
+ catch (error) {
304
+ if (error.code === "ENOENT")
305
+ return;
306
+ throw error;
307
+ }
308
+ throw migrationSnapshotChangedError();
309
+ }
310
+ function openStateSnapshotSource(filePath, optional = false) {
311
+ let pathStat;
312
+ try {
313
+ pathStat = fs.lstatSync(filePath, { bigint: true });
314
+ }
315
+ catch (error) {
316
+ if (optional && error.code === "ENOENT")
317
+ return undefined;
318
+ if (isSnapshotPathRaceError(error))
319
+ throw migrationSnapshotChangedError();
320
+ throw error;
321
+ }
322
+ if (!pathStat.isFile()) {
323
+ throw new ConfigError(`Migration snapshot source is not a regular file: ${filePath}`, "INVALID_CONFIG_FILE");
324
+ }
325
+ let fd;
326
+ try {
327
+ fd = fs.openSync(filePath, "r");
328
+ }
329
+ catch (error) {
330
+ if (isSnapshotPathRaceError(error))
331
+ throw migrationSnapshotChangedError();
332
+ throw error;
333
+ }
334
+ try {
335
+ const fdStat = fs.fstatSync(fd, { bigint: true });
336
+ if (!fdStat.isFile() || fdStat.dev !== pathStat.dev || fdStat.ino !== pathStat.ino) {
337
+ throw migrationSnapshotChangedError();
338
+ }
339
+ return { path: filePath, fd, dev: fdStat.dev, ino: fdStat.ino };
340
+ }
341
+ catch (error) {
342
+ fs.closeSync(fd);
343
+ throw error;
344
+ }
345
+ }
346
+ function readStateSnapshotSource(source, destinationFd) {
347
+ assertOpenStateSnapshotIdentity(source);
348
+ const before = fs.fstatSync(source.fd, { bigint: true });
349
+ const hash = createHash("sha256");
350
+ const buffer = Buffer.allocUnsafe(STATE_SNAPSHOT_BUFFER_BYTES);
351
+ let position = 0;
352
+ while (true) {
353
+ const bytesRead = fs.readSync(source.fd, buffer, 0, buffer.length, position);
354
+ if (bytesRead === 0)
355
+ break;
356
+ hash.update(buffer.subarray(0, bytesRead));
357
+ if (destinationFd !== undefined) {
358
+ let written = 0;
359
+ while (written < bytesRead) {
360
+ written += fs.writeSync(destinationFd, buffer, written, bytesRead - written);
361
+ }
362
+ }
363
+ position += bytesRead;
364
+ }
365
+ const after = fs.fstatSync(source.fd, { bigint: true });
366
+ assertOpenStateSnapshotIdentity(source);
367
+ if (!sameOpenStateSnapshotStat(before, after) || BigInt(position) !== after.size) {
368
+ throw migrationSnapshotChangedError();
369
+ }
370
+ return { bytes: position, sha256: hash.digest("hex") };
371
+ }
372
+ function fingerprintStateSnapshotSources(main, wal, walPath) {
373
+ const mainFingerprint = readStateSnapshotSource(main);
374
+ const walFingerprint = wal ? readStateSnapshotSource(wal) : null;
375
+ if (!wal)
376
+ assertStateSnapshotPathAbsent(walPath);
377
+ return { main: mainFingerprint, wal: walFingerprint };
378
+ }
379
+ function copyStateSnapshotSource(source, destination) {
380
+ const destinationFd = fs.openSync(destination, "wx", 0o600);
381
+ try {
382
+ const fingerprint = readStateSnapshotSource(source, destinationFd);
383
+ fs.fsyncSync(destinationFd);
384
+ return fingerprint;
385
+ }
386
+ finally {
387
+ fs.closeSync(destinationFd);
388
+ }
389
+ }
390
+ function copyStateSnapshotSources(main, wal, walPath, databasePath) {
391
+ const mainFingerprint = copyStateSnapshotSource(main, databasePath);
392
+ const walFingerprint = wal ? copyStateSnapshotSource(wal, `${databasePath}-wal`) : null;
393
+ if (!wal)
394
+ assertStateSnapshotPathAbsent(walPath);
395
+ return { main: mainFingerprint, wal: walFingerprint };
396
+ }
397
+ function sameStateSnapshotFingerprint(left, right) {
398
+ return left === null || right === null ? left === right : left.bytes === right.bytes && left.sha256 === right.sha256;
399
+ }
400
+ function sameStateSnapshotGeneration(left, right) {
401
+ return sameStateSnapshotFingerprint(left.main, right.main) && sameStateSnapshotFingerprint(left.wal, right.wal);
402
+ }
403
+ function createSqliteReadSnapshot(sourcePath, tempPrefix, applyPhase) {
404
+ const walPath = `${sourcePath}-wal`;
405
+ const main = openStateSnapshotSource(sourcePath);
406
+ if (!main)
407
+ throw new ConfigError(`SQLite database not found: ${sourcePath}`, "INVALID_CONFIG_FILE");
408
+ let wal;
409
+ let snapshotDir;
410
+ let mainOpen = true;
411
+ let walOpen = false;
412
+ try {
413
+ wal = openStateSnapshotSource(walPath, true);
414
+ walOpen = wal !== undefined;
415
+ snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), tempPrefix));
416
+ fs.chmodSync(snapshotDir, 0o700);
417
+ const databasePath = path.join(snapshotDir, "state.db");
418
+ const before = fingerprintStateSnapshotSources(main, wal, walPath);
419
+ migrationSnapshotHookForTests?.({ sourcePath, ...(applyPhase ? { applyPhase } : {}) });
420
+ const copied = copyStateSnapshotSources(main, wal, walPath, databasePath);
421
+ const after = fingerprintStateSnapshotSources(main, wal, walPath);
422
+ if (!sameStateSnapshotGeneration(before, copied) || !sameStateSnapshotGeneration(before, after)) {
423
+ throw migrationSnapshotChangedError();
424
+ }
425
+ const completedDir = snapshotDir;
426
+ fs.closeSync(main.fd);
427
+ mainOpen = false;
428
+ if (wal) {
429
+ fs.closeSync(wal.fd);
430
+ walOpen = false;
431
+ }
432
+ snapshotDir = undefined;
433
+ return {
434
+ databasePath,
435
+ cleanup: () => fs.rmSync(completedDir, { recursive: true, force: true }),
436
+ };
437
+ }
438
+ finally {
439
+ if (mainOpen) {
440
+ try {
441
+ fs.closeSync(main.fd);
442
+ }
443
+ catch {
444
+ // The original snapshot failure remains authoritative.
445
+ }
446
+ }
447
+ if (walOpen && wal) {
448
+ try {
449
+ fs.closeSync(wal.fd);
450
+ }
451
+ catch {
452
+ // The original snapshot failure remains authoritative.
453
+ }
454
+ }
455
+ if (snapshotDir)
456
+ fs.rmSync(snapshotDir, { recursive: true, force: true });
457
+ }
458
+ }
459
+ function bindStateConvertingMarker(db, operationId) {
460
+ db.exec(`
461
+ CREATE TABLE IF NOT EXISTS akm_migration_generation (
462
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
463
+ operation_id TEXT NOT NULL,
464
+ phase TEXT NOT NULL,
465
+ generation_sha256 TEXT
466
+ )
467
+ `);
468
+ const columns = new Set(db.prepare("PRAGMA table_info(akm_migration_generation)").all().map((column) => column.name));
469
+ if (!columns.has("generation_sha256")) {
470
+ db.exec("ALTER TABLE akm_migration_generation ADD COLUMN generation_sha256 TEXT");
471
+ }
472
+ const generationSha256 = canonicalStateGenerationSha256(db);
473
+ db.prepare("INSERT INTO akm_migration_generation(singleton, operation_id, phase, generation_sha256) VALUES (1, ?, 'state-converting', ?) ON CONFLICT(singleton) DO UPDATE SET operation_id=excluded.operation_id, phase=excluded.phase, generation_sha256=excluded.generation_sha256").run(operationId, generationSha256);
474
+ }
475
+ function readBoundStateGenerationMarker(db, operationId) {
476
+ if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='akm_migration_generation'").get()) {
477
+ return undefined;
478
+ }
479
+ const columns = new Set(db.prepare("PRAGMA table_info(akm_migration_generation)").all().map((column) => column.name));
480
+ if (!columns.has("generation_sha256"))
481
+ return undefined;
482
+ const marker = db
483
+ .prepare("SELECT operation_id, phase, generation_sha256 FROM akm_migration_generation WHERE singleton=1")
484
+ .get();
485
+ const generationSha256 = marker?.generation_sha256;
486
+ if (!marker || marker.operation_id !== operationId || !generationSha256 || !/^[a-f0-9]{64}$/.test(generationSha256)) {
487
+ return undefined;
488
+ }
489
+ if (marker.phase !== "state-converting" && marker.phase !== "state-applied")
490
+ return undefined;
491
+ if (canonicalStateGenerationSha256(db) !== generationSha256) {
492
+ throw new ConfigError("state.db no longer matches the exact logical generation bound to its state-converting marker.", "INVALID_CONFIG_FILE");
493
+ }
494
+ return { phase: marker.phase, generationSha256 };
495
+ }
496
+ function readBoundStateGenerationMarkerAtPath(statePath, operationId) {
497
+ if (!fs.existsSync(statePath))
498
+ return undefined;
499
+ const db = openDatabaseFinalizing(statePath, { readonly: true, create: false });
500
+ try {
501
+ return readBoundStateGenerationMarker(db, operationId);
70
502
  }
71
503
  finally {
72
504
  db.close();
73
505
  }
74
506
  }
507
+ function readBoundStateGenerationMarkerFromDisk(operationId) {
508
+ const statePath = getStateDbPathInDataDir();
509
+ if (!fs.existsSync(statePath))
510
+ return undefined;
511
+ const snapshot = createSqliteReadSnapshot(statePath, "akm-migration-state-");
512
+ try {
513
+ return readBoundStateGenerationMarkerAtPath(snapshot.databasePath, operationId);
514
+ }
515
+ finally {
516
+ snapshot.cleanup();
517
+ }
518
+ }
519
+ function cutoverMergeCommittedFromSnapshot(operationId) {
520
+ const statePath = getStateDbPathInDataDir();
521
+ if (!fs.existsSync(statePath))
522
+ return false;
523
+ const snapshot = createSqliteReadSnapshot(statePath, "akm-migration-state-");
524
+ try {
525
+ return cutoverMergeCommitted(snapshot.databasePath, operationId);
526
+ }
527
+ finally {
528
+ snapshot.cleanup();
529
+ }
530
+ }
531
+ function createMigrationInspectionSnapshots(applyPhase) {
532
+ const missingRoot = fs.mkdtempSync(path.join(os.tmpdir(), "akm-migration-inspect-"));
533
+ fs.chmodSync(missingRoot, 0o700);
534
+ const snapshots = [];
535
+ const capture = (sourcePath, prefix, name) => {
536
+ if (!fs.existsSync(sourcePath))
537
+ return path.join(missingRoot, name);
538
+ const snapshot = createSqliteReadSnapshot(sourcePath, prefix, applyPhase);
539
+ snapshots.push(snapshot);
540
+ return snapshot.databasePath;
541
+ };
542
+ try {
543
+ const paths = {
544
+ stateDbPath: capture(getStateDbPathInDataDir(), "akm-migration-state-", "state.db"),
545
+ workflowDbPath: capture(getLegacyWorkflowDbPath(), "akm-migration-workflow-", "workflow.db"),
546
+ indexDbPath: capture(getDbPath(), "akm-migration-index-", "index.db"),
547
+ };
548
+ return {
549
+ paths,
550
+ cleanup: () => {
551
+ for (const snapshot of snapshots)
552
+ snapshot.cleanup();
553
+ fs.rmSync(missingRoot, { recursive: true, force: true });
554
+ },
555
+ };
556
+ }
557
+ catch (error) {
558
+ for (const snapshot of snapshots)
559
+ snapshot.cleanup();
560
+ fs.rmSync(missingRoot, { recursive: true, force: true });
561
+ throw error;
562
+ }
563
+ }
564
+ function captureMigrationInspection(applyPhase) {
565
+ const before = fingerprintMigrationGeneration();
566
+ const snapshots = createMigrationInspectionSnapshots(applyPhase);
567
+ try {
568
+ const after = fingerprintMigrationGeneration();
569
+ if (!sameMigrationGeneration(before, after))
570
+ throw migrationSnapshotChangedError();
571
+ return {
572
+ paths: snapshots.paths,
573
+ artifacts: inspectMigrationState(snapshots.paths),
574
+ generation: after,
575
+ cleanup: snapshots.cleanup,
576
+ };
577
+ }
578
+ catch (error) {
579
+ snapshots.cleanup();
580
+ throw error;
581
+ }
582
+ }
583
+ function inspectMigrationStateFromSnapshots() {
584
+ const capture = captureMigrationInspection();
585
+ try {
586
+ return capture.artifacts;
587
+ }
588
+ finally {
589
+ capture.cleanup();
590
+ }
591
+ }
592
+ class MigrationPreflightGenerationError extends ConfigError {
593
+ }
594
+ let applyPreflightHookForTests;
595
+ export function _setApplyPreflightHookForTests(hook) {
596
+ applyPreflightHookForTests = hook;
597
+ }
598
+ function inspectExactApplyJournalGeneration(journal) {
599
+ if (journal.phase !== "state-applied" && journal.phase !== "workflow-applied") {
600
+ throw new ConfigError(`Cannot preflight migration journal phase ${journal.phase}.`, "INVALID_CONFIG_FILE");
601
+ }
602
+ applyPreflightHookForTests?.(journal.phase);
603
+ const capture = captureMigrationInspection(journal.phase);
604
+ try {
605
+ if (!sameMigrationGeneration(capture.generation, journal.generation)) {
606
+ throw new MigrationPreflightGenerationError(`Migration apply journal phase ${journal.phase} changed before its next mutation; the external generation was preserved.`, "INVALID_CONFIG_FILE");
607
+ }
608
+ return capture.artifacts;
609
+ }
610
+ finally {
611
+ capture.cleanup();
612
+ }
613
+ }
614
+ function readSingleFileBoundStateMarker(journal, stateSnapshotPath) {
615
+ const statePath = stateSnapshotPath ?? getStateDbPathInDataDir();
616
+ if (!fs.existsSync(statePath) || stateDatabaseHeaderUsesWal(statePath))
617
+ return undefined;
618
+ return stateSnapshotPath
619
+ ? readBoundStateGenerationMarkerAtPath(statePath, journal.operationId)
620
+ : readBoundStateGenerationMarkerFromDisk(journal.operationId);
621
+ }
622
+ function advanceBoundStateMarker(db, operationId) {
623
+ const marker = readBoundStateGenerationMarker(db, operationId);
624
+ if (!marker) {
625
+ throw new ConfigError("state.db lacks its exact marker-bound conversion generation.", "INVALID_CONFIG_FILE");
626
+ }
627
+ db.prepare("UPDATE akm_migration_generation SET phase='state-applied' WHERE singleton=1 AND operation_id=?").run(operationId);
628
+ }
75
629
  function generationFromBackup(manifest) {
76
630
  const fingerprint = (name) => {
77
631
  const artifact = manifest.artifacts[name];
@@ -83,30 +637,16 @@ function generationFromBackup(manifest) {
83
637
  workflow: { main: fingerprint("workflow.db"), wal: null, shm: null },
84
638
  };
85
639
  }
86
- function detectAdjacentGeneration(journal, manifest) {
640
+ function detectAdjacentGeneration(journal, manifest, live, current, workflowDbPath) {
87
641
  if (journal.phase === "rollback-prepared") {
88
642
  return {
89
- rollbackCompleted: sameMigrationGeneration(fingerprintMigrationGeneration(), generationFromBackup(manifest)),
643
+ rollbackCompleted: sameMigrationGeneration(current, generationFromBackup(manifest)),
90
644
  };
91
645
  }
92
- const live = inspectMigrationState();
93
- const markerPresent = journal.phase === "prepared"
94
- ? hasGenerationMarker(getStateDbPathInDataDir(), journal.operationId, "state-applied")
95
- : journal.phase === "state-applied"
96
- ? hasGenerationMarker(getWorkflowDbPath(), journal.operationId, "workflow-applied")
97
- : false;
98
- const current = fingerprintMigrationGeneration();
99
646
  const unchanged = (...names) => names.every((name) => sameArtifactFingerprint(journal.generation[name], current[name]));
100
- if (journal.phase === "prepared" && unchanged("config", "workflow") && markerPresent) {
101
- return {
102
- adjacent: {
103
- phase: "state-applied",
104
- complete: live.state.status === "current" || live.state.status === "missing",
105
- generation: current,
106
- },
107
- };
108
- }
109
- if (journal.phase === "state-applied" && unchanged("config", "state") && markerPresent) {
647
+ if (journal.phase === "state-applied" &&
648
+ unchanged("config", "state") &&
649
+ hasGenerationMarker(workflowDbPath, journal.operationId, "workflow-applied")) {
110
650
  return {
111
651
  adjacent: {
112
652
  phase: "workflow-applied",
@@ -116,7 +656,11 @@ function detectAdjacentGeneration(journal, manifest) {
116
656
  };
117
657
  }
118
658
  const expectedTarget = `${JSON.stringify(journal.targetConfig, null, 2)}\n`;
119
- if (journal.phase === "workflow-applied" &&
659
+ // Chunk 8, WI-8.2: config is written in the phase AFTER the cutover, so a crash
660
+ // in the config mutation gap leaves the journal at `cutover-applied` with the
661
+ // config already on disk. state (merged) + workflow (deleted) are unchanged
662
+ // since the cutover-applied advance — detect the config-applied adjacent.
663
+ if ((journal.phase === "workflow-applied" || journal.phase === "cutover-applied") &&
120
664
  unchanged("state", "workflow") &&
121
665
  fs.existsSync(getConfigPath()) &&
122
666
  readTextFileWithLimit(getConfigPath(), MAX_CONFIG_FILE_BYTES, "Config file") === expectedTarget) {
@@ -129,11 +673,16 @@ function detectAdjacentGeneration(journal, manifest) {
129
673
  function assertRollbackTransitionAllowed(journal, current) {
130
674
  const unchanged = journal.phase === "prepared"
131
675
  ? ["config", "workflow"]
132
- : journal.phase === "state-applied"
133
- ? ["config", "state"]
134
- : journal.phase === "workflow-applied"
135
- ? ["state", "workflow"]
136
- : ["config", "state", "workflow"];
676
+ : journal.phase === "state-converting"
677
+ ? ["config", "workflow"]
678
+ : journal.phase === "state-applied"
679
+ ? ["config", "state"]
680
+ : journal.phase === "workflow-applied" || journal.phase === "cutover-applied"
681
+ ? // config is applied in the phase after the cutover, so a rollback from
682
+ // either only needs state + workflow unchanged (workflow=deleted is
683
+ // recorded in the journal's own generation and compares equal).
684
+ ["state", "workflow"]
685
+ : ["config", "state", "workflow"];
137
686
  for (const name of unchanged) {
138
687
  if (!sameArtifactFingerprint(journal.generation[name], current[name])) {
139
688
  throw new ConfigError(`Refusing migration rollback because ${name} changed outside the journaled ${journal.phase} transition.`, "INVALID_CONFIG_FILE");
@@ -153,8 +702,8 @@ function configMatchesBytes(expectedSize, expectedHash) {
153
702
  return (fs.statSync(configPath).size === expectedSize &&
154
703
  createHash("sha256").update(Buffer.from(text)).digest("hex") === expectedHash);
155
704
  }
156
- function validateApplyPhase(journal, manifest) {
157
- const live = inspectMigrationState();
705
+ function validateApplyPhase(journal, manifest, inspectionPaths, inspectedState) {
706
+ const live = inspectedState ?? inspectMigrationState(inspectionPaths);
158
707
  const stateOriginal = sameArtifactState(live.state, manifest.artifacts["state.db"]);
159
708
  const workflowOriginal = sameArtifactState(live.workflow, manifest.artifacts["workflow.db"]);
160
709
  const configOriginal = manifest.artifacts["config.json"].present
@@ -166,6 +715,11 @@ function validateApplyPhase(journal, manifest) {
166
715
  const workflowApplied = manifest.artifacts["workflow.db"].present
167
716
  ? live.workflow.status === "current"
168
717
  : live.workflow.status === "missing";
718
+ // Chunk 8, WI-8.2: at/after the cutover, workflow.db is DELETED (its rows are
719
+ // merged into state.db). A backed-up-present workflow.db that is now missing is
720
+ // the intended post-cutover terminal state, not a failure.
721
+ const workflowDeleted = manifest.artifacts["workflow.db"].present && live.workflow.status === "missing";
722
+ const workflowFinal = workflowApplied || workflowDeleted;
169
723
  const expectedTarget = `${JSON.stringify(journal.targetConfig, null, 2)}\n`;
170
724
  const configApplied = live.config.status === "current" &&
171
725
  fs.existsSync(getConfigPath()) &&
@@ -174,16 +728,29 @@ function validateApplyPhase(journal, manifest) {
174
728
  ? true
175
729
  : journal.phase === "prepared"
176
730
  ? configOriginal && workflowOriginal && (stateOriginal || stateApplied)
177
- : journal.phase === "state-applied"
178
- ? stateApplied && configOriginal && (workflowOriginal || workflowApplied)
179
- : journal.phase === "workflow-applied"
180
- ? stateApplied && workflowApplied && (configOriginal || configApplied)
181
- : stateApplied && workflowApplied && configApplied;
731
+ : journal.phase === "state-converting" || journal.phase === "state-collapsing"
732
+ ? configOriginal && workflowOriginal && (stateOriginal || stateApplied)
733
+ : journal.phase === "state-applied"
734
+ ? stateApplied && configOriginal && (workflowOriginal || workflowApplied)
735
+ : journal.phase === "workflow-applied"
736
+ ? stateApplied && workflowApplied && (configOriginal || configApplied)
737
+ : journal.phase === "cutover-applied"
738
+ ? stateApplied && workflowFinal && (configOriginal || configApplied)
739
+ : // config-applied / tasks-* / pilot-* / committed
740
+ stateApplied && workflowFinal && configApplied;
182
741
  if (!reachable) {
183
742
  throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match a reachable config/state/workflow artifact state.`, "INVALID_CONFIG_FILE");
184
743
  }
744
+ return live;
185
745
  }
186
- function readApplyJournal() {
746
+ function postCutoverArtifacts(journal, manifest, current, paths, artifacts) {
747
+ assertPostCutoverWorkflowAuthenticated(journal, current);
748
+ if (!cutoverMergeCommitted(paths.stateDbPath, journal.operationId)) {
749
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} lacks its operation-bound cutover marker.`, "INVALID_CONFIG_FILE");
750
+ }
751
+ return validateApplyPhase(journal, manifest, paths, artifacts);
752
+ }
753
+ function readApplyJournalMetadata() {
187
754
  const journalPath = getMigrationApplyJournalPath();
188
755
  if (!fs.existsSync(journalPath))
189
756
  return {};
@@ -192,9 +759,16 @@ function readApplyJournal() {
192
759
  const value = JSON.parse(readTextFileWithLimit(journalPath, MAX_LOCAL_METADATA_BYTES, "Migration apply journal"));
193
760
  const phases = [
194
761
  "prepared",
762
+ "state-converting",
763
+ "state-collapsing",
195
764
  "state-applied",
196
765
  "workflow-applied",
766
+ "cutover-applied",
197
767
  "config-applied",
768
+ "tasks-prepared",
769
+ "tasks-applied",
770
+ "pilot-prepared",
771
+ "pilot-applied",
198
772
  "rollback-prepared",
199
773
  "committed",
200
774
  ];
@@ -252,21 +826,293 @@ function readApplyJournal() {
252
826
  if (manifest.runId !== journal.backupRunId || manifest.installationId !== journal.installationId) {
253
827
  throw new ConfigError(`Migration apply journal backup provenance does not match its manifest.`, "INVALID_CONFIG_FILE");
254
828
  }
255
- validateApplyPhase(journal, manifest);
256
- if (!sameMigrationGeneration(fingerprintMigrationGeneration(), journal.generation)) {
257
- const adjacent = detectAdjacentGeneration(journal, manifest);
258
- if (adjacent.adjacent || adjacent.rollbackCompleted)
259
- return { journal, config, ...adjacent };
829
+ return { journal, config, manifest };
830
+ }
831
+ catch (error) {
832
+ return {
833
+ journal,
834
+ error: `Unreadable migration apply journal at ${journalPath}: ${error instanceof Error ? error.message : String(error)}`,
835
+ };
836
+ }
837
+ }
838
+ function journalArtifactState(artifact, status = artifact.status) {
839
+ return {
840
+ status,
841
+ ...(artifact.migrationIds ? { migrationIds: artifact.migrationIds } : {}),
842
+ ...(artifact.migrationChecksums ? { migrationChecksums: artifact.migrationChecksums } : {}),
843
+ };
844
+ }
845
+ function stateBeforeCompatibilityConversion(journal, manifest) {
846
+ const workflowBackup = manifest.artifacts["workflow.db"];
847
+ const indexBackup = manifest.artifacts["index.db"];
848
+ return {
849
+ config: journalArtifactState(manifest.artifacts["config.json"]),
850
+ state: { status: manifest.artifacts["state.db"].present ? "current" : "missing" },
851
+ workflow: journal.phase === "workflow-applied" && workflowBackup.present
852
+ ? { status: "current" }
853
+ : journalArtifactState(workflowBackup),
854
+ index: indexBackup ? journalArtifactState(indexBackup) : { status: "missing" },
855
+ };
856
+ }
857
+ function isPreConversionCompatiblePhase(phase) {
858
+ return phase === "state-applied" || phase === "workflow-applied";
859
+ }
860
+ function isPostCutoverPhase(phase) {
861
+ return [
862
+ "cutover-applied",
863
+ "config-applied",
864
+ "tasks-prepared",
865
+ "tasks-applied",
866
+ "pilot-prepared",
867
+ "pilot-applied",
868
+ "committed",
869
+ ].includes(phase);
870
+ }
871
+ function isAuthenticatedWorkflowAdjacent(journal, current, workflowSnapshotPath) {
872
+ if (journal.phase !== "state-applied")
873
+ return false;
874
+ const markerPresent = workflowSnapshotPath
875
+ ? hasGenerationMarker(workflowSnapshotPath, journal.operationId, "workflow-applied")
876
+ : hasGenerationMarkerFromSnapshot(getLegacyWorkflowDbPath(), journal.operationId, "workflow-applied");
877
+ return (sameArtifactFingerprint(journal.generation.config, current.config) &&
878
+ sameArtifactFingerprint(journal.generation.state, current.state) &&
879
+ markerPresent);
880
+ }
881
+ function isAuthenticatedCutoverAdjacent(journal, current, stateSnapshotPath) {
882
+ return (journal.phase === "workflow-applied" &&
883
+ sameArtifactFingerprint(journal.generation.config, current.config) &&
884
+ workflowArtifactIsDeletionSubset(journal.generation, current) &&
885
+ (stateSnapshotPath
886
+ ? cutoverMergeCommitted(stateSnapshotPath, journal.operationId)
887
+ : cutoverMergeCommittedFromSnapshot(journal.operationId)));
888
+ }
889
+ function workflowArtifactIsDeletionSubset(expected, current) {
890
+ return ["main", "wal", "shm"].every((component) => {
891
+ const actual = current.workflow[component];
892
+ return actual === null || JSON.stringify(actual) === JSON.stringify(expected.workflow[component]);
893
+ });
894
+ }
895
+ function assertPostCutoverWorkflowAuthenticated(journal, current) {
896
+ if (workflowArtifactIsDeletionSubset(journal.generation, current))
897
+ return;
898
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not authorize the live workflow.db generation.`, "INVALID_CONFIG_FILE");
899
+ }
900
+ function readApplyJournal() {
901
+ const metadata = readApplyJournalMetadata();
902
+ if (metadata.error || !metadata.journal || !metadata.config || !metadata.manifest)
903
+ return metadata;
904
+ const { journal, config, manifest } = metadata;
905
+ let capture;
906
+ try {
907
+ capture = captureMigrationInspection();
908
+ }
909
+ catch (error) {
910
+ return {
911
+ journal,
912
+ config,
913
+ error: `Unreadable migration apply journal at ${getMigrationApplyJournalPath()}: ${error instanceof Error ? error.message : String(error)}`,
914
+ };
915
+ }
916
+ const rawGeneration = capture.generation;
917
+ const inspectedArtifacts = capture.artifacts;
918
+ try {
919
+ if (isPostCutoverPhase(journal.phase)) {
920
+ if (isTaskOnlyRepair(manifest)) {
921
+ const artifacts = validateApplyPhase(journal, manifest, capture.paths, inspectedArtifacts);
922
+ if (!sameMigrationGeneration(rawGeneration, journal.generation)) {
923
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
924
+ }
925
+ return { journal, config, artifacts };
926
+ }
927
+ return {
928
+ journal,
929
+ config,
930
+ artifacts: postCutoverArtifacts(journal, manifest, rawGeneration, capture.paths, inspectedArtifacts),
931
+ };
932
+ }
933
+ if (!sameMigrationGeneration(rawGeneration, journal.generation) &&
934
+ isAuthenticatedCutoverAdjacent(journal, rawGeneration, capture.paths.stateDbPath)) {
935
+ const artifacts = postCutoverArtifacts({ ...journal, phase: "cutover-applied" }, manifest, rawGeneration, capture.paths, inspectedArtifacts);
936
+ return {
937
+ journal,
938
+ config,
939
+ artifacts,
940
+ adjacent: {
941
+ phase: "cutover-applied",
942
+ complete: true,
943
+ generation: rawGeneration,
944
+ },
945
+ };
946
+ }
947
+ if (isPreConversionCompatiblePhase(journal.phase)) {
948
+ const stateMarker = readSingleFileBoundStateMarker(journal, capture.paths.stateDbPath);
949
+ if (stateMarker?.phase === "state-applied") {
950
+ if (sameMigrationGeneration(rawGeneration, journal.generation)) {
951
+ return { journal, config, artifacts: inspectedArtifacts };
952
+ }
953
+ if (isAuthenticatedWorkflowAdjacent(journal, rawGeneration, capture.paths.workflowDbPath)) {
954
+ return {
955
+ journal,
956
+ config,
957
+ artifacts: inspectedArtifacts,
958
+ adjacent: {
959
+ phase: "workflow-applied",
960
+ complete: true,
961
+ generation: rawGeneration,
962
+ },
963
+ };
964
+ }
965
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
966
+ }
967
+ }
968
+ if (isPreConversionCompatiblePhase(journal.phase)) {
969
+ if (sameMigrationGeneration(rawGeneration, journal.generation)) {
970
+ return {
971
+ journal,
972
+ config,
973
+ artifacts: stateBeforeCompatibilityConversion(journal, manifest),
974
+ requiresStateConversion: true,
975
+ };
976
+ }
977
+ if (isAuthenticatedWorkflowAdjacent(journal, rawGeneration, capture.paths.workflowDbPath)) {
978
+ return {
979
+ journal,
980
+ config,
981
+ artifacts: stateBeforeCompatibilityConversion({ ...journal, phase: "workflow-applied" }, manifest),
982
+ requiresStateConversion: true,
983
+ adjacent: {
984
+ phase: "workflow-applied",
985
+ complete: true,
986
+ generation: rawGeneration,
987
+ },
988
+ };
989
+ }
260
990
  throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
261
991
  }
262
- return { journal, config };
992
+ if (journal.phase === "state-converting") {
993
+ if (sameMigrationGeneration(rawGeneration, journal.generation)) {
994
+ return {
995
+ journal,
996
+ config,
997
+ artifacts: stateBeforeCompatibilityConversion(journal, manifest),
998
+ requiresStateConversion: true,
999
+ };
1000
+ }
1001
+ const marker = readBoundStateGenerationMarkerAtPath(capture.paths.stateDbPath, journal.operationId);
1002
+ if (marker?.phase === "state-converting") {
1003
+ return {
1004
+ journal,
1005
+ config,
1006
+ artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1007
+ requiresStateConversion: true,
1008
+ adjacent: {
1009
+ phase: "state-collapsing",
1010
+ complete: true,
1011
+ generation: rawGeneration,
1012
+ },
1013
+ };
1014
+ }
1015
+ throw new ConfigError("Migration apply journal phase state-converting does not match its exact marker-bound generation.", "INVALID_CONFIG_FILE");
1016
+ }
1017
+ if (journal.phase === "state-collapsing") {
1018
+ if (sameMigrationGeneration(rawGeneration, journal.generation)) {
1019
+ return {
1020
+ journal,
1021
+ config,
1022
+ artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1023
+ requiresStateConversion: true,
1024
+ };
1025
+ }
1026
+ const marker = readBoundStateGenerationMarkerAtPath(capture.paths.stateDbPath, journal.operationId);
1027
+ if (marker) {
1028
+ return {
1029
+ journal,
1030
+ config,
1031
+ artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1032
+ requiresStateConversion: true,
1033
+ adjacent: {
1034
+ phase: marker.phase === "state-applied" ? "state-applied" : "state-collapsing",
1035
+ complete: marker.phase === "state-applied",
1036
+ generation: rawGeneration,
1037
+ },
1038
+ };
1039
+ }
1040
+ throw new ConfigError("Migration apply journal phase state-collapsing does not match its exact marker-bound generation.", "INVALID_CONFIG_FILE");
1041
+ }
1042
+ validateApplyPhase(journal, manifest, capture.paths, inspectedArtifacts);
1043
+ if (!sameMigrationGeneration(rawGeneration, journal.generation)) {
1044
+ const adjacent = detectAdjacentGeneration(journal, manifest, inspectedArtifacts, rawGeneration, capture.paths.workflowDbPath);
1045
+ if (adjacent.adjacent || adjacent.rollbackCompleted) {
1046
+ return { journal, config, artifacts: inspectedArtifacts, ...adjacent };
1047
+ }
1048
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1049
+ }
1050
+ return { journal, config, artifacts: inspectedArtifacts };
263
1051
  }
264
1052
  catch (error) {
265
1053
  return {
266
1054
  journal,
267
- error: `Unreadable migration apply journal at ${journalPath}: ${error instanceof Error ? error.message : String(error)}`,
1055
+ config,
1056
+ ...(isPreConversionCompatiblePhase(journal.phase) ||
1057
+ journal.phase === "state-converting" ||
1058
+ journal.phase === "state-collapsing"
1059
+ ? {
1060
+ artifacts: stateBeforeCompatibilityConversion(journal, manifest),
1061
+ requiresStateConversion: true,
1062
+ }
1063
+ : { artifacts: inspectedArtifacts }),
1064
+ error: `Unreadable migration apply journal at ${getMigrationApplyJournalPath()}: ${error instanceof Error ? error.message : String(error)}`,
268
1065
  };
269
1066
  }
1067
+ finally {
1068
+ capture.cleanup();
1069
+ }
1070
+ }
1071
+ function authenticatePreConversionJournalForApply() {
1072
+ const metadata = readApplyJournalMetadata();
1073
+ if (metadata.error)
1074
+ throw new ConfigError(metadata.error, "INVALID_CONFIG_FILE");
1075
+ const journal = metadata.journal;
1076
+ if (!journal || !isPreConversionCompatiblePhase(journal.phase))
1077
+ return;
1078
+ const current = fingerprintMigrationGeneration();
1079
+ if (isAuthenticatedCutoverAdjacent(journal, current))
1080
+ return;
1081
+ if (readSingleFileBoundStateMarker(journal)?.phase === "state-applied") {
1082
+ if (sameMigrationGeneration(current, journal.generation) ||
1083
+ isAuthenticatedWorkflowAdjacent(journal, current) ||
1084
+ isAuthenticatedCutoverAdjacent(journal, current)) {
1085
+ return;
1086
+ }
1087
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1088
+ }
1089
+ if (!sameMigrationGeneration(current, journal.generation) &&
1090
+ !isAuthenticatedWorkflowAdjacent(journal, current) &&
1091
+ !isAuthenticatedCutoverAdjacent(journal, current)) {
1092
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1093
+ }
1094
+ }
1095
+ function preparePreConversionJournalForApply() {
1096
+ const metadata = readApplyJournalMetadata();
1097
+ if (metadata.error)
1098
+ throw new ConfigError(metadata.error, "INVALID_CONFIG_FILE");
1099
+ const journal = metadata.journal;
1100
+ if (!journal || !isPreConversionCompatiblePhase(journal.phase))
1101
+ return;
1102
+ let current = fingerprintMigrationGeneration();
1103
+ if (isAuthenticatedCutoverAdjacent(journal, current))
1104
+ return;
1105
+ if (readSingleFileBoundStateMarker(journal)?.phase === "state-applied")
1106
+ return;
1107
+ if (!sameMigrationGeneration(current, journal.generation)) {
1108
+ if (!isAuthenticatedWorkflowAdjacent(journal, current)) {
1109
+ throw new ConfigError(`Migration apply journal phase ${journal.phase} does not match the exact live artifact generation.`, "INVALID_CONFIG_FILE");
1110
+ }
1111
+ current = fingerprintMigrationGeneration();
1112
+ }
1113
+ journal.phase = "state-converting";
1114
+ journal.generation = current;
1115
+ writeApplyJournal(journal);
270
1116
  }
271
1117
  function writeApplyJournal(journal) {
272
1118
  fs.mkdirSync(path.dirname(getMigrationApplyJournalPath()), { recursive: true, mode: 0o700 });
@@ -278,8 +1124,7 @@ function writeApplyJournal(journal) {
278
1124
  writeFileAtomic(getMigrationApplyJournalPath(), serialized, 0o600);
279
1125
  }
280
1126
  function advanceApplyJournal(journal, phase) {
281
- const order = ["prepared", "state-applied", "workflow-applied", "config-applied", "committed"];
282
- if (order.indexOf(phase) > order.indexOf(journal.phase))
1127
+ if (APPLY_PHASE_ORDER.indexOf(phase) > APPLY_PHASE_ORDER.indexOf(journal.phase))
283
1128
  journal.phase = phase;
284
1129
  journal.generation = fingerprintMigrationGeneration();
285
1130
  writeApplyJournal(journal);
@@ -307,11 +1152,344 @@ function crashInMutationGapForTests(phase) {
307
1152
  if (process.env.AKM_TEST_MIGRATION_CRASH_GAP === phase)
308
1153
  process.kill(process.pid, "SIGKILL");
309
1154
  }
1155
+ function isForwardRecoveryPhase(phase) {
1156
+ return [
1157
+ "state-collapsing",
1158
+ "cutover-applied",
1159
+ "config-applied",
1160
+ "tasks-prepared",
1161
+ "tasks-applied",
1162
+ "pilot-prepared",
1163
+ "pilot-applied",
1164
+ "committed",
1165
+ ].includes(phase);
1166
+ }
1167
+ /** Publish a preflighted task-target batch inside the journal's forward-only region. */
1168
+ function runTaskTargetMigrationStep(journal, plan) {
1169
+ applyTaskTargetRefMigration(plan);
1170
+ crashInMutationGapForTests("tasks");
1171
+ advanceApplyJournal(journal, "tasks-applied");
1172
+ crashAfterForTests("tasks");
1173
+ }
1174
+ function isTaskOnlyRepair(manifest) {
1175
+ return (manifest.artifacts["config.json"].status === "current" &&
1176
+ ["current", "missing"].includes(manifest.artifacts["state.db"].status) &&
1177
+ manifest.artifacts["workflow.db"].status === "missing");
1178
+ }
1179
+ function runTaskOnlyRepair(journal, plan) {
1180
+ advanceApplyJournal(journal, "tasks-prepared");
1181
+ if (journal.phase === "tasks-prepared")
1182
+ runTaskTargetMigrationStep(journal, plan);
1183
+ advanceApplyJournal(journal, "committed");
1184
+ clearApplyJournal();
1185
+ }
1186
+ function runStateMigrationStep(journal) {
1187
+ if (journal.phase === "prepared") {
1188
+ advanceApplyJournal(journal, "state-converting");
1189
+ crashAfterForTests("state-converting");
1190
+ }
1191
+ if (journal.phase === "state-converting") {
1192
+ const db = openDatabaseFinalizing(getStateDbPathInDataDir());
1193
+ try {
1194
+ const marker = readBoundStateGenerationMarker(db, journal.operationId);
1195
+ if (marker?.phase === "state-applied") {
1196
+ throw new ConfigError("state.db is ahead of its state-converting journal.", "INVALID_CONFIG_FILE");
1197
+ }
1198
+ if (!marker) {
1199
+ db.transaction(() => {
1200
+ runStateMigrations(db);
1201
+ bindStateConvertingMarker(db, journal.operationId);
1202
+ })();
1203
+ }
1204
+ else {
1205
+ runStateMigrations(db, { applyPending: false });
1206
+ }
1207
+ }
1208
+ finally {
1209
+ db.close();
1210
+ }
1211
+ crashInMutationGapForTests("state-marker");
1212
+ advanceApplyJournal(journal, "state-collapsing");
1213
+ crashAfterForTests("state-marker");
1214
+ }
1215
+ if (journal.phase === "state-collapsing") {
1216
+ if (!sameMigrationGeneration(fingerprintMigrationGeneration(), journal.generation)) {
1217
+ const marker = readBoundStateGenerationMarkerFromDisk(journal.operationId);
1218
+ if (!marker) {
1219
+ throw new ConfigError("state.db does not match the exact marker-bound generation recorded before conversion.", "INVALID_CONFIG_FILE");
1220
+ }
1221
+ journal.phase = marker.phase === "state-applied" ? "state-applied" : "state-collapsing";
1222
+ journal.generation = fingerprintMigrationGeneration();
1223
+ writeApplyJournal(journal);
1224
+ if (journal.phase === "state-applied")
1225
+ return;
1226
+ }
1227
+ const db = openDatabaseFinalizing(getStateDbPathInDataDir());
1228
+ try {
1229
+ if (!readBoundStateGenerationMarker(db, journal.operationId)) {
1230
+ throw new ConfigError("state.db lacks its exact marker-bound conversion generation.", "INVALID_CONFIG_FILE");
1231
+ }
1232
+ collapseStateDbToSingleFile(db);
1233
+ crashInMutationGapForTests("state-converting");
1234
+ advanceBoundStateMarker(db, journal.operationId);
1235
+ }
1236
+ finally {
1237
+ db.close();
1238
+ }
1239
+ crashInMutationGapForTests("state");
1240
+ advanceApplyJournal(journal, "state-applied");
1241
+ crashAfterForTests("state");
1242
+ }
1243
+ }
1244
+ function stateDatabaseHeaderUsesWal(filePath) {
1245
+ const fd = fs.openSync(filePath, "r");
1246
+ try {
1247
+ const header = Buffer.alloc(20);
1248
+ if (fs.readSync(fd, header, 0, header.byteLength, 0) !== header.byteLength) {
1249
+ throw new ConfigError(`Cannot verify the SQLite header for ${filePath}.`, "INVALID_CONFIG_FILE");
1250
+ }
1251
+ if (!header.subarray(0, 16).equals(Buffer.from("SQLite format 3\0"))) {
1252
+ throw new ConfigError(`Cannot verify a non-SQLite state database at ${filePath}.`, "INVALID_CONFIG_FILE");
1253
+ }
1254
+ return header[18] === 2 || header[19] === 2;
1255
+ }
1256
+ finally {
1257
+ fs.closeSync(fd);
1258
+ }
1259
+ }
1260
+ function assertStateReadyForCutover(journal) {
1261
+ const statePath = getStateDbPathInDataDir();
1262
+ if (!fs.existsSync(statePath)) {
1263
+ throw new ConfigError("Cannot run cutover without state.db.", "INVALID_CONFIG_FILE");
1264
+ }
1265
+ if (stateDatabaseHeaderUsesWal(statePath)) {
1266
+ throw new ConfigError("Refusing cutover because state.db still uses WAL; resume through the state-converting phase first.", "INVALID_CONFIG_FILE");
1267
+ }
1268
+ const db = openDatabaseFinalizing(statePath, { readonly: true });
1269
+ try {
1270
+ const journalMode = String(db.prepare("PRAGMA journal_mode").get()?.journal_mode ?? "").toLowerCase();
1271
+ const hasMarkerTable = db
1272
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='akm_migration_generation'")
1273
+ .get();
1274
+ const marker = hasMarkerTable
1275
+ ? db
1276
+ .prepare("SELECT operation_id, phase FROM akm_migration_generation WHERE singleton=1 LIMIT 2")
1277
+ .all()
1278
+ : [];
1279
+ if (journalMode !== "delete" ||
1280
+ marker.length !== 1 ||
1281
+ marker[0]?.operation_id !== journal.operationId ||
1282
+ marker[0]?.phase !== "state-applied") {
1283
+ throw new ConfigError("Refusing cutover because state.db lacks the operation-bound state-converting/single-file proof.", "INVALID_CONFIG_FILE");
1284
+ }
1285
+ }
1286
+ finally {
1287
+ db.close();
1288
+ }
1289
+ }
1290
+ function assertMigrationArtifactsComplete() {
1291
+ const completed = inspectMigrationState();
1292
+ if (completed.config.status !== "current" ||
1293
+ ![completed.state.status, completed.workflow.status].every((status) => status === "current" || status === "missing")) {
1294
+ throw new ConfigError("Migration verification did not reach one current cross-artifact generation.", "INVALID_CONFIG_FILE");
1295
+ }
1296
+ }
1297
+ /** Expand a leading `~` against the home directory (config stashDir/source paths may use it). */
1298
+ function expandTilde(p) {
1299
+ if (p === "~")
1300
+ return os.homedir();
1301
+ if (p.startsWith("~/") || p.startsWith("~\\"))
1302
+ return path.join(os.homedir(), p.slice(2));
1303
+ return p;
1304
+ }
1305
+ /**
1306
+ * Stash roots for the cutover ref map's origin aliases, the
1307
+ * source-(b) legacy walk, AND the WI-8.5d content migration's `.stash.json`
1308
+ * fold + D-R6 rename walk. Derived from the TARGET config, which by this point in
1309
+ * the apply has already been normalized to the 0.9 `bundles` shape by
1310
+ * {@link parseMigrationTargetConfig}: each path-bearing bundle is a root, the
1311
+ * `defaultBundle` is the primary, and a bundle's `registryId` (or its key)
1312
+ * supplies the origin alias. The pre-cutover `stashDir`/`sources` shape is still
1313
+ * honored as a fallback for a transitional config that reaches here un-migrated.
1314
+ * Source (a) (the index `item_ref` join) is authoritative, so an unresolved root
1315
+ * only costs a few origin aliases.
1316
+ */
1317
+ function cutoverStashRootsFromConfig(config) {
1318
+ const roots = [];
1319
+ const bundles = config.bundles;
1320
+ if (bundles && typeof bundles === "object") {
1321
+ for (const [id, entry] of Object.entries(bundles)) {
1322
+ const bundlePath = entry.path;
1323
+ if (typeof bundlePath !== "string" || bundlePath.length === 0)
1324
+ continue; // only filesystem bundles
1325
+ const registryId = entry.registryId ?? id;
1326
+ roots.push({
1327
+ path: path.resolve(expandTilde(bundlePath)),
1328
+ registryId,
1329
+ primary: config.defaultBundle === id,
1330
+ });
1331
+ }
1332
+ }
1333
+ return roots;
1334
+ }
1335
+ /**
1336
+ * Roll a pre-cutover workflow.db forward to its final ledger (010) using the
1337
+ * FROZEN migration bodies (`src/migrate/legacy/workflow-migrations-bodies.ts`)
1338
+ * through the shared engine — never the live `WORKFLOW_MIGRATIONS` array
1339
+ * (`src/workflows/db.ts` is deleted in WI-8.3). The roll materialises every
1340
+ * migration-added column + DEFAULT so the subsequent state.db merge carries
1341
+ * faithful data.
1342
+ *
1343
+ * Pre-versioning (0.7-era) workflow.dbs — rows present but NO `schema_migrations`
1344
+ * ledger — are OUT of the migrator FROM-state (the rc-train fixtures pin a
1345
+ * versioned ledger). We FAIL CLOSED with a clear message rather than
1346
+ * bootstrapping (the old `bootstrapPreVersioningDb` back-fill is retired).
1347
+ */
1348
+ function runFrozenWorkflowRoll(operationId) {
1349
+ const workflowPath = getLegacyWorkflowDbPath();
1350
+ const db = openDatabaseFinalizing(workflowPath);
1351
+ try {
1352
+ const hasRuns = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='workflow_runs'").get();
1353
+ const hasLedger = !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'").get();
1354
+ if (hasRuns && !hasLedger) {
1355
+ throw new ConfigError(`Refusing to migrate a pre-versioning workflow.db at ${workflowPath} (no schema_migrations ledger). ` +
1356
+ "Pre-0.8 workflow databases are not a supported migrator source; upgrade through a 0.8.x release first.", "INVALID_CONFIG_FILE");
1357
+ }
1358
+ // Roll the pending frozen migrations ONLY — never the base-schema DDL. Any
1359
+ // real pre-cutover workflow.db already carries the base schema (its runtime
1360
+ // opener created it); running the baseline CREATE INDEX here would fail on a
1361
+ // pre-existing-but-narrower table. Matches the WI-8.2 workflow-applied path
1362
+ // (openDatabase + runSqliteMigrations, no pragmas/base-schema), preserving
1363
+ // the crash-recovery generation-fingerprint invariants that flow pins.
1364
+ runSqliteMigrations(db, FROZEN_WORKFLOW_MIGRATIONS, {
1365
+ generationMarker: { operationId, phase: "workflow-applied" },
1366
+ });
1367
+ }
1368
+ finally {
1369
+ db.close();
1370
+ }
1371
+ }
1372
+ /**
1373
+ * The `cutover-applied` phase (Chunk 8, WI-8.2). Builds + persists the
1374
+ * old-ref → item_ref map, runs the fail-closed three-DB merge/re-key
1375
+ * transaction, then the idempotent index-quarantine / workflow.db-unlink
1376
+ * boundary ops. A committed merge marker (from an interrupted-then-resumed
1377
+ * apply) short-circuits the merge so it runs exactly once.
1378
+ */
1379
+ function runCutoverStep(journal, target) {
1380
+ const statePath = getStateDbPathInDataDir();
1381
+ const workflowPath = getLegacyWorkflowDbPath();
1382
+ const indexPath = getDbPath();
1383
+ const stashRoots = cutoverStashRootsFromConfig(target);
1384
+ if (!cutoverMergeCommitted(statePath, journal.operationId)) {
1385
+ const refMap = buildCutoverRefMap({
1386
+ oldIndexDbPath: indexPath,
1387
+ stashRoots,
1388
+ mapOutputPath: cutoverRefMapPath(journal),
1389
+ });
1390
+ // Fail-closed: an integrity failure (unparseable ref / row-count mismatch)
1391
+ // throws a CutoverIntegrityError, which the outer catch converts to a
1392
+ // restore-from-backup. The state txn is atomic — a throw rolls it back, so
1393
+ // state.db + workflow.db are unchanged going into the rollback.
1394
+ runThreeDbCutover({ refMap, operationId: journal.operationId, statePath, workflowPath, oldIndexPath: indexPath });
1395
+ crashInMutationGapForTests("cutover-commit");
1396
+ }
1397
+ else {
1398
+ assertPostCutoverWorkflowAuthenticated(journal, fingerprintMigrationGeneration());
1399
+ }
1400
+ // Boundary ops run AFTER the committed state txn, OUTSIDE the fail-closed gate
1401
+ // (cutover-design.md §2 step 5/6). Idempotent + best-effort — they log and
1402
+ // return, never throw, so a rename/unlink hiccup never rolls back the merge.
1403
+ quarantineIndexDb(journal.operationId, indexPath);
1404
+ deleteWorkflowDb(workflowPath);
1405
+ // WI-8.5d: the content migration (`.stash.json` fold + delete, D-R6 reserved-
1406
+ // filename conformance) is an ADDITIVE filesystem step of the same phase. It
1407
+ // also runs AFTER the committed state txn, is best-effort (a throw is swallowed
1408
+ // + logged, never aborting a committed cutover), and idempotent (a resumed
1409
+ // apply finds no sidecars and no mis-named concepts, so it re-runs to a no-op).
1410
+ runContentMigrationStep(journal, target);
1411
+ }
1412
+ function cutoverRefMapPath(journal) {
1413
+ return path.join(path.dirname(getMigrationApplyJournalPath()), `cutover-refmap-${journal.operationId}.json`);
1414
+ }
1415
+ /** Required forward-only filesystem step after the core config/database cutover verifies. */
1416
+ function runPilotTreatmentStep(journal, target) {
1417
+ const refMap = loadCutoverRefMap(cutoverRefMapPath(journal));
1418
+ migratePilotTreatmentFiles(cutoverStashRootsFromConfig(target), refMap);
1419
+ }
1420
+ /**
1421
+ * Persist location for the content-migration report — next to the ApplyJournal
1422
+ * (alongside the cutover ref map). Survives `clearApplyJournal` (which removes
1423
+ * only the journal file), so the operator + the WI-8.5d test can read the D-R6
1424
+ * rename list after a committed apply.
1425
+ */
1426
+ function contentMigrationReportPath() {
1427
+ return path.join(path.dirname(getMigrationApplyJournalPath()), "content-migration-report.json");
1428
+ }
1429
+ /** Best-effort content migration + report persistence (see {@link runCutoverStep}). */
1430
+ function runContentMigrationStep(journal, target) {
1431
+ try {
1432
+ const roots = cutoverStashRootsFromConfig(target).map((r) => r.path);
1433
+ const report = runContentMigration(roots);
1434
+ // Fold the one-time pre-0.9 filesystem-proposal import into this same
1435
+ // additive step (it used to run on every proposal operation via
1436
+ // `withProposalsDb`). state.db has been merged + collapsed to single-file
1437
+ // DELETE mode by this point, so we open it raw and INSERT OR IGNORE each
1438
+ // legacy `proposal.json` on its UUID — idempotent, no ledger needed.
1439
+ //
1440
+ // rc-window edge: a user who already ran `akm migrate apply` on an EARLIER
1441
+ // rc binary (before this fold existed) AND never ran a proposal command
1442
+ // afterward would have their pre-0.9 fs proposals un-imported — the old
1443
+ // live-path import that once covered that gap is gone. Re-running this
1444
+ // idempotent `migrate apply` recovers them (the legacy files are left in
1445
+ // place on import, so they are still there to re-walk). Acceptable for an rc.
1446
+ report.legacyProposalsImported = importLegacyProposalsIntoState(getStateDbPathInDataDir(), roots);
1447
+ persistContentMigrationReport(report);
1448
+ if (report.sidecarsFolded > 0 || report.reservedRenames.length > 0 || report.legacyProposalsImported > 0) {
1449
+ console.log(JSON.stringify({
1450
+ event: "content-migration",
1451
+ operationId: journal.operationId,
1452
+ ...report,
1453
+ }));
1454
+ }
1455
+ }
1456
+ catch (error) {
1457
+ console.error(`[akm] content migration skipped (${error instanceof Error ? error.message : String(error)}); the committed cutover is unaffected.`);
1458
+ }
1459
+ }
1460
+ function persistContentMigrationReport(report) {
1461
+ try {
1462
+ const reportPath = contentMigrationReportPath();
1463
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true, mode: 0o700 });
1464
+ writeFileAtomic(reportPath, `${JSON.stringify(report, null, 2)}\n`, 0o600);
1465
+ }
1466
+ catch {
1467
+ // The report is auditing-only; a persistence failure never affects the cutover.
1468
+ }
1469
+ }
310
1470
  function unsafeArtifact(name, state) {
311
1471
  if (!["newer", "inconsistent", "corrupt"].includes(state.status))
312
1472
  return undefined;
313
1473
  return `${name} is ${state.status}${state.detail ? `: ${state.detail}` : ""}`;
314
1474
  }
1475
+ /**
1476
+ * Parse + validate a migration TARGET config, applying the Chunk-8 config-shape
1477
+ * migration (`stashDir`/`sources[]`/`installed[]` → `bundles`/`defaultBundle`)
1478
+ * as a pre-validation transform. This is why a target still carrying the
1479
+ * pre-cutover source shape loads (and reports "current" in `migrate status`)
1480
+ * even though the runtime loader now rejects that shape once `bundles` exists:
1481
+ * the migrator normalizes it FIRST, then the strict schema gates the result.
1482
+ *
1483
+ * The transform is idempotent, so an already-migrated prepared config, or the
1484
+ * new-shape config re-parsed from the apply journal, passes through untouched.
1485
+ */
1486
+ function parseMigrationTargetConfig(text, sourcePath) {
1487
+ const raw = parseConfigText(text, sourcePath);
1488
+ const migrated = migrateConfigSourcesToBundles(raw);
1489
+ // Re-serialize the normalized object through the canonical validator so the
1490
+ // version check, schema validation, and defaults merge stay in one place.
1491
+ return parseAndValidateConfigText(JSON.stringify(migrated), sourcePath);
1492
+ }
315
1493
  function loadTargetConfig(preparedConfigPath, artifacts) {
316
1494
  const targetPath = preparedConfigPath ?? (artifacts.config.status === "current" ? getConfigPath() : undefined);
317
1495
  if (!targetPath) {
@@ -340,7 +1518,7 @@ function loadTargetConfig(preparedConfigPath, artifacts) {
340
1518
  source: preparedConfigPath ? "prepared" : "active",
341
1519
  path: targetPath,
342
1520
  },
343
- config: parseAndValidateConfigText(text, targetPath),
1521
+ config: parseMigrationTargetConfig(text, targetPath),
344
1522
  };
345
1523
  }
346
1524
  catch (error) {
@@ -355,7 +1533,7 @@ function loadTargetConfig(preparedConfigPath, artifacts) {
355
1533
  }
356
1534
  }
357
1535
  function buildMigrationPlan(preparedConfigPath, activeApply) {
358
- const artifacts = inspectMigrationState();
1536
+ const artifacts = activeApply.artifacts ?? inspectMigrationStateFromSnapshots();
359
1537
  const restorePending = fs.existsSync(getMigrationRestoreJournalPath());
360
1538
  const target = activeApply.journal
361
1539
  ? {
@@ -375,14 +1553,25 @@ function buildMigrationPlan(preparedConfigPath, activeApply) {
375
1553
  ].filter((blocker) => blocker !== undefined);
376
1554
  if (target.state.status !== "current")
377
1555
  blockers.push(target.state.detail ?? "A current target config is required.");
378
- if (activeApply.error && !activeApply.journal)
1556
+ if (activeApply.error && (!activeApply.journal || target.state.status === "current"))
379
1557
  blockers.push(activeApply.error);
380
1558
  if (restorePending)
381
1559
  blockers.push(`Restore recovery is pending at ${getMigrationRestoreJournalPath()}.`);
1560
+ let taskRewrites = 0;
1561
+ if (blockers.length === 0 && target.config) {
1562
+ try {
1563
+ taskRewrites = planTaskTargetRefMigration(target.config).rewrites.length;
1564
+ }
1565
+ catch (error) {
1566
+ blockers.push(error instanceof Error ? error.message : String(error));
1567
+ }
1568
+ }
382
1569
  const needsApply = !!activeApply.journal ||
383
1570
  artifacts.config.status !== "current" ||
384
1571
  artifacts.state.status === "old" ||
385
- artifacts.workflow.status === "old";
1572
+ artifacts.workflow.status === "old" ||
1573
+ artifacts.workflow.status === "current" ||
1574
+ taskRewrites > 0;
386
1575
  return {
387
1576
  status: blockers.length > 0 ? "blocked" : needsApply ? "ready" : "current",
388
1577
  artifacts,
@@ -432,8 +1621,12 @@ export async function runMigrationApply(options = {}) {
432
1621
  return;
433
1622
  }
434
1623
  const result = withConfigLock(() => withMaintenanceStartBarrier(() => {
435
- assertNoArtifactReplacementBlockers();
436
1624
  recoverInterruptedRestoreWithLocksHeld();
1625
+ authenticatePreConversionJournalForApply();
1626
+ assertNoArtifactReplacementBlockers(undefined, {
1627
+ skipWorkflowClaims: fs.existsSync(getMigrationApplyJournalPath()),
1628
+ });
1629
+ preparePreConversionJournalForApply();
437
1630
  const active = readApplyJournal();
438
1631
  if (active.error)
439
1632
  throw new ConfigError(active.error, "INVALID_CONFIG_FILE");
@@ -474,69 +1667,103 @@ export async function runMigrationApply(options = {}) {
474
1667
  };
475
1668
  if (!active.journal)
476
1669
  writeApplyJournal(journal);
1670
+ const taskOnlyRepair = isTaskOnlyRepair(backup.manifest);
1671
+ let forwardRecoveryRequired = isForwardRecoveryPhase(journal.phase);
477
1672
  try {
478
- const beforeState = inspectMigrationState();
479
- if (beforeState.state.status === "old") {
480
- const db = openDatabase(getStateDbPathInDataDir());
481
- try {
482
- runStateMigrations(db, {
483
- generationMarker: { operationId: journal.operationId, phase: "state-applied" },
484
- });
1673
+ const taskTargetPlan = planTaskTargetRefMigration(target);
1674
+ if (taskOnlyRepair) {
1675
+ forwardRecoveryRequired = true;
1676
+ runTaskOnlyRepair(journal, taskTargetPlan);
1677
+ return { plan: inspectMigrationPlan(), backup };
1678
+ }
1679
+ runStateMigrationStep(journal);
1680
+ forwardRecoveryRequired = isForwardRecoveryPhase(journal.phase);
1681
+ if (isPostCutoverPhase(journal.phase))
1682
+ runCutoverStep(journal, target);
1683
+ if (journal.phase === "state-applied") {
1684
+ // Roll the pre-cutover workflow.db through the frozen migration bodies
1685
+ // only while the journal is still before the committed cutover.
1686
+ const beforeWorkflow = inspectExactApplyJournalGeneration(journal);
1687
+ if (beforeWorkflow.workflow.status === "old") {
1688
+ runFrozenWorkflowRoll(journal.operationId);
485
1689
  }
486
- finally {
487
- db.close();
1690
+ else if (beforeWorkflow.workflow.status !== "current" && beforeWorkflow.workflow.status !== "missing") {
1691
+ throw new ConfigError(`Cannot resume workflow.db from ${beforeWorkflow.workflow.status} state.`, "INVALID_CONFIG_FILE");
488
1692
  }
1693
+ crashInMutationGapForTests("workflow");
1694
+ advanceApplyJournal(journal, "workflow-applied");
1695
+ crashAfterForTests("workflow");
489
1696
  }
490
- else if (beforeState.state.status !== "current" && beforeState.state.status !== "missing") {
491
- throw new ConfigError(`Cannot resume state.db from ${beforeState.state.status} state.`, "INVALID_CONFIG_FILE");
1697
+ if (journal.phase === "workflow-applied") {
1698
+ const migrated = inspectExactApplyJournalGeneration(journal);
1699
+ for (const [name, state] of [
1700
+ ["state.db", migrated.state],
1701
+ ["workflow.db", migrated.workflow],
1702
+ ]) {
1703
+ if (state.status !== "current" && state.status !== "missing") {
1704
+ throw new ConfigError(`Migration left ${name} in ${state.status} state.`, "INVALID_CONFIG_FILE");
1705
+ }
1706
+ }
1707
+ assertStateReadyForCutover(journal);
1708
+ runCutoverStep(journal, target);
1709
+ forwardRecoveryRequired = true;
1710
+ crashInMutationGapForTests("cutover");
1711
+ advanceApplyJournal(journal, "cutover-applied");
1712
+ crashAfterForTests("cutover");
492
1713
  }
493
- crashInMutationGapForTests("state");
494
- advanceApplyJournal(journal, "state-applied");
495
- crashAfterForTests("state");
496
- const beforeWorkflow = inspectMigrationState();
497
- if (beforeWorkflow.workflow.status === "old") {
498
- const db = openDatabase(getWorkflowDbPath());
1714
+ if (journal.phase === "cutover-applied") {
1715
+ // The cutover is committed, so all remaining work is forward-only.
499
1716
  try {
500
- runWorkflowMigrations(db, {
501
- generationMarker: { operationId: journal.operationId, phase: "workflow-applied" },
502
- });
1717
+ const preCutoverText = readConfigText(getConfigPath());
1718
+ if (preCutoverText !== undefined) {
1719
+ mergeLockEntriesSync(migratedLockEntries(parseConfigText(preCutoverText, getConfigPath())));
1720
+ }
503
1721
  }
504
- finally {
505
- db.close();
1722
+ catch {
1723
+ // Advisory lock re-key only; the committed cutover is unaffected.
506
1724
  }
1725
+ backupExistingConfig(getConfigPath());
1726
+ writeConfigAtomic(getConfigPath(), sanitizeConfigForWrite(target));
1727
+ resetConfigCache();
1728
+ crashInMutationGapForTests("config");
1729
+ advanceApplyJournal(journal, "config-applied");
1730
+ crashAfterForTests("config");
507
1731
  }
508
- else if (beforeWorkflow.workflow.status !== "current" && beforeWorkflow.workflow.status !== "missing") {
509
- throw new ConfigError(`Cannot resume workflow.db from ${beforeWorkflow.workflow.status} state.`, "INVALID_CONFIG_FILE");
510
- }
511
- crashInMutationGapForTests("workflow");
512
- advanceApplyJournal(journal, "workflow-applied");
513
- crashAfterForTests("workflow");
514
- const migrated = inspectMigrationState();
515
- for (const [name, state] of [
516
- ["state.db", migrated.state],
517
- ["workflow.db", migrated.workflow],
518
- ]) {
519
- if (state.status !== "current" && state.status !== "missing") {
520
- throw new ConfigError(`Migration left ${name} in ${state.status} state.`, "INVALID_CONFIG_FILE");
521
- }
1732
+ if (journal.phase === "config-applied") {
1733
+ assertMigrationArtifactsComplete();
1734
+ advanceApplyJournal(journal, "tasks-prepared");
522
1735
  }
523
- backupExistingConfig(getConfigPath());
524
- writeConfigAtomic(getConfigPath(), sanitizeConfigForWrite(target));
525
- resetConfigCache();
526
- crashInMutationGapForTests("config");
527
- advanceApplyJournal(journal, "config-applied");
528
- crashAfterForTests("config");
529
- const completedArtifacts = inspectMigrationState();
530
- if (completedArtifacts.config.status !== "current" ||
531
- ![completedArtifacts.state.status, completedArtifacts.workflow.status].every((status) => status === "current" || status === "missing")) {
532
- throw new ConfigError("Migration verification did not reach one current cross-artifact generation.", "INVALID_CONFIG_FILE");
533
- }
534
- advanceApplyJournal(journal, "committed");
1736
+ if (journal.phase === "tasks-prepared") {
1737
+ forwardRecoveryRequired = true;
1738
+ runTaskTargetMigrationStep(journal, taskTargetPlan);
1739
+ }
1740
+ if (journal.phase === "tasks-applied")
1741
+ advanceApplyJournal(journal, "pilot-prepared");
1742
+ if (journal.phase === "pilot-prepared") {
1743
+ forwardRecoveryRequired = true;
1744
+ runPilotTreatmentStep(journal, target);
1745
+ crashInMutationGapForTests("pilot");
1746
+ advanceApplyJournal(journal, "pilot-applied");
1747
+ crashAfterForTests("pilot");
1748
+ }
1749
+ if (journal.phase === "pilot-applied")
1750
+ advanceApplyJournal(journal, "committed");
535
1751
  clearApplyJournal();
536
1752
  const completed = inspectMigrationPlan();
537
1753
  return { plan: completed, backup };
538
1754
  }
539
1755
  catch (error) {
1756
+ if (error instanceof MigrationPreflightGenerationError || error instanceof MigrationSnapshotChangedError) {
1757
+ forwardRecoveryRequired = true;
1758
+ }
1759
+ if (isForwardRecoveryPhase(journal.phase))
1760
+ forwardRecoveryRequired = true;
1761
+ if (!forwardRecoveryRequired && cutoverMergeCommitted(getStateDbPathInDataDir(), journal.operationId)) {
1762
+ forwardRecoveryRequired = true;
1763
+ }
1764
+ if (forwardRecoveryRequired) {
1765
+ throw new ConfigError(`Migration apply requires forward recovery from ${getMigrationApplyJournalPath()}: ${error instanceof Error ? error.message : String(error)}`, "INVALID_CONFIG_FILE");
1766
+ }
540
1767
  try {
541
1768
  const rollbackGeneration = fingerprintMigrationGeneration();
542
1769
  assertRollbackTransitionAllowed(journal, rollbackGeneration);