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

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -1,11 +1,114 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { randomUUID } from "node:crypto";
4
5
  import fs from "node:fs";
5
- import { isProcessAlive } from "./common.js";
6
+ import path from "node:path";
7
+ import { openDatabase } from "../storage/database.js";
8
+ import { isProcessAlive, MAX_LOCK_METADATA_BYTES, readTextFileDescriptorWithLimit } from "./common.js";
9
+ function readLockSnapshot(lockPath) {
10
+ let fd;
11
+ try {
12
+ fd = fs.openSync(lockPath, "r");
13
+ }
14
+ catch (err) {
15
+ if (err.code === "ENOENT")
16
+ return undefined;
17
+ throw err;
18
+ }
19
+ try {
20
+ const rawContent = readTextFileDescriptorWithLimit(fd, MAX_LOCK_METADATA_BYTES, "Lock metadata", lockPath);
21
+ const stat = fs.fstatSync(fd);
22
+ return {
23
+ rawContent,
24
+ identity: { dev: stat.dev, ino: stat.ino, size: stat.size, mtimeMs: stat.mtimeMs },
25
+ };
26
+ }
27
+ finally {
28
+ fs.closeSync(fd);
29
+ }
30
+ }
31
+ function sameIdentity(left, right) {
32
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs;
33
+ }
34
+ function operationMutexPath(lockPath) {
35
+ // `.sensitive` is an established non-asset suffix across stash walkers. The
36
+ // mutex may sit beside a secret/env lock and must never surface as an asset.
37
+ return path.join(path.dirname(lockPath), `.${path.basename(lockPath)}.operations.sensitive`);
38
+ }
39
+ /**
40
+ * Serialize every mutation of one canonical lock path. SQLite's write lock is
41
+ * released by the OS when a process dies, so this mutex needs no stale-owner
42
+ * deletion protocol (which would reproduce the same check/rename race it is
43
+ * meant to prevent).
44
+ */
45
+ function withLockOperationMutex(lockPath, run) {
46
+ const db = openDatabase(operationMutexPath(lockPath));
47
+ let began = false;
48
+ try {
49
+ db.exec("PRAGMA busy_timeout = 30000");
50
+ for (let attempt = 0; attempt < 5 && !began; attempt += 1) {
51
+ db.exec("BEGIN IMMEDIATE");
52
+ began = db.inTransaction;
53
+ if (!began)
54
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2 ** attempt);
55
+ }
56
+ if (!began)
57
+ throw new Error(`Could not acquire lock operation mutex for ${lockPath}.`);
58
+ const result = run();
59
+ db.exec("COMMIT");
60
+ began = false;
61
+ return result;
62
+ }
63
+ catch (error) {
64
+ if (began && db.inTransaction) {
65
+ try {
66
+ db.exec("ROLLBACK");
67
+ }
68
+ catch {
69
+ // Preserve the operation failure.
70
+ }
71
+ }
72
+ throw error;
73
+ }
74
+ finally {
75
+ db.close();
76
+ }
77
+ }
78
+ function tryAcquireLockRaw(lockPath, payload) {
79
+ try {
80
+ fs.writeFileSync(lockPath, payload, { flag: "wx" });
81
+ }
82
+ catch (err) {
83
+ if (err.code === "EEXIST")
84
+ return undefined;
85
+ throw err;
86
+ }
87
+ let snapshot;
88
+ try {
89
+ snapshot = readLockSnapshot(lockPath);
90
+ }
91
+ catch (error) {
92
+ releaseLockRaw(lockPath);
93
+ throw error;
94
+ }
95
+ if (!snapshot) {
96
+ releaseLockRaw(lockPath);
97
+ throw new Error(`Could not read newly acquired lock at ${lockPath}.`);
98
+ }
99
+ return { lockPath, ...snapshot };
100
+ }
101
+ function releaseLockRaw(lockPath) {
102
+ try {
103
+ fs.unlinkSync(lockPath);
104
+ }
105
+ catch {
106
+ // Sentinel already gone — fine.
107
+ }
108
+ }
6
109
  /**
7
110
  * Atomically create a sentinel at `lockPath` with `payload` as the body.
8
- * Returns true if we now own the lock, false if a sentinel already
111
+ * Returns an exact ownership handle if we now own the lock, or undefined if a sentinel already
9
112
  * exists (EEXIST). Throws any other error (permissions, missing parent
10
113
  * dir, etc.) — callers must ensure the parent directory exists.
11
114
  *
@@ -14,15 +117,11 @@ import { isProcessAlive } from "./common.js";
14
117
  * (improve.ts records pid + startedAt so audit can correlate runs).
15
118
  */
16
119
  export function tryAcquireLockSync(lockPath, payload) {
17
- try {
18
- fs.writeFileSync(lockPath, payload, { flag: "wx" });
19
- return true;
20
- }
21
- catch (err) {
22
- if (err.code === "EEXIST")
23
- return false;
24
- throw err;
25
- }
120
+ return withLockOperationMutex(lockPath, () => tryAcquireLockRaw(lockPath, payload));
121
+ }
122
+ /** Build a PID-bearing payload with a unique token for one acquisition attempt. */
123
+ export function createLockPayload(metadata = {}) {
124
+ return JSON.stringify({ ...metadata, pid: process.pid, lockId: randomUUID() });
26
125
  }
27
126
  /**
28
127
  * Inspect an existing sentinel at `lockPath` without modifying it.
@@ -36,70 +135,116 @@ export function tryAcquireLockSync(lockPath, payload) {
36
135
  * Does NOT remove the file. Callers decide recovery policy.
37
136
  */
38
137
  export function probeLock(lockPath, opts) {
39
- let rawContent;
40
- let ageMs;
41
- try {
42
- rawContent = fs.readFileSync(lockPath, "utf8");
43
- }
44
- catch (err) {
45
- if (err.code === "ENOENT")
46
- return { state: "absent" };
47
- return { state: "stale", reason: "unreadable" };
48
- }
138
+ let snapshot;
49
139
  try {
50
- const stat = fs.statSync(lockPath);
51
- ageMs = Date.now() - stat.mtimeMs;
140
+ snapshot = readLockSnapshot(lockPath);
52
141
  }
53
142
  catch {
54
- // Stat failed even though read succeeded — race-y removal in flight.
55
- return { state: "stale", reason: "unreadable", rawContent };
143
+ return { state: "stale", reason: "unreadable" };
56
144
  }
145
+ if (!snapshot)
146
+ return { state: "absent" };
147
+ const { rawContent, identity } = snapshot;
148
+ const ageMs = Date.now() - identity.mtimeMs;
57
149
  const holderPid = extractHolderPid(rawContent);
58
150
  if (holderPid === undefined) {
59
- return { state: "stale", reason: "invalid_pid", ageMs, rawContent };
151
+ return { state: "stale", reason: "invalid_pid", ageMs, rawContent, identity };
60
152
  }
61
153
  if (!isProcessAlive(holderPid)) {
62
- return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent };
154
+ return { state: "stale", reason: "pid_dead", holderPid, ageMs, rawContent, identity };
63
155
  }
64
156
  if (opts?.staleAfterMs !== undefined && ageMs > opts.staleAfterMs) {
65
- return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent };
157
+ return { state: "stale", reason: "age_exceeded", holderPid, ageMs, rawContent, identity };
66
158
  }
67
- return { state: "held", holderPid, ageMs, rawContent };
159
+ return { state: "held", holderPid, ageMs, rawContent, identity };
68
160
  }
69
161
  /**
70
- * Remove a lock file. Idempotent — silently ignores ENOENT. Used both to
71
- * reclaim stale locks (after probeLock returns `state: "stale"`) and to
72
- * release locks we own (after a successful tryAcquireLockSync).
162
+ * Revalidate and quarantine the probed sentinel while holding the same operation
163
+ * mutex used by acquisitions. A newer owner therefore cannot be renamed in the
164
+ * check/quarantine window, and a third contender cannot acquire until cleanup
165
+ * has completed.
73
166
  */
74
- export function releaseLock(lockPath) {
75
- try {
76
- fs.unlinkSync(lockPath);
77
- }
78
- catch {
79
- // Sentinel already gone — fine.
80
- }
167
+ export function reclaimStaleLock(lockPath, probe, options) {
168
+ if (probe.rawContent === undefined || probe.identity === undefined)
169
+ return false;
170
+ const expectedContent = probe.rawContent;
171
+ const expectedIdentity = probe.identity;
172
+ return withLockOperationMutex(lockPath, () => {
173
+ let current;
174
+ try {
175
+ current = readLockSnapshot(lockPath);
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ if (!current || current.rawContent !== expectedContent || !sameIdentity(current.identity, expectedIdentity)) {
181
+ return false;
182
+ }
183
+ const quarantinePath = `${lockPath}.stale-${process.pid}-${randomUUID()}`;
184
+ try {
185
+ fs.renameSync(lockPath, quarantinePath);
186
+ }
187
+ catch (err) {
188
+ if (err.code === "ENOENT")
189
+ return false;
190
+ throw err;
191
+ }
192
+ let quarantined;
193
+ try {
194
+ quarantined = readLockSnapshot(quarantinePath);
195
+ }
196
+ catch {
197
+ quarantined = undefined;
198
+ }
199
+ if (!quarantined ||
200
+ quarantined.rawContent !== expectedContent ||
201
+ !sameIdentity(quarantined.identity, expectedIdentity)) {
202
+ try {
203
+ // Restore without replacing a non-cooperating lock installed after quarantine.
204
+ fs.linkSync(quarantinePath, lockPath);
205
+ }
206
+ catch (err) {
207
+ if (err.code !== "EEXIST")
208
+ throw err;
209
+ }
210
+ releaseLockRaw(quarantinePath);
211
+ return false;
212
+ }
213
+ options?.afterQuarantineVerified?.();
214
+ try {
215
+ fs.unlinkSync(quarantinePath);
216
+ return true;
217
+ }
218
+ catch (err) {
219
+ if (err.code === "ENOENT")
220
+ return false;
221
+ throw err;
222
+ }
223
+ });
81
224
  }
82
225
  /**
83
- * Release a lock ONLY if it is still owned by `ownerPid`. Safe to call from a
84
- * `process.exit()` / `'exit'` handler as a backstop: `process.exit()` skips
85
- * `finally` blocks — so the normal lock-release never runs on signal death
86
- * (SIGTERM/SIGINT) — but it DOES fire `'exit'` listeners synchronously. Checking
87
- * ownership first means that if the lock was already released and re-acquired by
88
- * a different process, this leaves that process's lock intact (no cross-run
89
- * deletion / PID-reuse footgun). Synchronous so it is valid inside an exit handler.
226
+ * Release only the exact sentinel returned by `tryAcquireLockSync`. Content and
227
+ * file identity are revalidated while holding the acquisition operation mutex,
228
+ * so a stale holder cannot remove a successor. Synchronous and idempotent so it
229
+ * is safe in both `finally` blocks and process exit handlers.
90
230
  */
91
- export function releaseLockIfOwned(lockPath, ownerPid) {
92
- let rawContent;
93
- try {
94
- rawContent = fs.readFileSync(lockPath, "utf8");
95
- }
96
- catch {
97
- // Absent or unreadable — nothing of ours to release.
231
+ export function releaseLock(ownership) {
232
+ const { lockPath } = ownership;
233
+ if (!fs.existsSync(lockPath) && !fs.existsSync(operationMutexPath(lockPath)))
98
234
  return;
99
- }
100
- if (extractHolderPid(rawContent) === ownerPid) {
101
- releaseLock(lockPath);
102
- }
235
+ withLockOperationMutex(lockPath, () => {
236
+ let current;
237
+ try {
238
+ current = readLockSnapshot(lockPath);
239
+ }
240
+ catch {
241
+ // Absent or unreadable — nothing of ours to release.
242
+ return;
243
+ }
244
+ if (current && current.rawContent === ownership.rawContent && sameIdentity(current.identity, ownership.identity)) {
245
+ releaseLockRaw(lockPath);
246
+ }
247
+ });
103
248
  }
104
249
  /**
105
250
  * Extract a PID from a sentinel body. Accepts the two shapes used across
@@ -0,0 +1,392 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * The ONE durable filesystem-transaction engine (plan §2.2 / §4.5, Chunk 6
6
+ * WI-6.3). Replaces the three (+1) per-domain journal engines — proposal
7
+ * accept/revert, proposal reject, mv, consolidate — with a single journal
8
+ * home, journal format, fsync discipline, phase runner, and recovery scanner.
9
+ *
10
+ * ## Journal home
11
+ *
12
+ * `getDataDir()/txn/<rootNs24>/<transactionId>/journal.json`, namespaced by
13
+ * the sha256 of the resolved root the transaction mutates. Every legacy home
14
+ * (`proposal-transactions/`, `proposal-rejections/`, in-stash
15
+ * `.akm/mv-transactions/`, in-stash `consolidate-journal.json`) collapses
16
+ * onto this one.
17
+ *
18
+ * ## Phase model
19
+ *
20
+ * Each transaction KIND declares its ordered phase vocabulary and a commit
21
+ * point. A journal found at a phase strictly BEFORE the commit point rolls
22
+ * BACK (the batch never happened); at or after it rolls FORWARD (the kind's
23
+ * `finalize` resumes idempotently from the recorded phase). Phase writes are
24
+ * durable: tmp + fsync + rename + parent-dir fsync — the exact discipline
25
+ * every legacy engine used, now in one place.
26
+ *
27
+ * ## Kind handlers
28
+ *
29
+ * Domain logic (what the files are, how to roll back, which DB/index/event
30
+ * steps finalize) stays with the domain: each kind registers a
31
+ * {@link TxnKindHandler}. The engine owns discovery, safety fencing, journal
32
+ * I/O, ordering, and cleanup. Recovery entry points call
33
+ * {@link recoverTxnsForRoot} (after importing the domain registrar so the
34
+ * kinds are registered).
35
+ *
36
+ * ## Crash-window test seam
37
+ *
38
+ * `_setTxnMutationHookForTests` replaces the per-engine hooks; domain code
39
+ * fires named points through {@link txnMutationHook} exactly where the legacy
40
+ * engines fired theirs, so the subprocess crash runners re-key mechanically.
41
+ */
42
+ import { createHash, randomUUID } from "node:crypto";
43
+ import fs from "node:fs";
44
+ import path from "node:path";
45
+ import { getDataDir } from "./paths.js";
46
+ import { warn } from "./warn.js";
47
+ // ── Registry ─────────────────────────────────────────────────────────────────
48
+ const kinds = new Map();
49
+ /**
50
+ * Register (or replace) the handler for a transaction kind. Handlers are
51
+ * stored payload-erased; beginTxn/recovery re-associate `P` via the kind tag.
52
+ */
53
+ export function registerTxnKind(kind, handler) {
54
+ kinds.set(kind, handler);
55
+ }
56
+ function requireKind(kind) {
57
+ const handler = kinds.get(kind);
58
+ if (!handler)
59
+ throw new Error(`No transaction handler registered for kind "${kind}".`);
60
+ return handler;
61
+ }
62
+ /** True when `kind` has a registered handler (see {@link recoverTxnsForRoot}). */
63
+ function hasKind(kind) {
64
+ return kinds.has(kind);
65
+ }
66
+ // ── Test seam ────────────────────────────────────────────────────────────────
67
+ let mutationHookForTests;
68
+ /** TEST-ONLY crash-window hook used by subprocess recovery tests. */
69
+ export function _setTxnMutationHookForTests(hook) {
70
+ mutationHookForTests = hook;
71
+ }
72
+ /** Fire a named crash-window point (no-op outside tests). */
73
+ export function txnMutationHook(point) {
74
+ mutationHookForTests?.(point);
75
+ }
76
+ // ── Durable file I/O primitives (shared fsync discipline) ────────────────────
77
+ export function txnHash(content) {
78
+ return createHash("sha256").update(content).digest("hex");
79
+ }
80
+ export function txnFileHash(filePath) {
81
+ return txnHash(fs.readFileSync(filePath));
82
+ }
83
+ export function fsyncTxnFile(filePath) {
84
+ const fd = fs.openSync(filePath, "r");
85
+ try {
86
+ fs.fsyncSync(fd);
87
+ }
88
+ finally {
89
+ fs.closeSync(fd);
90
+ }
91
+ }
92
+ export function fsyncTxnDir(dirPath) {
93
+ try {
94
+ fsyncTxnFile(dirPath);
95
+ }
96
+ catch {
97
+ // Directory fsync is unavailable on some platforms.
98
+ }
99
+ }
100
+ /** Durably write `content` to `filePath` (tmp + fsync + rename + dir fsync). */
101
+ export function writeTxnFileDurably(filePath, content, mode = 0o600) {
102
+ const tempPath = `${filePath}.tmp`;
103
+ fs.writeFileSync(tempPath, content, { mode });
104
+ fsyncTxnFile(tempPath);
105
+ fs.renameSync(tempPath, filePath);
106
+ fsyncTxnDir(path.dirname(filePath));
107
+ }
108
+ // ── Journal home / discovery ─────────────────────────────────────────────────
109
+ /**
110
+ * Canonical spelling of a transaction root: realpath when the root exists
111
+ * (so symlinked spellings — e.g. a stash reached through macOS /tmp — hash
112
+ * to the SAME namespace and bind-compare equal), resolved otherwise.
113
+ */
114
+ export function canonicalTxnRoot(root) {
115
+ try {
116
+ return fs.realpathSync(path.resolve(root));
117
+ }
118
+ catch {
119
+ return path.resolve(root);
120
+ }
121
+ }
122
+ /** Namespace directory for all transactions mutating `root`. */
123
+ export function txnNamespaceDir(root) {
124
+ const ns = txnHash(canonicalTxnRoot(root)).slice(0, 24);
125
+ return path.join(getDataDir(), "txn", ns);
126
+ }
127
+ /** Mint a transaction id ahead of {@link beginTxn} (see its `transactionId`). */
128
+ export function mintTxnId() {
129
+ return randomUUID();
130
+ }
131
+ /** The directory a transaction with `transactionId` on `root` will own. */
132
+ export function txnDirFor(root, transactionId) {
133
+ return path.join(txnNamespaceDir(root), transactionId);
134
+ }
135
+ function writeJournal(txn) {
136
+ writeTxnFileDurably(txn.journalPath, `${JSON.stringify(txn.journal, null, 2)}\n`);
137
+ }
138
+ /** Durably record `phase` on the journal, then mirror it in memory. */
139
+ export function advanceTxn(txn, phase) {
140
+ const handler = requireKind(txn.journal.kind);
141
+ if (!handler.phases.includes(phase)) {
142
+ throw new Error(`Unknown phase "${phase}" for transaction kind "${txn.journal.kind}".`);
143
+ }
144
+ const next = { ...txn.journal, phase };
145
+ writeTxnFileDurably(txn.journalPath, `${JSON.stringify(next, null, 2)}\n`);
146
+ txn.journal.phase = phase;
147
+ }
148
+ /**
149
+ * Open a new transaction: mint the id, create its directory, and durably
150
+ * write the journal at the kind's initial phase. The caller stages sidecar
151
+ * files under `txn.dir` and then applies/finalizes through the kind handler.
152
+ */
153
+ export function beginTxn(args) {
154
+ const handler = requireKind(args.kind);
155
+ const transactionId = args.transactionId ?? randomUUID();
156
+ if (!/^[A-Za-z0-9._-]+$/.test(transactionId) || transactionId === "." || transactionId === "..") {
157
+ throw new Error(`Invalid transaction id "${transactionId}" — must be a plain path segment.`);
158
+ }
159
+ const dir = path.join(txnNamespaceDir(args.root), transactionId);
160
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
161
+ const journal = {
162
+ version: 1,
163
+ kind: args.kind,
164
+ phase: handler.phases[0],
165
+ transactionId,
166
+ root: canonicalTxnRoot(args.root),
167
+ changes: args.changes,
168
+ decidedAt: args.decidedAt ?? new Date().toISOString(),
169
+ payload: args.payload,
170
+ };
171
+ const txn = { journal, journalPath: path.join(dir, "journal.json"), dir };
172
+ writeJournal(txn);
173
+ return txn;
174
+ }
175
+ /** Remove a transaction directory (and its namespace dir when empty). */
176
+ export function cleanupTxn(dir) {
177
+ try {
178
+ fs.rmSync(dir, { recursive: true, force: true });
179
+ try {
180
+ fs.rmdirSync(path.dirname(dir));
181
+ }
182
+ catch {
183
+ // Other transactions may still exist in the namespace.
184
+ }
185
+ return null;
186
+ }
187
+ catch (error) {
188
+ const message = `transaction committed but cleanup failed at ${dir}: ${error instanceof Error ? error.message : String(error)}`;
189
+ warn(`[txn] ${message}`);
190
+ return message;
191
+ }
192
+ }
193
+ /**
194
+ * Grace period (ms) before a transaction directory/journal with no other
195
+ * evidence of activity is treated as stale rather than possibly belonging to
196
+ * a still-running operation. Shared by {@link sweepJournallessTxnDir} (racing
197
+ * a sibling's mkdir→journal window, and the unknown-kind sweep in
198
+ * {@link recoverTxnsForRoot}) and by read-only reporting such as `akm
199
+ * health`'s stale-journal advisory (see {@link listTxnJournalsTolerant}).
200
+ */
201
+ export const TXN_SWEEP_GRACE_MS = 300_000;
202
+ /**
203
+ * Sweep a transaction directory that cannot be recovered — it has NO journal,
204
+ * or (from {@link recoverTxnsForRoot}) a journal whose kind has no registered
205
+ * handler — but only when it is demonstrably stale. All kinds share one
206
+ * namespace per root, so a scanner may encounter a SIBLING transaction inside
207
+ * `beginTxn`'s mkdir→journal window, or one whose registrar this process
208
+ * simply has not imported yet; a grace period keeps the sweep from racing
209
+ * either. Returns true when the directory was removed.
210
+ */
211
+ export function sweepJournallessTxnDir(dir, graceMs = TXN_SWEEP_GRACE_MS) {
212
+ try {
213
+ const age = Date.now() - fs.statSync(dir).mtimeMs;
214
+ if (age < graceMs)
215
+ return false;
216
+ fs.rmSync(dir, { recursive: true, force: true });
217
+ return true;
218
+ }
219
+ catch {
220
+ return false;
221
+ }
222
+ }
223
+ /** True when `candidate` is inside `root` (both resolved). */
224
+ export function isWithinTxnRoot(candidate, root) {
225
+ const rel = path.relative(path.resolve(root), path.resolve(candidate));
226
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
227
+ }
228
+ function readJournal(journalPath) {
229
+ let journal;
230
+ try {
231
+ journal = JSON.parse(fs.readFileSync(journalPath, "utf8"));
232
+ }
233
+ catch (error) {
234
+ throw new Error(`Cannot read transaction journal at ${journalPath}: ${error instanceof Error ? error.message : String(error)}`);
235
+ }
236
+ if (journal.version !== 1 || typeof journal.kind !== "string" || typeof journal.phase !== "string") {
237
+ throw new Error(`Refusing unsafe transaction journal at ${journalPath}.`);
238
+ }
239
+ return journal;
240
+ }
241
+ /** Engine-level safety fences shared by every kind. */
242
+ function fenceJournal(journal, txnDir, root, journalPath) {
243
+ if (canonicalTxnRoot(journal.root) !== canonicalTxnRoot(root)) {
244
+ throw new Error(`Refusing transaction journal bound to a different root at ${journalPath}.`);
245
+ }
246
+ const handler = requireKind(journal.kind);
247
+ if (!handler.phases.includes(journal.phase)) {
248
+ throw new Error(`Refusing transaction journal with unknown phase "${journal.phase}" at ${journalPath}.`);
249
+ }
250
+ for (const change of journal.changes) {
251
+ if (typeof change.path !== "string" || !isWithinTxnRoot(change.path, root)) {
252
+ throw new Error(`Refusing transaction journal touching paths outside its root at ${journalPath}.`);
253
+ }
254
+ }
255
+ handler.validate?.(journal, txnDir, root);
256
+ }
257
+ /** True when `journal.phase` is at or after the kind's commit point. */
258
+ export function isCommittedPhase(journal) {
259
+ const handler = requireKind(journal.kind);
260
+ return handler.phases.indexOf(journal.phase) >= handler.phases.indexOf(handler.commitPhase);
261
+ }
262
+ /**
263
+ * Recover every interrupted transaction under `root`'s namespace: journals
264
+ * before their kind's commit point roll BACK; the rest roll FORWARD through
265
+ * the kind's `finalize`. Fully-finalized directories are swept. The domain
266
+ * registrar (which registers the kinds) must be imported by the caller.
267
+ *
268
+ * A journal whose `kind` has NO registered handler is SWEPT (same stale-dir
269
+ * grace period as {@link sweepJournallessTxnDir}) rather than thrown on. A
270
+ * kind can disappear for good — 0.9.0 deleted `akm mv` and with it the
271
+ * `kind:"mv"` handler — and an unrecoverable leftover journal must never
272
+ * brick every later recovery scan (index refresh, proposal accept/reject) run
273
+ * against the same root. Kinds that ARE registered keep failing LOUDLY on any
274
+ * fence violation: those journals may fence an interrupted, irreversible
275
+ * mutation. The grace period also covers the transient case where the caller
276
+ * has not imported a live kind's registrar yet.
277
+ *
278
+ * `filter` optionally narrows recovery (e.g. one kind, one proposal id). The
279
+ * unknown-kind sweep runs BEFORE the filter: such a journal is garbage no
280
+ * matter what the caller asked to recover, and leaving it behind is what
281
+ * bricks the next scan.
282
+ */
283
+ export async function recoverTxnsForRoot(root, filter) {
284
+ const nsDir = txnNamespaceDir(root);
285
+ const recovered = [];
286
+ if (!fs.existsSync(nsDir))
287
+ return recovered;
288
+ for (const entry of fs.readdirSync(nsDir, { withFileTypes: true })) {
289
+ if (!entry.isDirectory())
290
+ continue;
291
+ const dir = path.join(nsDir, entry.name);
292
+ const journalPath = path.join(dir, "journal.json");
293
+ if (!fs.existsSync(journalPath)) {
294
+ sweepJournallessTxnDir(dir);
295
+ continue;
296
+ }
297
+ const journal = readJournal(journalPath);
298
+ if (!hasKind(journal.kind)) {
299
+ if (sweepJournallessTxnDir(dir)) {
300
+ warn(`[txn] swept unrecoverable journal of unregistered kind "${journal.kind}" at ${journalPath}.`);
301
+ }
302
+ continue;
303
+ }
304
+ if (filter && !filter(journal))
305
+ continue;
306
+ fenceJournal(journal, dir, root, journalPath);
307
+ const handler = requireKind(journal.kind);
308
+ const txn = { journal, journalPath, dir };
309
+ const terminal = handler.phases[handler.phases.length - 1];
310
+ if (!isCommittedPhase(journal)) {
311
+ await handler.rollback(txn);
312
+ }
313
+ else if (journal.phase !== terminal) {
314
+ await handler.finalize(txn);
315
+ }
316
+ recovered.push(journal);
317
+ cleanupTxn(dir);
318
+ }
319
+ return recovered;
320
+ }
321
+ /**
322
+ * Enumerate (without recovering) every journal across ALL namespaces that
323
+ * matches `predicate`. Used by stash-scoped recovery entry points that don't
324
+ * know which roots their interrupted transactions were bound to.
325
+ */
326
+ export function listTxnJournals(predicate) {
327
+ const home = path.join(getDataDir(), "txn");
328
+ const matches = [];
329
+ if (!fs.existsSync(home))
330
+ return matches;
331
+ for (const ns of fs.readdirSync(home, { withFileTypes: true })) {
332
+ if (!ns.isDirectory())
333
+ continue;
334
+ const nsDir = path.join(home, ns.name);
335
+ for (const entry of fs.readdirSync(nsDir, { withFileTypes: true })) {
336
+ if (!entry.isDirectory())
337
+ continue;
338
+ const journalPath = path.join(nsDir, entry.name, "journal.json");
339
+ if (!fs.existsSync(journalPath))
340
+ continue;
341
+ // Unreadable/invalid journals fail LOUDLY: a caller deciding what to
342
+ // recover must never silently overlook a damaged journal (it may fence
343
+ // an interrupted, irreversible mutation).
344
+ const journal = readJournal(journalPath);
345
+ if (predicate(journal))
346
+ matches.push(journal);
347
+ }
348
+ }
349
+ return matches;
350
+ }
351
+ /**
352
+ * Read-only, best-effort sibling of {@link listTxnJournals} for reporting
353
+ * (e.g. `akm health`'s stale-journal advisory): a corrupt `journal.json` is
354
+ * counted rather than thrown, so one damaged journal doesn't abort the whole
355
+ * scan. Recovery call sites (which must decide how to roll a journal forward
356
+ * or back) keep using {@link listTxnJournals} — it fails loudly on purpose,
357
+ * since silently skipping a damaged journal there could leave an interrupted,
358
+ * irreversible mutation unrecovered.
359
+ */
360
+ export function listTxnJournalsTolerant(predicate) {
361
+ const home = path.join(getDataDir(), "txn");
362
+ const matches = [];
363
+ const unreadableMtimes = [];
364
+ if (!fs.existsSync(home))
365
+ return { matches, unreadableMtimes };
366
+ for (const ns of fs.readdirSync(home, { withFileTypes: true })) {
367
+ if (!ns.isDirectory())
368
+ continue;
369
+ const nsDir = path.join(home, ns.name);
370
+ for (const entry of fs.readdirSync(nsDir, { withFileTypes: true })) {
371
+ if (!entry.isDirectory())
372
+ continue;
373
+ const journalPath = path.join(nsDir, entry.name, "journal.json");
374
+ let mtimeMs;
375
+ try {
376
+ mtimeMs = fs.statSync(journalPath).mtimeMs;
377
+ }
378
+ catch {
379
+ continue; // no journal.json here, or it vanished mid-scan
380
+ }
381
+ try {
382
+ const journal = readJournal(journalPath);
383
+ if (predicate(journal))
384
+ matches.push({ journal, mtimeMs });
385
+ }
386
+ catch {
387
+ unreadableMtimes.push(mtimeMs);
388
+ }
389
+ }
390
+ }
391
+ return { matches, unreadableMtimes };
392
+ }