akm-cli 0.9.1 → 0.9.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -0,0 +1,148 @@
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
+ * Side-effect-free SQLite snapshots for planning and preview paths.
6
+ *
7
+ * A normal SQLite read-only connection can still write lock bytes into an
8
+ * existing `-shm` file when the source database is in WAL mode. Dry planning
9
+ * must not do that. This helper copies a stable main/WAL pair and opens that
10
+ * private copy, so SQLite never attaches to the operator's original
11
+ * main/WAL/SHM files.
12
+ */
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { openDatabaseFinalizing } from "./database.js";
17
+ export class SqliteReadSnapshotUnavailableError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "SqliteReadSnapshotUnavailableError";
21
+ }
22
+ }
23
+ function pathExists(filePath) {
24
+ try {
25
+ fs.lstatSync(filePath);
26
+ return true;
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return false;
31
+ throw error;
32
+ }
33
+ }
34
+ function fileFingerprint(filePath) {
35
+ try {
36
+ const stat = fs.statSync(filePath, { bigint: true });
37
+ return { size: stat.size, mtimeNs: stat.mtimeNs, ctimeNs: stat.ctimeNs };
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT")
41
+ return undefined;
42
+ throw error;
43
+ }
44
+ }
45
+ function databaseFingerprint(dbPath) {
46
+ const main = fileFingerprint(dbPath);
47
+ if (!main) {
48
+ throw new SqliteReadSnapshotUnavailableError("SQLite snapshot source disappeared while it was being inspected");
49
+ }
50
+ const wal = fileFingerprint(`${dbPath}-wal`);
51
+ return { main, ...(wal ? { wal } : {}) };
52
+ }
53
+ function fingerprintsEqual(left, right) {
54
+ const sameFile = (a, b) => a?.size === b?.size && a?.mtimeNs === b?.mtimeNs && a?.ctimeNs === b?.ctimeNs;
55
+ return sameFile(left.main, right.main) && sameFile(left.wal, right.wal);
56
+ }
57
+ /**
58
+ * Open an isolated copy of an existing SQLite database.
59
+ *
60
+ * Returns `undefined` only when the source is absent. Committed WAL frames are
61
+ * copied beside the private main file; the source SHM is intentionally not
62
+ * copied because SQLite can safely create one inside the disposable directory.
63
+ * A live rollback-journal database fails closed because a file-level copy
64
+ * cannot distinguish its committed and uncommitted pages without attaching to
65
+ * the source.
66
+ */
67
+ export function openSqliteReadSnapshot(dbPath) {
68
+ if (!pathExists(dbPath))
69
+ return undefined;
70
+ if (pathExists(`${dbPath}-journal`)) {
71
+ throw new SqliteReadSnapshotUnavailableError("an active SQLite rollback journal is present; a non-mutating point-in-time snapshot is unavailable");
72
+ }
73
+ const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-sqlite-read-"));
74
+ const snapshotPath = path.join(snapshotDir, "snapshot.db");
75
+ let db;
76
+ try {
77
+ let copied = false;
78
+ for (let attempt = 0; attempt < 3; attempt++) {
79
+ try {
80
+ if (pathExists(`${dbPath}-journal`)) {
81
+ throw new SqliteReadSnapshotUnavailableError("an active SQLite rollback journal appeared while taking the non-mutating snapshot");
82
+ }
83
+ const before = databaseFingerprint(dbPath);
84
+ fs.copyFileSync(dbPath, snapshotPath);
85
+ if (before.wal)
86
+ fs.copyFileSync(`${dbPath}-wal`, `${snapshotPath}-wal`);
87
+ else
88
+ fs.rmSync(`${snapshotPath}-wal`, { force: true });
89
+ const after = databaseFingerprint(dbPath);
90
+ if (fingerprintsEqual(before, after)) {
91
+ copied = true;
92
+ break;
93
+ }
94
+ }
95
+ catch (error) {
96
+ if (error.code === "ENOENT")
97
+ continue;
98
+ throw error;
99
+ }
100
+ }
101
+ if (!copied) {
102
+ throw new SqliteReadSnapshotUnavailableError("SQLite main/WAL files kept changing while taking the non-mutating snapshot");
103
+ }
104
+ db = openDatabaseFinalizing(snapshotPath, { readonly: true, create: false });
105
+ const closeSnapshot = db.close.bind(db);
106
+ let closed = false;
107
+ const cleanup = (bestEffort) => {
108
+ if (closed)
109
+ return;
110
+ closed = true;
111
+ process.removeListener("exit", cleanupOnExit);
112
+ if (bestEffort) {
113
+ try {
114
+ closeSnapshot();
115
+ }
116
+ catch {
117
+ // Process teardown is already committed; keep removing the copy.
118
+ }
119
+ try {
120
+ fs.rmSync(snapshotDir, { recursive: true, force: true });
121
+ }
122
+ catch {
123
+ // Best-effort process-exit backstop. Normal close still surfaces IO errors.
124
+ }
125
+ return;
126
+ }
127
+ try {
128
+ closeSnapshot();
129
+ }
130
+ finally {
131
+ fs.rmSync(snapshotDir, { recursive: true, force: true });
132
+ }
133
+ };
134
+ const cleanupOnExit = () => cleanup(true);
135
+ process.once("exit", cleanupOnExit);
136
+ db.close = () => cleanup(false);
137
+ return db;
138
+ }
139
+ catch (error) {
140
+ try {
141
+ db?.close();
142
+ }
143
+ finally {
144
+ fs.rmSync(snapshotDir, { recursive: true, force: true });
145
+ }
146
+ throw error;
147
+ }
148
+ }
@@ -30,7 +30,8 @@ import { ConfigError } from "../../core/errors.js";
30
30
  import { getTaskLogDir } from "../../core/paths.js";
31
31
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
32
32
  import { parseSchedule, translateToCron } from "../schedule.js";
33
- import { buildScheduledTaskInvocation, parseScheduledTaskArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
33
+ import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
34
+ import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
34
35
  import { nodeFs, throwIfNotOk } from "./exec-utils.js";
35
36
  const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
36
37
  const END = (id) => `# akm:task ${assertCronValue(id)} END`;
@@ -38,6 +39,7 @@ const DISABLED_PREFIX = "# akm:disabled ";
38
39
  const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
39
40
  const BLOCK_END_RE = /^# akm:task ([\w.@:_-]+) END$/;
40
41
  export const PORTABLE_CRON_LINE_LIMIT = 1000;
42
+ const CRON_SNAPSHOT = Symbol("akm-cron-binding-snapshot");
41
43
  export function CRON_BACKEND(options = {}) {
42
44
  const exec = options.exec ?? defaultCronExec();
43
45
  const fsLike = options.fs ?? nodeFs();
@@ -48,7 +50,9 @@ export function CRON_BACKEND(options = {}) {
48
50
  const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext, envPath ?? ""));
49
51
  return {
50
52
  name: "cron",
51
- install(task, opts) {
53
+ install(task, opts, expected) {
54
+ if (expected)
55
+ assertSchedulerExpectationIdentity(expected, task);
52
56
  // Create the log directory before writing the crontab line — cron
53
57
  // appends with `>>` and the surrounding shell will fail the entire
54
58
  // entry if the parent directory doesn't exist.
@@ -56,77 +60,201 @@ export function CRON_BACKEND(options = {}) {
56
60
  assertPortableCronLine(cronLine);
57
61
  fsLike.ensureDir(logDir);
58
62
  const existing = readCrontab(exec);
59
- const block = renderBlock(task.id, cronLine, task.enabled);
60
- const next = upsertBlock(existing, task.id, block);
63
+ const nativeId = schedulerBindingNativeId(task);
64
+ const blocks = listBlocks(existing);
65
+ const matching = blocks.filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
66
+ const artifacts = matching.map((block) => cronArtifact(block.id, block.body));
67
+ const priorArtifact = expected
68
+ ? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, expected.state === "absent" ? 0 : 1)
69
+ : matching.length > 1
70
+ ? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, 1)
71
+ : artifacts[0];
72
+ const prior = priorArtifact ? matching.find((block) => block.id === priorArtifact.nativeId) : undefined;
73
+ if (expected) {
74
+ assertSchedulerMutationArtifact(priorArtifact, expected);
75
+ }
76
+ else if (prior) {
77
+ assertSchedulerNativeArtifactOwner(prior.id, task, extractCronInvocation(prior.body)?.invocation);
78
+ }
79
+ const block = renderBlock(nativeId, cronLine, task.enabled, task.executionEvidenceDigest);
80
+ const next = upsertBlock(existing, nativeId, block);
61
81
  replaceCrontab(exec, existing, next);
62
82
  },
63
- uninstall(id) {
83
+ uninstall(nativeId, expected) {
84
+ if (expected)
85
+ assertSchedulerExpectationIdentity({ ...expected, state: "present" });
64
86
  const existing = readCrontab(exec);
65
- const next = removeBlock(existing, id);
87
+ const matching = listBlocks(existing).filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
88
+ const priorArtifact = expected
89
+ ? assertSchedulerNativeArtifactCardinality(matching.map((block) => cronArtifact(block.id, block.body)), nativeId, 1)
90
+ : undefined;
91
+ const prior = expected
92
+ ? matching.find((block) => block.id === priorArtifact?.nativeId)
93
+ : matching.find(({ id }) => id === nativeId);
94
+ if (!prior)
95
+ return;
96
+ if (expected) {
97
+ assertSchedulerRemovalArtifact(nativeId, expected, extractCronInvocation(prior.body)?.invocation, normalizeSignature(prior.body));
98
+ }
99
+ const next = removeBlock(existing, nativeId);
66
100
  replaceCrontab(exec, existing, next);
67
101
  },
68
- setEnabled(id, enabled) {
102
+ setEnabled(nativeId, enabled) {
69
103
  const existing = readCrontab(exec);
70
- const next = toggleBlock(existing, id, enabled);
104
+ const next = toggleBlock(existing, nativeId, enabled);
71
105
  replaceCrontab(exec, existing, next);
72
106
  },
73
107
  list() {
74
- const existing = readCrontab(exec);
75
- const refs = [];
76
- for (const { id, body } of listBlocks(existing)) {
77
- const installed = extractCronInvocation(body);
78
- // An invocation that no longer parses (e.g. a pre-0.9 `tasks run`
79
- // entry surviving the scheduler-ABI respelling, or any other
80
- // foreign/malformed line between our markers) is an orphan of its
81
- // marker id, not a hard failure: omit it here so `akmTasksSync`
82
- // treats the id as "not present" and reinstalls it from the current
83
- // task file — going through the normal eligibility/`--rebind` gate
84
- // exactly like a fresh install.
85
- if (!installed)
86
- continue;
87
- refs.push({
88
- id,
89
- signature: normalizeSignature(body),
90
- ...(installed.target !== undefined ? { target: installed.target } : {}),
91
- binding: installed.binding,
92
- contextPath: installed.contextPath,
93
- });
94
- }
95
- return refs;
108
+ return [...inspectCronState(readCrontab(exec)).installed];
96
109
  },
97
110
  listForRebind() {
98
111
  const existing = readCrontab(exec);
99
112
  return listBlocks(existing).map(({ id, body }) => {
100
113
  const installed = extractCronInvocation(body);
101
- return {
102
- id,
114
+ const ref = {
115
+ id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
103
116
  signature: normalizeSignature(body),
104
117
  ...(installed?.target !== undefined ? { target: installed.target } : {}),
105
118
  };
119
+ Object.defineProperty(ref, "nativeId", { value: id });
120
+ if (installed)
121
+ Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
122
+ return ref;
106
123
  });
107
124
  },
125
+ listNativeArtifacts() {
126
+ return [...inspectCronState(readCrontab(exec)).artifacts];
127
+ },
128
+ inspectBindings() {
129
+ return inspectCronState(readCrontab(exec));
130
+ },
131
+ snapshotBindings(ids) {
132
+ const crontab = readCrontab(exec);
133
+ const inspection = inspectCronState(crontab);
134
+ const keys = new Set(ids.map(schedulerNativeArtifactKey));
135
+ return Object.freeze({
136
+ kind: CRON_SNAPSHOT,
137
+ nativeIds: Object.freeze([...ids]),
138
+ artifacts: Object.freeze(inspection.artifacts.filter((artifact) => keys.has(schedulerNativeArtifactKey(artifact.nativeId)))),
139
+ crontab,
140
+ });
141
+ },
142
+ restoreBindings(snapshot, expectedCurrent) {
143
+ if (!isCronBindingSnapshot(snapshot)) {
144
+ throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
145
+ }
146
+ const existing = readCrontab(exec);
147
+ const current = inspectCronState(existing);
148
+ const safeNativeIds = [];
149
+ const errors = [];
150
+ if (expectedCurrent) {
151
+ for (const nativeId of snapshot.nativeIds) {
152
+ try {
153
+ const expected = expectedCurrent.find((candidate) => candidate.nativeId === nativeId);
154
+ if (!expected) {
155
+ throw new ConfigError(`Missing cron rollback expectation for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
156
+ }
157
+ assertSchedulerRollbackArtifactCardinality(current.artifacts, expected);
158
+ safeNativeIds.push(nativeId);
159
+ }
160
+ catch (error) {
161
+ errors.push(error);
162
+ }
163
+ }
164
+ }
165
+ else {
166
+ safeNativeIds.push(...snapshot.nativeIds);
167
+ }
168
+ const priorBlocks = new Map(listBlocks(snapshot.crontab).map((block) => [block.id, block]));
169
+ let restored = existing;
170
+ for (const nativeId of safeNativeIds) {
171
+ const prior = priorBlocks.get(nativeId);
172
+ restored = prior
173
+ ? upsertBlock(restored, nativeId, [BEGIN(nativeId), prior.body, END(nativeId)].join("\n"))
174
+ : removeBlock(restored, nativeId);
175
+ }
176
+ if (restored !== existing) {
177
+ try {
178
+ replaceCrontab(exec, existing, restored);
179
+ }
180
+ catch (error) {
181
+ errors.push(error);
182
+ }
183
+ }
184
+ if (errors.length > 0) {
185
+ throw new AggregateError(errors, "Cron rollback CAS rejected one or more changed native artifacts.");
186
+ }
187
+ },
108
188
  expectedSignature(task, opts) {
109
189
  const cronLine = buildCronLine(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
110
190
  assertPortableCronLine(cronLine);
111
- return normalizeSignature(cronBlockBody(cronLine, task.enabled));
191
+ return normalizeSignature(cronBlockBody(cronLine, task.enabled, task.executionEvidenceDigest));
112
192
  },
113
193
  };
114
194
  }
195
+ function inspectCronState(crontab) {
196
+ const installed = [];
197
+ const artifacts = [];
198
+ for (const { id, body } of listBlocks(crontab)) {
199
+ const parsed = extractCronInvocation(body);
200
+ const fingerprint = normalizeSignature(body);
201
+ const artifact = cronArtifact(id, body);
202
+ artifacts.push(artifact);
203
+ if (!parsed)
204
+ continue;
205
+ const ref = {
206
+ id: schedulerLogicalBindingId(id, parsed.invocation),
207
+ signature: fingerprint,
208
+ ...(parsed.target !== undefined ? { target: parsed.target } : {}),
209
+ binding: parsed.binding,
210
+ contextPath: parsed.contextPath,
211
+ };
212
+ Object.defineProperty(ref, "nativeId", { value: id });
213
+ Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
214
+ installed.push(ref);
215
+ }
216
+ return Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) });
217
+ }
218
+ function cronArtifact(nativeId, body) {
219
+ const parsed = extractCronInvocation(body);
220
+ const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
221
+ const artifact = parsed
222
+ ? {
223
+ nativeId,
224
+ ...(owner !== undefined ? { bindingId: owner } : {}),
225
+ invocation: Object.freeze([...parsed.invocation]),
226
+ }
227
+ : { nativeId };
228
+ Object.defineProperty(artifact, "fingerprint", { value: normalizeSignature(body) });
229
+ return artifact;
230
+ }
231
+ function isCronBindingSnapshot(value) {
232
+ return (typeof value === "object" &&
233
+ value !== null &&
234
+ value.kind === CRON_SNAPSHOT &&
235
+ typeof value.crontab === "string");
236
+ }
115
237
  // ── helpers (exported for tests) ────────────────────────────────────────────
116
- export function buildCronLine(task, akmArgv, logDir, contextPath, target) {
117
- const spec = parseSchedule(task.schedule, "cron");
238
+ export function buildCronLine(task, akmArgv, logDir, contextPath, _target) {
239
+ const spec = parseSchedule(task.cron, "cron");
118
240
  const cronExpr = translateToCron(spec);
119
- const logPath = path.join(logDir, `${task.id}.log`);
120
- const invocation = buildScheduledTaskInvocation(akmArgv, task.id, contextPath, target);
241
+ const logPath = path.join(logDir, `${schedulerBindingNativeId(task)}.log`);
242
+ const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
121
243
  const cmd = invocation.argv.map((part) => quoteForCron(part)).join(" ");
122
244
  return `${cronExpr} ${cmd} >> ${quoteForCron(logPath)} 2>&1`;
123
245
  }
124
246
  /** The crontab line as it appears inside a block — commented when disabled. */
125
- export function cronBlockBody(cronLine, enabled) {
126
- return enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
247
+ export function cronBlockBody(cronLine, enabled, executionEvidenceDigest) {
248
+ const lines = [
249
+ cronLine,
250
+ ...(executionEvidenceDigest === undefined
251
+ ? []
252
+ : [`# akm:workflow-evidence ${assertSchedulerExecutionEvidenceDigest(executionEvidenceDigest)}`]),
253
+ ];
254
+ return (enabled ? lines : lines.map((line) => `${DISABLED_PREFIX}${line}`)).join("\n");
127
255
  }
128
- export function renderBlock(id, cronLine, enabled) {
129
- return [BEGIN(id), cronBlockBody(cronLine, enabled), END(id)].join("\n");
256
+ export function renderBlock(id, cronLine, enabled, executionEvidenceDigest) {
257
+ return [BEGIN(id), cronBlockBody(cronLine, enabled, executionEvidenceDigest), END(id)].join("\n");
130
258
  }
131
259
  /**
132
260
  * Parse the akm-owned blocks out of a crontab, returning each task id with the
@@ -195,7 +323,7 @@ export function extractCronInvocation(body) {
195
323
  const redirectIndex = fields.indexOf(">>", commandStart);
196
324
  if (redirectIndex === -1)
197
325
  return undefined;
198
- return parseScheduledTaskArgv(fields.slice(commandStart, redirectIndex));
326
+ return parseScheduledBindingArgv(fields.slice(commandStart, redirectIndex));
199
327
  }
200
328
  /** Reverse {@link quoteForCron} for a single whitespace-free token. */
201
329
  function splitCronShellWords(value) {
@@ -5,7 +5,7 @@ import { CRON_BACKEND } from "./cron.js";
5
5
  import { LAUNCHD_BACKEND } from "./launchd.js";
6
6
  import { SCHTASKS_BACKEND } from "./schtasks.js";
7
7
  // WI-9.10e: the former `_setBackendsForTests` module-mutation seam was retired.
8
- // Tests inject a fake `TaskBackend` directly via the `deps.backend` parameter
8
+ // Tests inject a fake `SchedulerBackend` directly via the `deps.backend` parameter
9
9
  // the `akm task` mutation entries already accept (the backend carries its own
10
10
  // `name`), so no module-level override binding is needed. `selectBackend`'s
11
11
  // `options.platform` covers the platform-steering the seam's second override