akm-cli 0.9.1 → 0.9.2-alpha.1

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 (341) hide show
  1. package/CHANGELOG.md +88 -0
  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/tasks/core/extract.yml +6 -5
  12. package/dist/assets/tasks/core/improve.yml +6 -5
  13. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  14. package/dist/assets/tasks/core/sync.yml +6 -5
  15. package/dist/assets/tasks/core/version-check.yml +6 -5
  16. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  17. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  21. package/dist/cli/confirm.js +2 -2
  22. package/dist/cli/parse-args.js +3 -24
  23. package/dist/cli/retired-commands.js +1 -1
  24. package/dist/cli/shared.js +2 -2
  25. package/dist/cli.js +11 -9
  26. package/dist/commands/agent/agent-dispatch.js +55 -89
  27. package/dist/commands/agent/contribute-cli.js +12 -45
  28. package/dist/commands/command/builtin-action.js +32 -0
  29. package/dist/commands/command/command-cli.js +99 -0
  30. package/dist/commands/command/command-execution.js +308 -0
  31. package/dist/commands/command/execution-source-loader.js +176 -0
  32. package/dist/commands/command/portable-template.js +60 -0
  33. package/dist/commands/config-cli.js +10 -4
  34. package/dist/commands/env/env.js +4 -2
  35. package/dist/commands/feedback-cli.js +1 -1
  36. package/dist/commands/health/checks.js +241 -29
  37. package/dist/commands/health/html-report.js +0 -14
  38. package/dist/commands/health/report-view-model.js +0 -1
  39. package/dist/commands/health/surfaces.js +6 -7
  40. package/dist/commands/health/types.js +0 -2
  41. package/dist/commands/health.js +63 -18
  42. package/dist/commands/improve/collapse-detector.js +2 -2
  43. package/dist/commands/improve/consolidate.js +251 -214
  44. package/dist/commands/improve/distill/promote-memory.js +71 -34
  45. package/dist/commands/improve/distill/quality-gate.js +17 -5
  46. package/dist/commands/improve/distill.js +232 -155
  47. package/dist/commands/improve/eligibility.js +112 -79
  48. package/dist/commands/improve/execution.js +57 -0
  49. package/dist/commands/improve/extract-cli.js +5 -5
  50. package/dist/commands/improve/extract.js +521 -342
  51. package/dist/commands/improve/improve-strategies.js +43 -14
  52. package/dist/commands/improve/improve.js +249 -29
  53. package/dist/commands/improve/loop-stages.js +11 -17
  54. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  55. package/dist/commands/improve/outcome-loop.js +22 -38
  56. package/dist/commands/improve/planner.js +134 -0
  57. package/dist/commands/improve/preparation.js +730 -409
  58. package/dist/commands/improve/reflect.js +386 -223
  59. package/dist/commands/improve/run-context.js +3 -4
  60. package/dist/commands/improve/salience.js +6 -58
  61. package/dist/commands/improve/session-asset.js +12 -12
  62. package/dist/commands/lint/index.js +101 -29
  63. package/dist/commands/migrate-cli.js +11 -69
  64. package/dist/commands/migration-tool.js +6 -9
  65. package/dist/commands/models-cli.js +27 -0
  66. package/dist/commands/proposal/drain.js +258 -186
  67. package/dist/commands/proposal/proposal-cli.js +32 -10
  68. package/dist/commands/proposal/proposal.js +2 -5
  69. package/dist/commands/proposal/propose.js +192 -172
  70. package/dist/commands/proposal/repository.js +54 -91
  71. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  72. package/dist/commands/read/curate.js +10 -0
  73. package/dist/commands/read/registry-search.js +25 -9
  74. package/dist/commands/read/remember-cli.js +14 -2
  75. package/dist/commands/read/search.js +10 -4
  76. package/dist/commands/read/show.js +139 -153
  77. package/dist/commands/registry-cli.js +16 -7
  78. package/dist/commands/remember.js +33 -18
  79. package/dist/commands/sources/add-cli.js +19 -178
  80. package/dist/commands/sources/bundle-cli.js +15 -3
  81. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  82. package/dist/commands/sources/info.js +2 -1
  83. package/dist/commands/sources/installed-stashes.js +835 -152
  84. package/dist/commands/sources/schema-repair.js +174 -95
  85. package/dist/commands/sources/self-update.js +30 -74
  86. package/dist/commands/sources/source-add.js +3 -5
  87. package/dist/commands/sources/sources-cli.js +2 -15
  88. package/dist/commands/sources/update-transaction.js +220 -0
  89. package/dist/commands/tasks/tasks-cli.js +3 -3
  90. package/dist/commands/tasks/tasks.js +736 -317
  91. package/dist/commands/workflow-cli.js +2 -2
  92. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  93. package/dist/core/adapter/adapters/akm-adapter.js +83 -34
  94. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  95. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  96. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  97. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  98. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  99. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  100. package/dist/core/adapter/adapters/index.js +0 -9
  101. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  102. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  103. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  104. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  105. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  106. package/dist/core/adapter/execution-source.js +308 -0
  107. package/dist/core/adapter/recognize-match.js +36 -13
  108. package/dist/core/adapter/registry.js +0 -9
  109. package/dist/core/asset/stash-meta.js +94 -4
  110. package/dist/core/common.js +6 -11
  111. package/dist/core/config/config-io.js +3 -3
  112. package/dist/core/config/config-schema.js +18 -40
  113. package/dist/core/config/config-sources.js +11 -21
  114. package/dist/core/config/config-walker.js +31 -13
  115. package/dist/core/config/config.js +21 -20
  116. package/dist/core/config/schema/engines.js +8 -7
  117. package/dist/core/config/schema/improve-processes.js +29 -5
  118. package/dist/core/config/schema/primitives.js +1 -23
  119. package/dist/core/config/schema/sources-bundles.js +13 -16
  120. package/dist/core/errors.js +2 -0
  121. package/dist/core/events.js +68 -32
  122. package/dist/core/extra-params.js +1 -0
  123. package/dist/core/improve-result.js +315 -0
  124. package/dist/core/lesson-lint.js +0 -6
  125. package/dist/core/maintenance-barrier.js +4 -4
  126. package/dist/core/network-policy.js +152 -0
  127. package/dist/core/paths.js +1 -1
  128. package/dist/core/recognition-util.js +4 -4
  129. package/dist/core/registry-url.js +456 -0
  130. package/dist/core/state/migrations.js +161 -47
  131. package/dist/core/state-db.js +453 -80
  132. package/dist/core/system-error.js +32 -0
  133. package/dist/core/time.js +2 -12
  134. package/dist/core/write-source.js +0 -18
  135. package/dist/execution/directory-identity.js +52 -0
  136. package/dist/execution/executable-identity.js +107 -0
  137. package/dist/execution/guarded-source.js +398 -0
  138. package/dist/execution/json.js +95 -0
  139. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  140. package/dist/execution/record.js +55 -0
  141. package/dist/execution/resolved-request.js +730 -0
  142. package/dist/execution/source.js +320 -0
  143. package/dist/indexer/bundle-identity-guard.js +5 -4
  144. package/dist/indexer/db/graph-db.js +33 -0
  145. package/dist/indexer/graph/graph-boost.js +3 -4
  146. package/dist/indexer/graph/graph-extraction.js +562 -373
  147. package/dist/indexer/index-written-assets.js +65 -34
  148. package/dist/indexer/indexer.js +443 -222
  149. package/dist/indexer/installations.js +6 -0
  150. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  151. package/dist/indexer/passes/memory-inference.js +227 -174
  152. package/dist/indexer/scan/doc-to-entry.js +7 -9
  153. package/dist/indexer/scan/drain-dir.js +51 -23
  154. package/dist/indexer/search/db-search.js +98 -22
  155. package/dist/indexer/search/search-fields.js +1 -1
  156. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  157. package/dist/indexer/search/search-source.js +7 -16
  158. package/dist/indexer/search/semantic-status.js +10 -1
  159. package/dist/indexer/usage/show-usage.js +105 -0
  160. package/dist/indexer/usage/usage-events.js +7 -2
  161. package/dist/indexer/walk/matchers.js +40 -10
  162. package/dist/indexer/walk/path-resolver.js +5 -2
  163. package/dist/indexer/walk/walker.js +20 -2
  164. package/dist/integrations/agent/builder-shared.js +3 -6
  165. package/dist/integrations/agent/conversation-fallback.js +16 -0
  166. package/dist/integrations/agent/engine-resolution.js +87 -87
  167. package/dist/integrations/agent/execution-cascade.js +566 -0
  168. package/dist/integrations/agent/execution-definitions.js +211 -0
  169. package/dist/integrations/agent/execution-lowering.js +811 -0
  170. package/dist/integrations/agent/execution-preparation.js +67 -0
  171. package/dist/integrations/agent/index.js +0 -2
  172. package/dist/integrations/agent/inline-execution.js +74 -0
  173. package/dist/integrations/agent/model-map.js +515 -0
  174. package/dist/integrations/agent/persona-fallback.js +30 -0
  175. package/dist/integrations/agent/request-lowering.js +186 -0
  176. package/dist/integrations/agent/runner-dispatch.js +230 -37
  177. package/dist/integrations/agent/runner.js +12 -83
  178. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  179. package/dist/integrations/harnesses/aider/index.js +0 -1
  180. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  181. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  182. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  183. package/dist/integrations/harnesses/claude/index.js +1 -5
  184. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  185. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  186. package/dist/integrations/harnesses/codex/index.js +0 -1
  187. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/copilot/index.js +0 -1
  189. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/gemini/index.js +0 -1
  191. package/dist/integrations/harnesses/index.js +4 -44
  192. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  193. package/dist/integrations/harnesses/opencode/index.js +0 -2
  194. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  195. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  196. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  197. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  198. package/dist/integrations/harnesses/openhands/index.js +0 -1
  199. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  200. package/dist/integrations/harnesses/pi/index.js +0 -1
  201. package/dist/integrations/harnesses/shared.js +0 -1
  202. package/dist/integrations/harnesses/types.js +1 -3
  203. package/dist/integrations/lockfile.js +82 -79
  204. package/dist/integrations/session-logs/index.js +6 -17
  205. package/dist/integrations/session-logs/provider-base.js +1 -29
  206. package/dist/llm/client.js +10 -5
  207. package/dist/llm/embedder.js +6 -7
  208. package/dist/llm/embedders/local.js +37 -88
  209. package/dist/llm/embedders/types.js +1 -1
  210. package/dist/llm/graph-extract.js +75 -50
  211. package/dist/llm/index-passes.js +43 -5
  212. package/dist/llm/memory-infer.js +8 -6
  213. package/dist/llm/metadata-enhance.js +5 -3
  214. package/dist/llm/structured-call.js +122 -25
  215. package/dist/output/format-exempt.js +1 -1
  216. package/dist/output/render-registry.js +0 -16
  217. package/dist/output/renderers.js +12 -7
  218. package/dist/output/shapes/curate.js +1 -0
  219. package/dist/output/shapes/helpers.js +10 -2
  220. package/dist/output/shapes/passthrough.js +2 -0
  221. package/dist/output/text/command-format.js +28 -32
  222. package/dist/output/text/health-format.js +1 -29
  223. package/dist/output/text/migrate.js +6 -56
  224. package/dist/output/text/proposal-format.js +16 -1
  225. package/dist/output/text/workflow-format.js +16 -0
  226. package/dist/registry/network.js +279 -0
  227. package/dist/registry/pinned-request-helper.js +247 -0
  228. package/dist/registry/pinned-transport.js +717 -0
  229. package/dist/registry/providers/skills-sh.js +18 -6
  230. package/dist/registry/providers/static-index.js +20 -7
  231. package/dist/registry/resolve.js +53 -28
  232. package/dist/scripts/akm-migrate-node.js +19122 -52218
  233. package/dist/scripts/akm-migrate.js +19064 -51567
  234. package/dist/setup/registry-stash-loader.js +64 -20
  235. package/dist/setup/semantic-assets.js +9 -34
  236. package/dist/setup/setup.js +12 -30
  237. package/dist/setup/source-identity.js +17 -0
  238. package/dist/setup/steps/sources.js +36 -15
  239. package/dist/setup/steps/tasks.js +39 -11
  240. package/dist/sources/providers/git-provider.js +3 -3
  241. package/dist/sources/providers/npm.js +2 -2
  242. package/dist/sources/providers/provider-utils.js +4 -3
  243. package/dist/sources/providers/website.js +11 -7
  244. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  245. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  246. package/dist/sources/website-url.js +73 -0
  247. package/dist/storage/engines/sqlite-migrations.js +81 -26
  248. package/dist/storage/managed-db.js +27 -24
  249. package/dist/storage/repositories/events-repository.js +3 -0
  250. package/dist/storage/repositories/index-connection.js +42 -10
  251. package/dist/storage/repositories/index-entries-repository.js +167 -207
  252. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  253. package/dist/storage/repositories/index-entry-schema.js +255 -0
  254. package/dist/storage/repositories/index-fts-repository.js +10 -10
  255. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  256. package/dist/storage/repositories/index-meta-repository.js +0 -11
  257. package/dist/storage/repositories/index-schema.js +71 -342
  258. package/dist/storage/repositories/index-utility-repository.js +12 -17
  259. package/dist/storage/repositories/index-vec-repository.js +1 -1
  260. package/dist/storage/repositories/proposals-repository.js +4 -127
  261. package/dist/storage/repositories/registry-cache.js +2 -1
  262. package/dist/storage/repositories/task-history-repository.js +20 -40
  263. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  264. package/dist/storage/sqlite-read-snapshot.js +148 -0
  265. package/dist/tasks/backends/cron.js +170 -42
  266. package/dist/tasks/backends/index.js +1 -1
  267. package/dist/tasks/backends/launchd.js +787 -202
  268. package/dist/tasks/backends/schtasks.js +282 -83
  269. package/dist/tasks/embedded.js +7 -7
  270. package/dist/tasks/frozen-script.js +50 -0
  271. package/dist/tasks/resolve-akm-bin.js +5 -1
  272. package/dist/tasks/runner.js +239 -251
  273. package/dist/tasks/runtime-v3.js +281 -0
  274. package/dist/tasks/scheduler-binding.js +272 -0
  275. package/dist/tasks/scheduler-invocation.js +57 -43
  276. package/dist/tasks/scheduler-sync.js +654 -0
  277. package/dist/tasks/source-v3.js +752 -0
  278. package/dist/tasks/standalone-script-entry.js +5 -0
  279. package/dist/tasks/task-id.js +29 -0
  280. package/dist/workflows/authoring/authoring.js +15 -32
  281. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  282. package/dist/workflows/exec/exec-unit.js +7 -28
  283. package/dist/workflows/exec/frozen-judge.js +57 -89
  284. package/dist/workflows/exec/lowering-notices.js +23 -0
  285. package/dist/workflows/exec/native-executor.js +301 -458
  286. package/dist/workflows/exec/param-secrets.js +4 -3
  287. package/dist/workflows/exec/run-workflow.js +26 -32
  288. package/dist/workflows/exec/step-work.js +105 -109
  289. package/dist/workflows/exec/unit-dispatch.js +103 -27
  290. package/dist/workflows/exec/unit-writer.js +3 -3
  291. package/dist/workflows/exec/worktree.js +2 -2
  292. package/dist/workflows/ir/compile.js +86 -72
  293. package/dist/workflows/ir/environment-v4.js +328 -0
  294. package/dist/workflows/ir/freeze-v4.js +122 -0
  295. package/dist/workflows/ir/plan-hash.js +13 -7
  296. package/dist/workflows/ir/schema-v4.js +525 -0
  297. package/dist/workflows/ir/schema.js +25 -284
  298. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  299. package/dist/workflows/parser.js +27 -24
  300. package/dist/workflows/program/schema.js +1 -2
  301. package/dist/workflows/renderer.js +42 -29
  302. package/dist/workflows/resource-limits.js +4 -5
  303. package/dist/workflows/runtime/agent-identity.js +11 -13
  304. package/dist/workflows/runtime/plan-classifier.js +8 -8
  305. package/dist/workflows/runtime/runs.js +27 -43
  306. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  307. package/dist/workflows/source-files.js +373 -0
  308. package/dist/workflows/source-ir/compile.js +196 -0
  309. package/dist/workflows/source-ir/github-yaml.js +577 -0
  310. package/dist/workflows/source-ir/ordering.js +38 -0
  311. package/dist/workflows/source-ir/program.js +50 -0
  312. package/dist/workflows/source-ir/result.js +26 -0
  313. package/dist/workflows/source-ir/schema.js +772 -0
  314. package/dist/workflows/source-ir/semantics.js +242 -0
  315. package/dist/workflows/source-ir/uses.js +14 -0
  316. package/docs/README.md +2 -0
  317. package/docs/migration/README.md +3 -1
  318. package/docs/migration/release-notes/0.9.2.md +55 -0
  319. package/docs/migration/release-notes/README.md +5 -0
  320. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  321. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  322. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  323. package/docs/reference/README.md +1 -0
  324. package/docs/reference/cli.md +230 -98
  325. package/docs/reference/configuration.md +153 -27
  326. package/docs/reference/data-and-telemetry.md +19 -1
  327. package/docs/reference/supported-formats.md +23 -3
  328. package/docs/reference/tasks.md +182 -0
  329. package/docs/reference/workflow-schema.md +91 -40
  330. package/docs/reference/workflows.md +33 -6
  331. package/package.json +10 -6
  332. package/schemas/akm-config.json +372 -216
  333. package/schemas/akm-task.json +324 -80
  334. package/schemas/akm-workflow.json +6 -9
  335. package/dist/core/migration-operation.js +0 -75
  336. package/dist/integrations/agent/model-aliases.js +0 -74
  337. package/dist/tasks/parser.js +0 -380
  338. package/dist/tasks/schema.js +0 -123
  339. package/dist/tasks/validator.js +0 -80
  340. package/dist/workflows/ir/freeze.js +0 -320
  341. 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