akm-cli 0.9.16 → 0.9.17-alpha.10

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 (403) hide show
  1. package/CHANGELOG.md +2101 -0
  2. package/STABILITY.md +11 -10
  3. package/dist/akm +124 -193
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/prompts/consolidate-system.md +4 -11
  15. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  17. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  18. package/dist/assets/templates/html/health.html +3 -5
  19. package/dist/cli/retired-commands.js +1 -1
  20. package/dist/cli/shared.js +6 -2
  21. package/dist/cli/unknown-flags.js +24 -1
  22. package/dist/cli.js +68 -10
  23. package/dist/commands/agent/agent-dispatch.js +1 -1
  24. package/dist/commands/command/command-execution.js +24 -62
  25. package/dist/commands/feedback-cli.js +0 -1
  26. package/dist/commands/health/accept-rate.js +6 -0
  27. package/dist/commands/health/archive-usage.js +92 -0
  28. package/dist/commands/health/checks.js +83 -74
  29. package/dist/commands/health/config-skew.js +38 -0
  30. package/dist/commands/health/data-dir-usage.js +25 -13
  31. package/dist/commands/health/egress.js +54 -0
  32. package/dist/commands/health/html-report.js +1 -42
  33. package/dist/commands/health/improve-metrics.js +136 -591
  34. package/dist/commands/health/md-report.js +1 -6
  35. package/dist/commands/health/plugin-staleness.js +53 -3
  36. package/dist/commands/health/renderers.js +12 -4
  37. package/dist/commands/health/report-view-model.js +14 -120
  38. package/dist/commands/health/types-improve.js +4 -19
  39. package/dist/commands/health/windows.js +64 -74
  40. package/dist/commands/health.js +145 -143
  41. package/dist/commands/improve/consolidate/chunking.js +26 -117
  42. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  43. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  44. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  45. package/dist/commands/improve/consolidate.js +589 -1127
  46. package/dist/commands/improve/content-hash.js +16 -24
  47. package/dist/commands/improve/distill/content-repair.js +18 -100
  48. package/dist/commands/improve/distill-guards.js +20 -81
  49. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  50. package/dist/commands/improve/distill.js +608 -1041
  51. package/dist/commands/improve/eligibility.js +126 -390
  52. package/dist/commands/improve/execution.js +8 -10
  53. package/dist/commands/improve/extract-prompt.js +1 -2
  54. package/dist/commands/improve/extract.js +487 -1046
  55. package/dist/commands/improve/feedback-valence.js +0 -25
  56. package/dist/commands/improve/improve-cli.js +75 -169
  57. package/dist/commands/improve/improve-result-file.js +10 -66
  58. package/dist/commands/improve/improve-strategies.js +52 -4
  59. package/dist/commands/improve/improve-usage-report.js +18 -64
  60. package/dist/commands/improve/improve.js +480 -1074
  61. package/dist/commands/improve/ledger.js +119 -0
  62. package/dist/commands/improve/locks.js +2 -8
  63. package/dist/commands/improve/loop-stages.js +415 -1073
  64. package/dist/commands/improve/memory/derived-ref.js +12 -77
  65. package/dist/commands/improve/memory/memory-belief.js +16 -118
  66. package/dist/commands/improve/memory/memory-improve.js +266 -14
  67. package/dist/commands/improve/outcome-loop.js +28 -156
  68. package/dist/commands/improve/planner.js +5 -15
  69. package/dist/commands/improve/preparation.js +779 -2319
  70. package/dist/commands/improve/proactive-maintenance.js +34 -101
  71. package/dist/commands/improve/reflect-noise.js +104 -280
  72. package/dist/commands/improve/reflect.js +642 -1353
  73. package/dist/commands/improve/retrieval-gate.js +127 -0
  74. package/dist/commands/improve/retrieval-scope.js +92 -0
  75. package/dist/commands/improve/salience.js +41 -240
  76. package/dist/commands/improve/session-asset.js +19 -100
  77. package/dist/commands/improve/stage.js +322 -0
  78. package/dist/commands/lint/base-linter.js +37 -15
  79. package/dist/commands/proposal/drain.js +261 -578
  80. package/dist/commands/proposal/proposal-cli.js +19 -20
  81. package/dist/commands/proposal/proposal-types.js +31 -24
  82. package/dist/commands/proposal/proposal.js +38 -8
  83. package/dist/commands/proposal/propose.js +134 -160
  84. package/dist/commands/proposal/repository.js +1097 -1394
  85. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  86. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  87. package/dist/commands/proposal/validators/proposals.js +22 -89
  88. package/dist/commands/read/curate.js +105 -462
  89. package/dist/commands/read/knowledge.js +3 -2
  90. package/dist/commands/read/search-cli.js +16 -33
  91. package/dist/commands/read/search.js +17 -23
  92. package/dist/commands/read/show.js +57 -108
  93. package/dist/commands/sources/bundle-cli.js +25 -2
  94. package/dist/commands/sources/bundle-config-ops.js +4 -0
  95. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  96. package/dist/commands/sources/info.js +127 -29
  97. package/dist/commands/sources/installed-stashes.js +197 -746
  98. package/dist/commands/sources/schema-repair.js +98 -129
  99. package/dist/commands/sources/source-add.js +62 -12
  100. package/dist/commands/sources/source-manage.js +9 -2
  101. package/dist/commands/sources/stash-cli.js +24 -4
  102. package/dist/commands/tasks/explain.js +10 -13
  103. package/dist/commands/tasks/tasks-cli.js +12 -13
  104. package/dist/commands/tasks/tasks.js +350 -936
  105. package/dist/commands/tasks/validate.js +26 -24
  106. package/dist/commands/workflow/plan.js +22 -29
  107. package/dist/commands/workflow-cli.js +4 -4
  108. package/dist/core/adapter/adapters/akm-adapter.js +2 -1
  109. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  110. package/dist/core/adapter/adapters/akm-metadata.js +42 -12
  111. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  112. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  113. package/dist/core/adapter/execution-source.js +17 -29
  114. package/dist/core/asset/asset-placement.js +4 -13
  115. package/dist/core/asset/frontmatter.js +106 -1
  116. package/dist/core/asset/resolve-ref.js +1 -1
  117. package/dist/core/bundle-id.js +42 -5
  118. package/dist/core/bundle-rename.js +285 -0
  119. package/dist/core/config/config-io.js +1 -2
  120. package/dist/core/config/config-schema.js +9 -34
  121. package/dist/core/config/config-walker.js +1 -1
  122. package/dist/core/config/config.js +184 -111
  123. package/dist/core/config/engine-semantics.js +0 -2
  124. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  125. package/dist/core/config/schema/embedding.js +20 -5
  126. package/dist/core/config/schema/engines.js +5 -0
  127. package/dist/core/config/schema/execution.js +1 -1
  128. package/dist/core/config/schema/experimental.js +1 -1
  129. package/dist/core/config/schema/improve-processes.js +54 -125
  130. package/dist/core/config/schema/improve.js +4 -42
  131. package/dist/core/config/schema/index-config.js +9 -48
  132. package/dist/core/config/schema/scheduler.js +12 -12
  133. package/dist/core/config/schema/search.js +6 -22
  134. package/dist/core/env-secret-ref.js +0 -1
  135. package/dist/core/errors.js +8 -9
  136. package/dist/core/file-change.js +13 -5
  137. package/dist/core/file-lock.js +76 -173
  138. package/dist/core/improve-result.js +35 -7
  139. package/dist/core/improve-types.js +0 -1
  140. package/dist/core/logs-db.js +2 -2
  141. package/dist/core/loopback.js +7 -12
  142. package/dist/core/non-task-input.js +20 -0
  143. package/dist/core/parse.js +13 -16
  144. package/dist/core/paths.js +0 -24
  145. package/dist/core/redaction.js +109 -2
  146. package/dist/core/run-lock.js +2 -5
  147. package/dist/core/spawn-env.js +1 -1
  148. package/dist/core/state/migrations.js +123 -61
  149. package/dist/core/state-db-scope.js +2 -4
  150. package/dist/core/state-db.js +126 -692
  151. package/dist/core/time.js +0 -20
  152. package/dist/core/type-presentation.js +1 -9
  153. package/dist/core/write-source.js +294 -1005
  154. package/dist/execution/input-contract.js +1 -1
  155. package/dist/execution/resolved-request.js +135 -689
  156. package/dist/execution/source.js +63 -257
  157. package/dist/execution/target-ref.js +1 -1
  158. package/dist/indexer/bundle-identity-guard.js +2 -2
  159. package/dist/indexer/db/llm-cache.js +2 -2
  160. package/dist/indexer/ensure-index.js +77 -73
  161. package/dist/indexer/index-rebuild-lock.js +3 -11
  162. package/dist/indexer/index-writer-lock.js +8 -17
  163. package/dist/indexer/index-written-assets.js +141 -154
  164. package/dist/indexer/indexer.js +400 -1124
  165. package/dist/indexer/links/declared-links.js +90 -0
  166. package/dist/indexer/materialize-embeddings.js +60 -397
  167. package/dist/indexer/passes/memory-inference.js +96 -90
  168. package/dist/indexer/passes/metadata.js +132 -219
  169. package/dist/indexer/read-preflight.js +0 -7
  170. package/dist/indexer/scan/doc-to-entry.js +2 -3
  171. package/dist/indexer/scan/drain-dir.js +1 -1
  172. package/dist/indexer/search/db-search.js +190 -590
  173. package/dist/indexer/search/fts-query.js +30 -41
  174. package/dist/indexer/search/ranking.js +28 -154
  175. package/dist/indexer/search/search-attribution.js +12 -32
  176. package/dist/indexer/search/search-fields.js +11 -15
  177. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  178. package/dist/indexer/search/search-source.js +1 -4
  179. package/dist/indexer/usage/usage-events.js +36 -7
  180. package/dist/indexer/walk/walker.js +3 -4
  181. package/dist/integrations/agent/engine-fallback.js +23 -40
  182. package/dist/integrations/agent/engine-resolution.js +93 -183
  183. package/dist/integrations/agent/execution.js +507 -0
  184. package/dist/integrations/agent/model-map.js +28 -156
  185. package/dist/integrations/agent/request-lowering.js +66 -141
  186. package/dist/integrations/agent/runner-dispatch.js +143 -321
  187. package/dist/integrations/agent/runner.js +54 -14
  188. package/dist/integrations/lockfile.js +53 -101
  189. package/dist/llm/client.js +18 -6
  190. package/dist/llm/embedders/deterministic.js +2 -3
  191. package/dist/llm/embedders/profile.js +71 -0
  192. package/dist/llm/embedders/remote.js +11 -17
  193. package/dist/llm/feature-gate.js +0 -8
  194. package/dist/llm/index-passes.js +3 -5
  195. package/dist/llm/memory-infer.js +1 -2
  196. package/dist/llm/structured-call.js +5 -24
  197. package/dist/output/generic-render.js +23 -11
  198. package/dist/output/html-render.js +13 -10
  199. package/dist/output/render-registry.js +3 -32
  200. package/dist/output/shapes/helpers.js +25 -38
  201. package/dist/output/shapes/passthrough.js +1 -9
  202. package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
  203. package/dist/output/text/command-format.js +69 -31
  204. package/dist/output/text/helpers.js +1 -1
  205. package/dist/output/text/migrate.js +5 -14
  206. package/dist/output/text/proposal-format.js +48 -3
  207. package/dist/output/text/show-format.js +13 -17
  208. package/dist/output/text/workflow-format.js +0 -32
  209. package/dist/output/text.js +2 -0
  210. package/dist/registry/factory.js +4 -19
  211. package/dist/registry/network.js +66 -220
  212. package/dist/registry/providers/index.js +0 -2
  213. package/dist/registry/providers/skills-sh.js +3 -14
  214. package/dist/registry/providers/static-index.js +24 -26
  215. package/dist/registry/resolve.js +55 -131
  216. package/dist/scripts/akm-migrate-node.js +42948 -92369
  217. package/dist/scripts/akm-migrate.js +42935 -92354
  218. package/dist/setup/registry-stash-loader.js +4 -13
  219. package/dist/setup/semantic-assets.js +3 -44
  220. package/dist/setup/setup.js +1 -1
  221. package/dist/setup/steps/connection.js +5 -6
  222. package/dist/setup/steps/platforms.js +2 -2
  223. package/dist/setup/steps/tasks.js +25 -15
  224. package/dist/sources/provider-factory.js +17 -18
  225. package/dist/sources/providers/filesystem.js +2 -3
  226. package/dist/sources/providers/git-install.js +7 -1
  227. package/dist/sources/providers/git-provider.js +0 -3
  228. package/dist/sources/providers/git-stash.js +83 -21
  229. package/dist/sources/providers/npm.js +2 -4
  230. package/dist/sources/providers/provider-utils.js +5 -10
  231. package/dist/sources/providers/website.js +0 -2
  232. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  233. package/dist/sources/website-url.js +2 -2
  234. package/dist/storage/database.js +9 -35
  235. package/dist/storage/repositories/improve-ledger-repository.js +209 -0
  236. package/dist/storage/repositories/index-connection.js +39 -72
  237. package/dist/storage/repositories/index-entries-repository.js +131 -129
  238. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  239. package/dist/storage/repositories/index-entry-schema.js +101 -268
  240. package/dist/storage/repositories/index-fts-repository.js +86 -256
  241. package/dist/storage/repositories/index-links-repository.js +143 -0
  242. package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
  243. package/dist/storage/repositories/index-meta-repository.js +6 -4
  244. package/dist/storage/repositories/index-schema.js +257 -325
  245. package/dist/storage/repositories/index-utility-repository.js +8 -29
  246. package/dist/storage/repositories/index-vec-repository.js +133 -414
  247. package/dist/storage/repositories/outcome-repository.js +2 -1
  248. package/dist/storage/repositories/proposals-repository.js +104 -1
  249. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  250. package/dist/storage/repositories/salience-repository.js +1 -19
  251. package/dist/storage/repositories/task-history-repository.js +26 -4
  252. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  253. package/dist/storage/sqlite-migrations.js +136 -0
  254. package/dist/storage/sqlite-pragmas.js +11 -9
  255. package/dist/storage/sqlite-transaction.js +170 -0
  256. package/dist/storage/state-db-integrity.js +130 -0
  257. package/dist/tasks/activation-config.js +134 -62
  258. package/dist/tasks/backends/cron.js +191 -302
  259. package/dist/tasks/backends/exec-utils.js +2 -5
  260. package/dist/tasks/backends/launchd.js +141 -748
  261. package/dist/tasks/backends/schtasks.js +119 -623
  262. package/dist/tasks/prepare/prepare-support.js +5 -15
  263. package/dist/tasks/prepare/prepare.js +0 -2
  264. package/dist/tasks/resolve-akm-bin.js +20 -79
  265. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  266. package/dist/tasks/run/load-task.js +1 -1
  267. package/dist/tasks/scheduler-binding.js +20 -238
  268. package/dist/tasks/scheduler-invocation.js +136 -244
  269. package/dist/tasks/scheduler-lock.js +53 -0
  270. package/dist/tasks/scheduler-sync.js +368 -679
  271. package/dist/tasks/source/parse-task-source.js +55 -9
  272. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  273. package/dist/tasks/source/task-to-v4.js +464 -88
  274. package/dist/workflows/authoring/authoring.js +3 -12
  275. package/dist/workflows/compile.js +211 -0
  276. package/dist/workflows/concurrency-policy.js +13 -74
  277. package/dist/workflows/exec/child-invocation.js +3 -17
  278. package/dist/workflows/exec/child-workflow.js +32 -141
  279. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  280. package/dist/workflows/exec/environment.js +98 -0
  281. package/dist/workflows/exec/exec-unit.js +33 -140
  282. package/dist/workflows/exec/frozen-judge.js +7 -59
  283. package/dist/workflows/exec/native-executor.js +82 -341
  284. package/dist/workflows/exec/param-secrets.js +29 -47
  285. package/dist/workflows/exec/run-workflow.js +154 -387
  286. package/dist/workflows/exec/scheduler.js +9 -36
  287. package/dist/workflows/exec/step-work.js +127 -430
  288. package/dist/workflows/exec/unit-dispatch.js +11 -63
  289. package/dist/workflows/exec/unit-writer.js +8 -52
  290. package/dist/workflows/exec/worktree.js +39 -273
  291. package/dist/workflows/freeze/child-output-references.js +4 -15
  292. package/dist/workflows/freeze/environment.js +99 -92
  293. package/dist/workflows/freeze/freeze.js +172 -0
  294. package/dist/workflows/freeze/step-values.js +19 -21
  295. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  296. package/dist/workflows/freeze/targets/command.js +10 -33
  297. package/dist/workflows/freeze/targets/script.js +5 -12
  298. package/dist/workflows/freeze/targets/shell.js +3 -6
  299. package/dist/workflows/freeze/targets/task.js +25 -80
  300. package/dist/workflows/freeze/task-bindings.js +20 -67
  301. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  302. package/dist/workflows/ir/params.js +6 -51
  303. package/dist/workflows/ir/plan-hash.js +2 -34
  304. package/dist/workflows/parser.js +140 -43
  305. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  306. package/dist/workflows/renderer.js +36 -69
  307. package/dist/workflows/resource-limits.js +12 -120
  308. package/dist/workflows/runtime/agent-identity.js +8 -40
  309. package/dist/workflows/runtime/run-outputs.js +3 -6
  310. package/dist/workflows/runtime/run-plan.js +316 -0
  311. package/dist/workflows/runtime/runs.js +48 -200
  312. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  313. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  314. package/dist/workflows/validate-summary.js +2 -7
  315. package/docs/integration/bundling-akm.md +49 -42
  316. package/docs/migration/README.md +1 -0
  317. package/docs/migration/release-notes/0.9.17.md +43 -0
  318. package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
  319. package/docs/reference/cli.md +232 -135
  320. package/docs/reference/configuration.md +71 -57
  321. package/docs/reference/data-and-telemetry.md +20 -21
  322. package/docs/reference/tasks.md +105 -39
  323. package/docs/reference/workflow-schema.md +14 -18
  324. package/docs/reference/workflows.md +6 -9
  325. package/package.json +1 -1
  326. package/schemas/akm-config.json +115 -738
  327. package/schemas/akm-workflow.json +1 -0
  328. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  329. package/dist/assets/prompts/contradiction-judge.md +0 -33
  330. package/dist/assets/prompts/graph-extract-system.md +0 -1
  331. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  332. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  333. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  334. package/dist/commands/health/advisories.js +0 -150
  335. package/dist/commands/health/metrics.js +0 -329
  336. package/dist/commands/health/surfaces.js +0 -102
  337. package/dist/commands/improve/anti-collapse.js +0 -83
  338. package/dist/commands/improve/collapse-detector.js +0 -432
  339. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  340. package/dist/commands/improve/consolidate/merge.js +0 -149
  341. package/dist/commands/improve/distill/promote-memory.js +0 -291
  342. package/dist/commands/improve/distill/quality-gate.js +0 -337
  343. package/dist/commands/improve/eval-cases.js +0 -52
  344. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  345. package/dist/commands/improve/proposal-envelope.js +0 -31
  346. package/dist/commands/improve/run-context.js +0 -123
  347. package/dist/commands/improve/shared.js +0 -31
  348. package/dist/commands/improve/source-identity.js +0 -28
  349. package/dist/commands/improve/triage.js +0 -96
  350. package/dist/commands/proposal/drain-policies.js +0 -151
  351. package/dist/commands/sources/update-transaction.js +0 -220
  352. package/dist/core/action-contributors.js +0 -28
  353. package/dist/core/config/config-version-shim.js +0 -101
  354. package/dist/core/fs-txn.js +0 -405
  355. package/dist/core/lexical-score.js +0 -25
  356. package/dist/core/maintenance-barrier.js +0 -167
  357. package/dist/execution/executable-identity.js +0 -105
  358. package/dist/execution/guarded-source.js +0 -427
  359. package/dist/indexer/db/graph-db.js +0 -444
  360. package/dist/indexer/graph/graph-boost.js +0 -427
  361. package/dist/indexer/graph/graph-dedup.js +0 -95
  362. package/dist/indexer/graph/graph-extraction.js +0 -1108
  363. package/dist/indexer/search/name-match.js +0 -35
  364. package/dist/indexer/search/ranking-contributors.js +0 -515
  365. package/dist/indexer/search/ranking-types.js +0 -4
  366. package/dist/indexer/walk/project-context.js +0 -192
  367. package/dist/integrations/agent/execution-cascade.js +0 -566
  368. package/dist/integrations/agent/execution-definitions.js +0 -202
  369. package/dist/integrations/agent/execution-lowering.js +0 -841
  370. package/dist/integrations/agent/execution-preparation.js +0 -98
  371. package/dist/integrations/agent/inline-execution.js +0 -74
  372. package/dist/llm/graph-extract.js +0 -728
  373. package/dist/llm/metadata-enhance.js +0 -96
  374. package/dist/registry/create-provider-registry.js +0 -29
  375. package/dist/registry/pinned-request-helper.js +0 -247
  376. package/dist/registry/pinned-transport.js +0 -717
  377. package/dist/sources/providers/index.js +0 -14
  378. package/dist/storage/engines/sqlite-migrations.js +0 -271
  379. package/dist/storage/repositories/canaries-repository.js +0 -107
  380. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  381. package/dist/storage/repositories/registry-cache.js +0 -113
  382. package/dist/tasks/scheduler-sync-preview.js +0 -52
  383. package/dist/tasks/source/task-to-v3.js +0 -507
  384. package/dist/workflows/freeze/resolve-steps.js +0 -86
  385. package/dist/workflows/freeze/source-freeze.js +0 -64
  386. package/dist/workflows/ir/compile.js +0 -321
  387. package/dist/workflows/ir/environment-v4.js +0 -330
  388. package/dist/workflows/ir/freeze-v4.js +0 -153
  389. package/dist/workflows/ir/schema-v4.js +0 -745
  390. package/dist/workflows/ir/schema.js +0 -354
  391. package/dist/workflows/program/schema.js +0 -77
  392. package/dist/workflows/runtime/checkin.js +0 -57
  393. package/dist/workflows/runtime/plan-classifier.js +0 -196
  394. package/dist/workflows/runtime/unit-checkin.js +0 -45
  395. package/dist/workflows/runtime/unit-phases.js +0 -20
  396. package/dist/workflows/schema.js +0 -4
  397. package/dist/workflows/source-ir/compile.js +0 -200
  398. package/dist/workflows/source-ir/program.js +0 -50
  399. package/dist/workflows/source-ir/result.js +0 -26
  400. package/dist/workflows/source-ir/schema.js +0 -786
  401. package/dist/workflows/source-ir/triggers.js +0 -79
  402. package/dist/workflows/source-ir/uses.js +0 -40
  403. package/dist/workflows/validator.js +0 -60
@@ -11,12 +11,15 @@
11
11
  *
12
12
  * Platform notes:
13
13
  * • The `bootstrap` / `bootout` / `enable` / `disable` subcommands require
14
- * macOS 10.10 (Yosemite) or newer. On older systems the equivalents
15
- * are `launchctl load -w` / `unload -w`. We only target modern macOS.
14
+ * macOS 10.10 (Yosemite) or newer. We only target modern macOS.
16
15
  * • `gui/<uid>` is the per-user GUI launchd domain — agents in this
17
- * domain only run while the user is logged in (no background runs at
18
- * the loginwindow). Tasks that need to run when the user is logged
19
- * out should be installed as system Daemons, which is out of scope.
16
+ * domain only run while the user is logged in. Tasks that need to run
17
+ * when the user is logged out should be installed as system Daemons,
18
+ * which is out of scope.
19
+ * • launchd strips the environment; the syncing shell's PATH goes into the
20
+ * plist's `EnvironmentVariables` so task bodies find the same binaries,
21
+ * next to the AKM directory environment the row sets (see
22
+ * `src/tasks/scheduler-invocation.ts`).
20
23
  *
21
24
  * Tests inject a fake exec + filesystem so the backend can be unit-tested
22
25
  * without touching the host launchctl.
@@ -31,713 +34,133 @@ import { getTaskLogDir } from "../../core/paths.js";
31
34
  import { warn } from "../../core/warn.js";
32
35
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
33
36
  import { parseSchedule, translateToLaunchd } from "../schedule.js";
34
- import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
35
- import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
37
+ import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
38
+ import { buildScheduledInvocation, parseScheduledInvocationArgv, scheduledRowEnvironmentEntries, scheduledRowEnvironmentFrom, } from "../scheduler-invocation.js";
36
39
  import { escapeXml, nodeExec, nodeFs, runOrThrow } from "./exec-utils.js";
37
40
  export const LAUNCHD_LABEL_PREFIX = "com.akm.task.";
38
- const MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES = 4 * 1024 * 1024;
39
- const MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES = 4096;
40
41
  const LAUNCHD_AKM_LABEL_RE = /^com\.akm\.task\.[A-Za-z0-9._-]{1,1024}$/u;
41
- const LAUNCHD_SNAPSHOT = Symbol("akm-launchd-binding-snapshot");
42
42
  export function LAUNCHD_BACKEND(options = {}) {
43
43
  const exec = options.exec ?? defaultLaunchdExec();
44
44
  const fsLike = options.fs ?? defaultLaunchdFs();
45
45
  const agentsDir = options.agentsDir ?? defaultAgentsDir();
46
46
  const logDir = options.logDir ?? getTaskLogDir();
47
47
  const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
48
- const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
48
+ const envPath = options.envPath === false ? undefined : typeof options.envPath === "string" ? options.envPath : process.env.PATH;
49
49
  const plistPath = (nativeId) => path.join(agentsDir, `${LAUNCHD_LABEL_PREFIX}${nativeId}.plist`);
50
50
  const label = (nativeId) => `${LAUNCHD_LABEL_PREFIX}${nativeId}`;
51
51
  const target = (nativeId) => `gui/${exec.uid()}/${label(nativeId)}`;
52
- const defaultContextPath = launchdDefaultContextPath(options, scheduledContext);
53
- const setEnableState = (nativeId, enabled) => setLaunchdEnableState(exec, target(nativeId), enabled);
52
+ const setEnableState = (nativeId, enabled) => {
53
+ const verb = enabled ? "enable" : "disable";
54
+ runOrThrow(exec, ["launchctl", verb, target(nativeId)], {
55
+ message: (result) => `launchctl ${verb} failed: ${result.stderr || result.stdout || "no output"}.`,
56
+ });
57
+ };
58
+ const bootout = (nativeId) => runOrThrow(exec, ["launchctl", "bootout", target(nativeId)], {
59
+ isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
60
+ message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
61
+ });
62
+ const xmlFor = (task, opts) => buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.environment, envPath);
54
63
  return {
55
64
  name: "launchd",
56
- install(task, opts, expected) {
57
- installLaunchdBinding(task, opts, expected, {
58
- exec,
59
- fsLike,
60
- agentsDir,
61
- logDir,
62
- akmArgv,
63
- defaultContextPath,
64
- plistPath,
65
- label,
66
- target,
67
- setEnableState,
68
- });
69
- },
70
- uninstall(nativeId, expected) {
71
- const inspectionContext = { exec, fsLike, agentsDir, plistPath, target, label };
72
- const initialNamespace = inspectStableLaunchdNamespace([nativeId], inspectionContext);
73
- let initialArtifact;
74
- if (expected) {
75
- assertSchedulerExpectationIdentity({ ...expected, state: "present" });
76
- initialArtifact = assertSchedulerNativeArtifactCardinality(initialNamespace.inspection.artifacts, nativeId, 1);
77
- if (!initialArtifact) {
78
- throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} disappeared during inspection.`, "INVALID_CONFIG_FILE");
79
- }
80
- assertSchedulerRemovalArtifact(initialArtifact.nativeId, expected, initialArtifact.invocation, initialArtifact.fingerprint);
81
- }
82
- else {
83
- initialArtifact = assertLaunchdDirectRemovalOwner(initialNamespace.inspection.artifacts, nativeId);
84
- }
85
- let resolvedNativeId = initialArtifact?.nativeId ?? nativeId;
86
- const file = plistPath(resolvedNativeId);
87
- const snapshot = snapshotLaunchdBindings([resolvedNativeId], {
88
- exec,
89
- fsLike,
90
- agentsDir,
91
- plistPath,
92
- target,
93
- label,
94
- });
95
- const finalArtifact = expected
96
- ? assertSchedulerNativeArtifactCardinality(snapshot.artifacts, nativeId, 1)
97
- : assertLaunchdDirectRemovalOwner(snapshot.artifacts, nativeId);
98
- if (expected) {
99
- if (!finalArtifact) {
100
- throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} disappeared during snapshot.`, "INVALID_CONFIG_FILE");
101
- }
102
- assertSchedulerRemovalArtifact(finalArtifact.nativeId, expected, finalArtifact.invocation, finalArtifact.fingerprint);
103
- }
104
- if (!finalArtifact)
105
- return;
106
- resolvedNativeId = finalArtifact.nativeId;
65
+ install(task, opts) {
66
+ const xml = xmlFor(task, opts);
67
+ const nativeId = schedulerBindingNativeId(task);
68
+ const file = plistPath(nativeId);
69
+ fsLike.ensureDir(agentsDir);
70
+ // launchd refuses to start a job whose StandardOutPath directory is
71
+ // missing; create it before touching launchd state.
72
+ fsLike.ensureDir(logDir);
73
+ const tempFile = path.join(agentsDir, `.${nativeId}.${Date.now()}.tmp`);
74
+ fsLike.writeFile(tempFile, xml);
107
75
  try {
108
- runOrThrow(exec, ["launchctl", "bootout", target(resolvedNativeId)], {
109
- isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
110
- message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
76
+ bootout(nativeId);
77
+ fsLike.replaceFile(tempFile, file);
78
+ // A disable override survives bootout and plist replacement. Clear it
79
+ // before bootstrap, then apply the desired state after registration.
80
+ setEnableState(nativeId, true);
81
+ runOrThrow(exec, ["launchctl", "bootstrap", `gui/${exec.uid()}`, file], {
82
+ message: (r) => `launchctl bootstrap failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
83
+ hint: "Ensure `launchctl` is available; on macOS it is part of the base system.",
111
84
  });
112
- // launchctl disable overrides persist after the plist is removed.
113
- setEnableState(resolvedNativeId, true);
114
- if (fsLike.exists(file))
115
- fsLike.removeFile(file);
85
+ if (!task.enabled)
86
+ setEnableState(nativeId, false);
116
87
  }
117
- catch (primaryError) {
118
- try {
119
- const rollbackExpected = [
120
- launchdRollbackExpectationForCurrent(resolvedNativeId, inspectionContext, initialArtifact),
121
- ];
122
- restoreLaunchdBindings(snapshot, { exec, fsLike, agentsDir, plistPath, target, label, setEnableState }, rollbackExpected);
123
- }
124
- catch (rollbackError) {
125
- throw new AggregateError([primaryError, rollbackError], `${errorMessage(primaryError)}; rollback for launchd removal ${JSON.stringify(nativeId)} was incomplete.`);
126
- }
127
- throw primaryError;
88
+ finally {
89
+ if (fsLike.exists(tempFile))
90
+ fsLike.removeFile(tempFile);
128
91
  }
129
92
  },
93
+ uninstall(nativeId) {
94
+ bootout(nativeId);
95
+ // launchctl disable overrides persist after the plist is removed.
96
+ setEnableState(nativeId, true);
97
+ const file = plistPath(nativeId);
98
+ if (fsLike.exists(file))
99
+ fsLike.removeFile(file);
100
+ },
130
101
  setEnabled(nativeId, enabled) {
131
102
  setEnableState(nativeId, enabled);
132
103
  },
133
104
  list() {
134
- return [
135
- ...inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label }).installed,
136
- ];
137
- },
138
- listForRebind() {
139
- const namespace = inspectStableLaunchdNamespace([], { exec, fsLike, agentsDir, plistPath, target, label });
140
- const refs = [];
141
- for (const entry of namespace.entries) {
142
- if (entry.plist === undefined)
105
+ const domain = exec.run(["launchctl", "print", `gui/${exec.uid()}`]);
106
+ if (domain.status !== 0) {
107
+ throw new ConfigError(`launchctl failed to enumerate the loaded user domain: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
108
+ }
109
+ const loadedLabels = parseLaunchdLoadedLabels(domain.stdout);
110
+ const disabledLabels = readDisabledLabels(exec);
111
+ const rows = [];
112
+ if (!fsLike.exists(agentsDir))
113
+ return rows;
114
+ for (const file of fsLike.list(agentsDir).sort()) {
115
+ if (!file.startsWith(LAUNCHD_LABEL_PREFIX) || !file.endsWith(".plist"))
116
+ continue;
117
+ const nativeId = file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length);
118
+ const raw = fsLike.readFile(plistPath(nativeId));
119
+ const parsed = extractPlistInvocation(raw);
120
+ if (!parsed)
143
121
  continue;
144
- const installed = extractPlistInvocation(entry.plist);
145
- const id = entry.nativeId;
146
- const ref = {
147
- id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
148
- ...(installed?.target !== undefined ? { target: installed.target } : {}),
149
- };
150
- Object.defineProperty(ref, "nativeId", { value: id });
151
- if (installed)
152
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
153
- refs.push(ref);
122
+ const enabled = !disabledLabels.has(label(nativeId));
123
+ const loaded = loadedLabels.has(label(nativeId));
124
+ rows.push({
125
+ id: schedulerLogicalBindingId(nativeId, parsed.invocation),
126
+ nativeId,
127
+ enabled: enabled && loaded,
128
+ // An unloaded service or a disable override is drift: the signature
129
+ // carries both, so sync bootstraps/enables it again.
130
+ signature: launchdFingerprint(raw, enabled, loaded),
131
+ ...(parsed.target !== undefined ? { target: parsed.target } : {}),
132
+ binding: parsed.binding,
133
+ ...(parsed.contextPath !== undefined ? { contextPath: parsed.contextPath } : {}),
134
+ ...(parsed.environment !== undefined ? { environment: parsed.environment } : {}),
135
+ invocation: parsed.invocation,
136
+ });
154
137
  }
155
- return refs;
156
- },
157
- listNativeArtifacts() {
158
- return [...inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label }).artifacts];
159
- },
160
- inspectBindings() {
161
- return inspectLaunchdState({ exec, fsLike, agentsDir, plistPath, target, label });
162
- },
163
- snapshotBindings(ids) {
164
- return snapshotLaunchdBindings(ids, { exec, fsLike, agentsDir, plistPath, target, label });
165
- },
166
- restoreBindings(snapshot, expectedCurrent) {
167
- restoreLaunchdBindings(snapshot, { exec, fsLike, agentsDir, plistPath, target, label, setEnableState }, expectedCurrent);
138
+ return rows;
168
139
  },
169
140
  expectedSignature(task, opts) {
170
- return launchdFingerprint(buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target), task.enabled, true);
141
+ return launchdFingerprint(xmlFor(task, opts), task.enabled, true);
171
142
  },
172
143
  };
173
144
  }
174
- function installLaunchdBinding(task, opts, expected, context) {
175
- const { exec, fsLike, agentsDir, logDir, akmArgv, defaultContextPath, plistPath, label, target, setEnableState } = context;
176
- if (expected)
177
- assertSchedulerExpectationIdentity(expected, task);
178
- // Capture PATH at install time so launchd (which strips the environment
179
- // aggressively) can find the same binaries the user sees interactively.
180
- const xml = buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
181
- const nativeId = schedulerBindingNativeId(task);
182
- const file = plistPath(nativeId);
183
- fsLike.ensureDir(agentsDir);
184
- // launchd refuses to start a job when StandardOutPath/StandardErrorPath
185
- // points at a non-existent directory; create it before reading or mutating
186
- // launchd state so a local preparation failure has no scheduler side effect.
187
- fsLike.ensureDir(logDir);
188
- const inspectionContext = { exec, fsLike, agentsDir, plistPath, target, label };
189
- const initialNamespace = inspectStableLaunchdNamespace([nativeId], inspectionContext);
190
- const initialArtifact = expected
191
- ? assertSchedulerNativeArtifactCardinality(initialNamespace.inspection.artifacts, nativeId, expected.state === "absent" ? 0 : 1)
192
- : assertLaunchdDirectMutationOwner(initialNamespace.inspection.artifacts, nativeId, task);
193
- if (expected)
194
- assertSchedulerMutationArtifact(initialArtifact, expected);
195
- const priorEntry = initialArtifact
196
- ? initialNamespace.entries.find((entry) => entry.nativeId === initialArtifact.nativeId)
197
- : undefined;
198
- const previousPlist = priorEntry?.plist;
199
- const previousEnabled = priorEntry?.enabled ?? true;
200
- const tempFile = path.join(agentsDir, `.${nativeId}.${Date.now()}.tmp`);
201
- fsLike.writeFile(tempFile, xml);
202
- let bootoutCompleted = false;
203
- let previousWasLoaded = false;
204
- let fileReplaced = false;
205
- let enableStateTouched = false;
206
- try {
207
- if (expected) {
208
- const finalInspection = inspectStableLaunchdNamespace([nativeId], inspectionContext).inspection;
209
- const finalArtifact = assertSchedulerNativeArtifactCardinality(finalInspection.artifacts, nativeId, expected.state === "absent" ? 0 : 1);
210
- assertSchedulerMutationArtifact(finalArtifact, expected);
211
- }
212
- else {
213
- const finalArtifact = assertLaunchdDirectMutationOwner(inspectStableLaunchdNamespace([nativeId], inspectionContext).inspection.artifacts, nativeId, task);
214
- if (finalArtifact?.nativeId !== initialArtifact?.nativeId ||
215
- finalArtifact?.fingerprint !== initialArtifact?.fingerprint) {
216
- throw new ConfigError(`launchd task ${JSON.stringify(nativeId)} changed while it was being prepared; refusing to replace an unverified owner.`, "INVALID_CONFIG_FILE");
217
- }
218
- }
219
- const bootout = runOrThrow(exec, ["launchctl", "bootout", target(nativeId)], {
220
- isOk: (r) => r.status === 0 || isServiceNotFoundResult(r),
221
- message: (r) => `launchctl bootout failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
222
- });
223
- bootoutCompleted = true;
224
- previousWasLoaded = previousPlist !== undefined && bootout.status === 0;
225
- fsLike.replaceFile(tempFile, file);
226
- fileReplaced = true;
227
- // A disable override survives bootout and plist replacement. Clear it
228
- // before bootstrap, then apply the desired state after registration.
229
- enableStateTouched = true;
230
- setEnableState(nativeId, true);
231
- runOrThrow(exec, ["launchctl", "bootstrap", `gui/${exec.uid()}`, file], {
232
- message: (r) => `launchctl bootstrap failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
233
- hint: "Ensure `launchctl` is available; on macOS it is part of the base system.",
234
- });
235
- if (!task.enabled)
236
- setEnableState(nativeId, false);
237
- }
238
- catch (err) {
239
- if (!bootoutCompleted)
240
- throw err;
241
- const rollbackErrors = [];
242
- let priorFileRestored = !fileReplaced;
243
- if (fileReplaced) {
244
- let replacementUnloaded = false;
245
- try {
246
- const rollbackBootout = exec.run(["launchctl", "bootout", target(nativeId)]);
247
- replacementUnloaded = rollbackBootout.status === 0 || isServiceNotFoundResult(rollbackBootout);
248
- if (!replacementUnloaded) {
249
- rollbackErrors.push(new ConfigError(`launchctl bootout during rollback failed: ${rollbackBootout.stderr || rollbackBootout.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
250
- }
251
- }
252
- catch (rollbackError) {
253
- rollbackErrors.push(rollbackError);
254
- }
255
- if (replacementUnloaded) {
256
- try {
257
- if (previousPlist === undefined) {
258
- if (fsLike.exists(file))
259
- fsLike.removeFile(file);
260
- }
261
- else {
262
- fsLike.writeFile(tempFile, previousPlist);
263
- fsLike.replaceFile(tempFile, file);
264
- }
265
- priorFileRestored = true;
266
- }
267
- catch (rollbackError) {
268
- rollbackErrors.push(rollbackError);
269
- }
270
- }
271
- }
272
- restoreLaunchdInstallState({
273
- exec,
274
- nativeId,
275
- file,
276
- previousPlist,
277
- previousEnabled,
278
- previousWasLoaded,
279
- priorFileRestored,
280
- enableStateTouched,
281
- setEnableState,
282
- rollbackErrors,
283
- });
284
- if (rollbackErrors.length > 0) {
285
- const message = err instanceof Error ? err.message : String(err);
286
- throw new AggregateError([err, ...rollbackErrors], `${message}; rollback for launchd task "${task.id}" was incomplete.`);
287
- }
288
- throw err;
289
- }
290
- finally {
291
- if (fsLike.exists(tempFile))
292
- fsLike.removeFile(tempFile);
293
- }
294
- }
295
- function restoreLaunchdInstallState(input) {
296
- const { exec, nativeId, file, previousPlist, previousEnabled, previousWasLoaded, priorFileRestored, enableStateTouched, setEnableState, rollbackErrors, } = input;
297
- if (previousPlist !== undefined && previousWasLoaded && priorFileRestored) {
298
- try {
299
- setEnableState(nativeId, true);
300
- const restore = exec.run(["launchctl", "bootstrap", `gui/${exec.uid()}`, file]);
301
- if (restore.status !== 0) {
302
- rollbackErrors.push(new ConfigError(`launchctl bootstrap during rollback failed: ${restore.stderr || restore.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
303
- }
304
- }
305
- catch (rollbackError) {
306
- rollbackErrors.push(rollbackError);
307
- }
308
- if (!previousEnabled) {
309
- try {
310
- setEnableState(nativeId, false);
311
- }
312
- catch (rollbackError) {
313
- rollbackErrors.push(rollbackError);
314
- }
315
- }
316
- }
317
- else if (enableStateTouched) {
318
- try {
319
- setEnableState(nativeId, previousPlist === undefined || previousEnabled);
320
- }
321
- catch (rollbackError) {
322
- rollbackErrors.push(rollbackError);
323
- }
324
- }
325
- }
326
- function snapshotLaunchdBindings(ids, context) {
327
- const requestedKeys = new Set(ids.map(schedulerNativeArtifactKey));
328
- const namespace = inspectStableLaunchdNamespace(ids, context);
329
- const matchingEntries = namespace.entries.filter((entry) => requestedKeys.has(schedulerNativeArtifactKey(entry.nativeId)));
330
- const matchedKeys = new Set(matchingEntries.map((entry) => schedulerNativeArtifactKey(entry.nativeId)));
331
- const entries = [
332
- ...matchingEntries.map((entry) => Object.freeze({
333
- id: entry.nativeId,
334
- ...(entry.plist !== undefined ? { plist: entry.plist } : {}),
335
- loaded: entry.loaded,
336
- enabled: entry.enabled,
337
- })),
338
- ...ids
339
- .filter((id) => !matchedKeys.has(schedulerNativeArtifactKey(id)))
340
- .map((id) => Object.freeze({ id, loaded: false, enabled: true })),
341
- ];
342
- const artifacts = matchingEntries.map((entry) => entry.artifact);
343
- return Object.freeze({
344
- kind: LAUNCHD_SNAPSHOT,
345
- nativeIds: Object.freeze([...ids]),
346
- artifacts: Object.freeze(artifacts),
347
- entries: Object.freeze(entries),
348
- });
349
- }
350
- function restoreLaunchdBindings(snapshot, context, expectedCurrent) {
351
- if (!isLaunchdBindingSnapshot(snapshot)) {
352
- throw new ConfigError("Invalid launchd scheduler snapshot.", "INVALID_CONFIG_FILE");
353
- }
354
- const preflightErrors = [];
355
- const duplicateSnapshotKeys = new Set();
356
- const requestedIdsByKey = new Map();
357
- for (const nativeId of snapshot.nativeIds) {
358
- const key = schedulerNativeArtifactKey(nativeId);
359
- const priorId = requestedIdsByKey.get(key);
360
- if (priorId !== undefined) {
361
- duplicateSnapshotKeys.add(key);
362
- preflightErrors.push(new ConfigError(`Launchd scheduler snapshot requests normalized duplicate artifacts ${JSON.stringify(priorId)} and ${JSON.stringify(nativeId)}; refusing restore.`, "INVALID_CONFIG_FILE"));
363
- continue;
364
- }
365
- requestedIdsByKey.set(key, nativeId);
366
- }
367
- const snapshotEntryIdsByKey = new Map();
368
- for (const entry of snapshot.entries) {
369
- const key = schedulerNativeArtifactKey(entry.id);
370
- const priorId = snapshotEntryIdsByKey.get(key);
371
- if (priorId !== undefined) {
372
- if (duplicateSnapshotKeys.has(key))
373
- continue;
374
- duplicateSnapshotKeys.add(key);
375
- preflightErrors.push(new ConfigError(`Launchd scheduler snapshot contains normalized duplicate artifacts ${JSON.stringify(priorId)} and ${JSON.stringify(entry.id)}; refusing restore.`, "INVALID_CONFIG_FILE"));
376
- continue;
377
- }
378
- snapshotEntryIdsByKey.set(key, entry.id);
379
- }
380
- let rollbackInventory;
381
- try {
382
- rollbackInventory = inspectStableLaunchdNamespace(snapshot.nativeIds, context).inspection;
383
- }
384
- catch (error) {
385
- preflightErrors.push(error);
386
- }
387
- if (preflightErrors.length > 0) {
388
- throw new AggregateError(preflightErrors, `Failed to preflight ${preflightErrors.length} launchd scheduler restore operation(s).`);
389
- }
390
- const expectationsById = new Map();
391
- const expectationIdsByKey = new Map();
392
- if (expectedCurrent) {
393
- for (const expected of expectedCurrent) {
394
- const key = schedulerNativeArtifactKey(expected.nativeId);
395
- const priorExact = expectationsById.get(expected.nativeId);
396
- const priorKeyId = expectationIdsByKey.get(key);
397
- if (priorExact !== undefined || priorKeyId !== undefined) {
398
- preflightErrors.push(new ConfigError(`Launchd rollback expectations contain duplicate normalized artifact ${JSON.stringify(expected.nativeId)}.`, "INVALID_CONFIG_FILE"));
399
- continue;
400
- }
401
- expectationsById.set(expected.nativeId, expected);
402
- expectationIdsByKey.set(key, expected.nativeId);
403
- }
404
- }
405
- const plannedEntries = [];
406
- const coveredExpectationIds = new Set();
407
- if (rollbackInventory) {
408
- for (const entry of snapshot.entries) {
409
- if (duplicateSnapshotKeys.has(schedulerNativeArtifactKey(entry.id)))
410
- continue;
411
- try {
412
- let current;
413
- if (expectedCurrent) {
414
- const expected = expectationsById.get(entry.id);
415
- if (!expected) {
416
- throw new ConfigError(`Missing launchd rollback expectation for ${JSON.stringify(entry.id)}.`, "INVALID_CONFIG_FILE");
417
- }
418
- coveredExpectationIds.add(entry.id);
419
- current = assertSchedulerRollbackArtifactCardinality(rollbackInventory.artifacts, expected);
420
- }
421
- else {
422
- const currentCount = rollbackInventory.artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === schedulerNativeArtifactKey(entry.id)).length;
423
- current = assertSchedulerNativeArtifactCardinality(rollbackInventory.artifacts, entry.id, currentCount === 0 ? 0 : 1);
424
- if (current !== undefined && current.nativeId !== entry.id) {
425
- throw new ConfigError(`Launchd scheduler artifact ${JSON.stringify(entry.id)} changed to case- or suffix-equivalent spelling ${JSON.stringify(current.nativeId)} before restore.`, "INVALID_CONFIG_FILE");
426
- }
427
- }
428
- if (entry.loaded && entry.plist === undefined) {
429
- assertUnrestorableLaunchdSnapshotUnchanged(entry, rollbackInventory.artifacts);
430
- }
431
- plannedEntries.push(Object.freeze({
432
- entry,
433
- immediateExpectation: launchdRollbackExpectationFromArtifact(entry.id, current),
434
- noOp: entry.loaded && entry.plist === undefined,
435
- }));
436
- }
437
- catch (error) {
438
- preflightErrors.push(error);
439
- }
440
- }
441
- }
442
- if (expectedCurrent) {
443
- for (const expected of expectedCurrent) {
444
- if (!coveredExpectationIds.has(expected.nativeId)) {
445
- preflightErrors.push(new ConfigError(`Unexpected launchd rollback expectation for ${JSON.stringify(expected.nativeId)} is not covered by the snapshot.`, "INVALID_CONFIG_FILE"));
446
- }
447
- }
448
- }
449
- if (preflightErrors.length > 0) {
450
- throw new AggregateError(preflightErrors, `Failed to preflight ${preflightErrors.length} launchd scheduler restore operation(s).`);
451
- }
452
- const errors = [];
453
- for (const planned of plannedEntries) {
454
- if (planned.noOp)
455
- continue;
456
- try {
457
- const immediateInventory = inspectStableLaunchdNamespace(snapshot.nativeIds, context).inspection;
458
- assertSchedulerRollbackArtifactCardinality(immediateInventory.artifacts, planned.immediateExpectation);
459
- }
460
- catch (error) {
461
- errors.push(error);
462
- continue;
463
- }
464
- restoreLaunchdBindingEntry(planned.entry, context, errors);
465
- }
466
- if (errors.length > 0) {
467
- throw new AggregateError(errors, `Failed to completely restore ${errors.length} launchd scheduler operation(s).`);
468
- }
469
- }
470
- function launchdRollbackExpectationFromArtifact(nativeId, current) {
471
- if (current === undefined) {
472
- return Object.freeze({ nativeId, allowed: Object.freeze([{ state: "absent" }]) });
473
- }
474
- if (current.nativeId !== nativeId || current.fingerprint === undefined) {
475
- throw new ConfigError(`Launchd scheduler artifact ${JSON.stringify(nativeId)} has no exact restorable fingerprint.`, "INVALID_CONFIG_FILE");
476
- }
477
- return Object.freeze({
478
- nativeId,
479
- allowed: Object.freeze([
480
- Object.freeze({
481
- state: "present",
482
- ...(current.bindingId !== undefined ? { bindingId: current.bindingId } : {}),
483
- ...(current.invocation !== undefined ? { invocation: current.invocation } : {}),
484
- fingerprint: current.fingerprint,
485
- }),
486
- ]),
487
- });
488
- }
489
- function assertUnrestorableLaunchdSnapshotUnchanged(entry, currentArtifacts) {
490
- const current = assertSchedulerNativeArtifactCardinality(currentArtifacts, entry.id, 1);
491
- const expectedFingerprint = launchdMissingPlistArtifact(entry.id, entry.enabled, entry.loaded).fingerprint;
492
- if (current?.nativeId !== entry.id ||
493
- current.invocation !== undefined ||
494
- current.bindingId !== undefined ||
495
- current.fingerprint !== expectedFingerprint) {
496
- throw new ConfigError(`Loaded launchd artifact ${JSON.stringify(entry.id)} has no restorable plist and changed after its snapshot; refusing mutation.`, "INVALID_CONFIG_FILE");
497
- }
498
- }
499
- /**
500
- * Enumerate the current launchd AKM service namespace (loaded domain members,
501
- * print-disabled labels, and on-disk plists) in one pass.
502
- *
503
- * This used to run the enumeration twice and fail closed if the two passes
504
- * disagreed, guarding against launchd state mutating between the two
505
- * `launchctl` shell-outs (milliseconds apart, in-process). The only actor
506
- * that could cause that is another concurrent akm process or the user
507
- * running launchctl by hand at that exact instant; a subsequent `task sync`
508
- * (idempotent by design) reconciles any such drift on its own, so the
509
- * two-pass fencing added a TOCTOU-shaped guard around a race with a working
510
- * self-heal already in place, not a way to avoid one.
511
- */
512
- function inspectStableLaunchdNamespace(seedIds, context) {
513
- const domain = context.exec.run(["launchctl", "print", `gui/${context.exec.uid()}`]);
514
- if (domain.status !== 0) {
515
- throw new ConfigError(`launchctl failed to enumerate the loaded user domain during scheduler state inspection: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
516
- }
517
- const loadedLabels = parseLaunchdLoadedLabels(domain.stdout);
518
- const disabledLabels = readDisabledLabels(context.exec);
519
- const akmDisabledLabels = [...disabledLabels].filter((label) => label.startsWith(LAUNCHD_LABEL_PREFIX)).sort();
520
- const plistEntries = [];
521
- if (context.fsLike.exists(context.agentsDir)) {
522
- for (const file of context.fsLike.list(context.agentsDir).sort()) {
523
- if (!file.startsWith(LAUNCHD_LABEL_PREFIX) || !file.endsWith(".plist"))
524
- continue;
525
- const nativeId = file.slice(LAUNCHD_LABEL_PREFIX.length, -".plist".length);
526
- plistEntries.push(Object.freeze([nativeId, context.fsLike.readFile(context.plistPath(nativeId))]));
527
- }
528
- }
529
- const ids = new Set(seedIds);
530
- for (const [nativeId] of plistEntries)
531
- ids.add(nativeId);
532
- for (const serviceLabel of loadedLabels)
533
- ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
534
- for (const serviceLabel of akmDisabledLabels)
535
- ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
536
- const plistByNativeId = new Map(plistEntries);
537
- const entries = [];
538
- for (const nativeId of [...ids].sort()) {
539
- const serviceLabel = context.label(nativeId);
540
- const plist = plistByNativeId.get(nativeId);
541
- const enabled = !disabledLabels.has(serviceLabel);
542
- const loaded = loadedLabels.has(serviceLabel);
543
- if (plist === undefined && enabled && !loaded)
544
- continue;
545
- const artifact = plist === undefined
546
- ? launchdMissingPlistArtifact(nativeId, enabled, loaded)
547
- : launchdArtifact(nativeId, plist, enabled, loaded);
548
- entries.push(Object.freeze({ nativeId, ...(plist !== undefined ? { plist } : {}), enabled, loaded, artifact }));
549
- }
550
- const artifacts = entries.map((entry) => entry.artifact);
551
- const installed = [];
552
- for (const entry of entries) {
553
- if (entry.plist === undefined)
554
- continue;
555
- const parsed = extractPlistInvocation(entry.plist);
556
- if (!parsed)
557
- continue;
558
- installed.push(withInstalledInvocation({
559
- id: schedulerLogicalBindingId(entry.nativeId, parsed.invocation),
560
- enabled: entry.enabled && entry.loaded,
561
- ...(entry.loaded ? { signature: entry.artifact.fingerprint } : {}),
562
- ...(parsed.target !== undefined ? { target: parsed.target } : {}),
563
- binding: parsed.binding,
564
- contextPath: parsed.contextPath,
565
- }, parsed.invocation, entry.nativeId));
566
- }
567
- return Object.freeze({
568
- inspection: Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) }),
569
- entries: Object.freeze(entries),
570
- });
571
- }
572
- function restoreLaunchdBindingEntry(entry, context, errors) {
573
- try {
574
- runOrThrow(context.exec, ["launchctl", "bootout", context.target(entry.id)], {
575
- isOk: (result) => result.status === 0 || isServiceNotFoundResult(result),
576
- message: (result) => `launchctl bootout failed while restoring task "${entry.id}": ${result.stderr || result.stdout || "no output"}.`,
577
- });
578
- }
579
- catch (error) {
580
- errors.push(error);
581
- return;
582
- }
583
- const file = context.plistPath(entry.id);
584
- let priorFileRestored = false;
585
- try {
586
- if (entry.plist === undefined) {
587
- if (context.fsLike.exists(file))
588
- context.fsLike.removeFile(file);
589
- }
590
- else {
591
- context.fsLike.ensureDir(context.agentsDir);
592
- context.fsLike.writeFile(file, entry.plist);
593
- }
594
- priorFileRestored = true;
595
- }
596
- catch (error) {
597
- errors.push(error);
598
- }
599
- if (entry.loaded && entry.plist !== undefined && priorFileRestored) {
600
- let overrideCleared = false;
601
- try {
602
- // A disable override survives bootout. Clear it before bootstrap or
603
- // launchd can refuse to register the exact prior loaded definition.
604
- context.setEnableState(entry.id, true);
605
- overrideCleared = true;
606
- }
607
- catch (error) {
608
- errors.push(error);
609
- }
610
- if (overrideCleared) {
611
- try {
612
- runOrThrow(context.exec, ["launchctl", "bootstrap", `gui/${context.exec.uid()}`, file], {
613
- message: (result) => `launchctl bootstrap failed while restoring task "${entry.id}": ${result.stderr || result.stdout || "no output"}.`,
614
- });
615
- }
616
- catch (error) {
617
- errors.push(error);
618
- }
619
- }
620
- }
621
- try {
622
- // Apply the prior override last even when an earlier step failed; this is
623
- // both the exact state ordering and the best available partial recovery.
624
- context.setEnableState(entry.id, entry.enabled);
625
- }
626
- catch (error) {
627
- errors.push(error);
628
- }
629
- }
630
- function isLaunchdBindingSnapshot(value) {
631
- return (typeof value === "object" &&
632
- value !== null &&
633
- value.kind === LAUNCHD_SNAPSHOT &&
634
- Array.isArray(value.entries));
635
- }
636
- function inspectLaunchdState(context) {
637
- return inspectStableLaunchdNamespace([], context).inspection;
638
- }
639
- function assertLaunchdDirectMutationOwner(artifacts, nativeId, task) {
640
- const key = schedulerNativeArtifactKey(nativeId);
641
- const count = artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
642
- const artifact = assertSchedulerNativeArtifactCardinality(artifacts, nativeId, count === 0 ? 0 : 1);
643
- if (artifact)
644
- assertSchedulerNativeArtifactOwner(artifact.nativeId, task, artifact.invocation);
645
- return artifact;
646
- }
647
- function assertLaunchdDirectRemovalOwner(artifacts, nativeId) {
648
- const key = schedulerNativeArtifactKey(nativeId);
649
- const count = artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
650
- const artifact = assertSchedulerNativeArtifactCardinality(artifacts, nativeId, count === 0 ? 0 : 1);
651
- if (!artifact)
652
- return undefined;
653
- if (artifact.nativeId !== nativeId || artifact.invocation === undefined) {
654
- throw new ConfigError(`launchd artifact ${JSON.stringify(artifact.nativeId)} is not the exact proven requested owner ${JSON.stringify(nativeId)}; refusing direct removal.`, "INVALID_CONFIG_FILE");
655
- }
656
- return artifact;
657
- }
658
- function launchdRollbackExpectationForCurrent(nativeId, context, priorArtifact) {
659
- const namespace = inspectStableLaunchdNamespace([nativeId], context);
660
- const key = schedulerNativeArtifactKey(nativeId);
661
- const count = namespace.inspection.artifacts.filter((artifact) => schedulerNativeArtifactKey(artifact.nativeId) === key).length;
662
- const artifact = assertSchedulerNativeArtifactCardinality(namespace.inspection.artifacts, nativeId, count === 0 ? 0 : 1);
663
- if (!artifact) {
664
- return Object.freeze({ nativeId, allowed: Object.freeze([{ state: "absent" }]) });
665
- }
666
- if (artifact.fingerprint === undefined ||
667
- artifact.invocation === undefined ||
668
- priorArtifact?.invocation === undefined ||
669
- !sameStringArray(artifact.invocation, priorArtifact.invocation)) {
670
- throw new ConfigError(`launchd artifact ${JSON.stringify(nativeId)} has no exact transaction-owned rollback fingerprint.`, "INVALID_CONFIG_FILE");
671
- }
672
- return Object.freeze({
673
- nativeId,
674
- allowed: Object.freeze([
675
- Object.freeze({
676
- state: "present",
677
- ...(artifact.bindingId !== undefined ? { bindingId: artifact.bindingId } : {}),
678
- invocation: Object.freeze([...artifact.invocation]),
679
- fingerprint: artifact.fingerprint,
680
- }),
681
- ]),
682
- });
683
- }
684
- function sameStringArray(left, right) {
685
- return left.length === right.length && left.every((value, index) => value === right[index]);
686
- }
687
- function errorMessage(error) {
688
- return error instanceof Error ? error.message : String(error);
689
- }
690
- function launchdMissingPlistArtifact(nativeId, enabled, loaded) {
691
- const artifact = { nativeId };
692
- Object.defineProperty(artifact, "fingerprint", {
693
- value: `launchd:missing-plist:enabled=${enabled}:loaded=${loaded}`,
694
- });
695
- return artifact;
696
- }
697
- function launchdArtifact(nativeId, raw, enabled, loaded) {
698
- const parsed = extractPlistInvocation(raw);
699
- const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
700
- const artifact = parsed
701
- ? {
702
- nativeId,
703
- ...(owner !== undefined ? { bindingId: owner } : {}),
704
- invocation: Object.freeze([...parsed.invocation]),
705
- }
706
- : { nativeId };
707
- if (enabled !== undefined && loaded !== undefined) {
708
- Object.defineProperty(artifact, "fingerprint", { value: launchdFingerprint(raw, enabled, loaded) });
709
- }
710
- return artifact;
711
- }
712
145
  function launchdFingerprint(raw, enabled, loaded) {
713
146
  const signed = raw.replace(/<!-- akm-enabled:(?:true|false) -->/, `<!-- akm-enabled:${enabled} -->`);
714
- return `${normalizeSignature(signed)}:loaded=${loaded}`;
715
- }
716
- function withInstalledInvocation(ref, invocation, nativeId) {
717
- Object.defineProperty(ref, "nativeId", { value: nativeId });
718
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...invocation]) });
719
- return ref;
720
- }
721
- function launchdDefaultContextPath(options, scheduledContext) {
722
- const pathEnv = options.envPath === false
723
- ? undefined
724
- : typeof options.envPath === "string"
725
- ? options.envPath
726
- : (process.env.PATH ?? "");
727
- return schedulerContextPath(schedulerContextDescriptor(scheduledContext, pathEnv ?? ""));
728
- }
729
- function setLaunchdEnableState(exec, nativeTarget, enabled) {
730
- const verb = enabled ? "enable" : "disable";
731
- runOrThrow(exec, ["launchctl", verb, nativeTarget], {
732
- message: (result) => `launchctl ${verb} failed: ${result.stderr || result.stdout || "no output"}.`,
733
- });
147
+ return `${signed.replace(/\r\n/g, "\n").trim()}:loaded=${loaded}`;
734
148
  }
735
149
  export function extractPlistInvocation(xml) {
736
150
  const block = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
737
151
  if (!block)
738
152
  return undefined;
739
153
  const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => decodeXmlEntities(m[1]));
740
- return parseScheduledBindingArgv(args);
154
+ const parsed = parseScheduledInvocationArgv(args);
155
+ if (!parsed)
156
+ return undefined;
157
+ const variables = {};
158
+ const dict = xml.match(/<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/)?.[1] ?? "";
159
+ for (const entry of dict.matchAll(/<key>([^<]*)<\/key>\s*<string>([\s\S]*?)<\/string>/g)) {
160
+ variables[decodeXmlEntities(entry[1])] = decodeXmlEntities(entry[2]);
161
+ }
162
+ const environment = scheduledRowEnvironmentFrom(variables);
163
+ return environment ? { ...parsed, environment } : parsed;
741
164
  }
742
165
  function decodeXmlEntities(value) {
743
166
  return value
@@ -748,25 +171,34 @@ function decodeXmlEntities(value) {
748
171
  .replaceAll("&amp;", "&");
749
172
  }
750
173
  // ── XML builder (exported for tests) ────────────────────────────────────────
751
- export function buildPlistXml(task, akmArgv, logDir, contextPath, _target) {
752
- const spec = parseSchedule(task.cron, "launchd");
753
- const trigger = translateToLaunchd(spec);
754
- const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
755
- const argv = invocation.argv;
174
+ function renderPlistEnvironment(envPath, environment) {
175
+ const variables = [
176
+ ...(envPath ? [["PATH", envPath]] : []),
177
+ ...scheduledRowEnvironmentEntries(environment),
178
+ ];
179
+ if (variables.length === 0)
180
+ return "";
181
+ return [
182
+ " <key>EnvironmentVariables</key>",
183
+ " <dict>",
184
+ ...variables.flatMap(([name, value]) => [` <key>${name}</key>`, ` <string>${escapeXml(value)}</string>`]),
185
+ " </dict>",
186
+ "",
187
+ ].join("\n");
188
+ }
189
+ export function buildPlistXml(task, akmArgv, logDir, environment, envPath) {
190
+ const trigger = translateToLaunchd(parseSchedule(task.cron, "launchd"));
191
+ const argv = buildScheduledInvocation(akmArgv, task.invocation);
756
192
  const programArgs = argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
757
193
  const nativeId = schedulerBindingNativeId(task);
758
194
  const logPath = path.join(logDir, `${nativeId}.log`);
759
- const triggerXml = renderLaunchdTrigger(trigger);
760
- const executionEvidence = task.executionEvidenceDigest === undefined
761
- ? ""
762
- : ` <!-- akm-workflow-evidence:${assertSchedulerExecutionEvidenceDigest(task.executionEvidenceDigest)} -->\n`;
763
195
  const xml = launchdTemplate
764
- .replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n${executionEvidence}`)
196
+ .replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n`)
765
197
  .replace("{{LABEL}}", LAUNCHD_LABEL_PREFIX + escapeXml(nativeId))
766
198
  .replace("{{PROGRAM_ARGS}}", programArgs)
767
199
  .replaceAll("{{LOG_PATH}}", escapeXml(logPath))
768
- .replace("{{ENV_VARS}}", "")
769
- .replace("{{TRIGGER_XML}}", triggerXml);
200
+ .replace("{{ENV_VARS}}", () => renderPlistEnvironment(envPath, environment))
201
+ .replace("{{TRIGGER_XML}}", renderLaunchdTrigger(trigger));
770
202
  for (const char of xml) {
771
203
  const code = char.codePointAt(0) ?? 0;
772
204
  if (code <= 0x08 || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f)) {
@@ -778,22 +210,18 @@ export function buildPlistXml(task, akmArgv, logDir, contextPath, _target) {
778
210
  function renderLaunchdTrigger(trigger) {
779
211
  if (trigger.calendars !== undefined) {
780
212
  const lines = [" <key>StartCalendarInterval</key>", " <array>"];
781
- for (const calendar of trigger.calendars) {
213
+ for (const calendar of trigger.calendars)
782
214
  lines.push(...renderCalendar(calendar, " "));
783
- }
784
215
  lines.push(" </array>");
785
216
  return lines.join("\n");
786
217
  }
787
- const cal = trigger.calendar ?? {};
788
- const lines = [" <key>StartCalendarInterval</key>", ...renderCalendar(cal, " ")];
789
- return lines.join("\n");
218
+ return [" <key>StartCalendarInterval</key>", ...renderCalendar(trigger.calendar ?? {}, " ")].join("\n");
790
219
  }
791
220
  function renderCalendar(calendar, indent) {
792
221
  const valueIndent = `${indent} `;
793
222
  const lines = [`${indent}<dict>`];
794
- if (calendar.Minute !== undefined) {
223
+ if (calendar.Minute !== undefined)
795
224
  lines.push(`${valueIndent}<key>Minute</key><integer>${calendar.Minute}</integer>`);
796
- }
797
225
  if (calendar.Hour !== undefined)
798
226
  lines.push(`${valueIndent}<key>Hour</key><integer>${calendar.Hour}</integer>`);
799
227
  if (calendar.Day !== undefined)
@@ -806,49 +234,25 @@ function renderCalendar(calendar, indent) {
806
234
  lines.push(`${indent}</dict>`);
807
235
  return lines;
808
236
  }
809
- function normalizeSignature(xml) {
810
- return xml.replace(/\r\n/g, "\n").trim();
811
- }
812
237
  /**
813
238
  * Collect the akm-owned service labels present in `launchctl` output.
814
239
  *
815
- * This reads the CURRENT USER'S OWN scheduler inventory — the tasks they asked
816
- * akm to schedule, on their own machine. It is not an attacker-controlled
817
- * document, so it is parsed permissively: scan for labels in our own
818
- * `com.akm.task.` namespace and ignore everything else.
819
- *
820
- * It previously enforced a rigid grammar over the whole document and returned
821
- * `undefined` — surfacing as a hard INVALID_CONFIG_FILE that refused to inspect
822
- * scheduler state at all — if ANY line failed to match. Real
823
- * `launchctl print gui/<uid>` output on macOS is far richer than that grammar
824
- * allowed, so on a real Mac this rejected the user's own inventory and broke
825
- * every launchd operation. Caught by the gated native-scheduler suite, which
826
- * had never been dispatched before.
827
- *
828
- * The two bounds that remain are real resource bounds, not structural ones: a
829
- * cap on how much output we will read, and a cap on how many distinct akm
830
- * labels we will track. Exceeding either still returns `undefined`.
240
+ * This reads the CURRENT USER'S OWN scheduler inventory, so it is parsed
241
+ * permissively: scan for labels in our own `com.akm.task.` namespace and
242
+ * ignore everything else.
831
243
  */
832
244
  export function parseLaunchdLoadedLabels(output) {
833
245
  const labels = new Set();
834
- // Our own namespace is the only thing we look for. `[^\s"{}=,()]` stops the
835
- // token at whatever punctuation the surrounding launchctl syntax uses, so a
836
- // label works whether it appears as a bare table cell, a quoted string, or a
837
- // dictionary key.
838
- const labelPattern = /com\.akm\.task\.[^\s"{}=,()]+/gu;
839
- for (const match of boundLaunchdOutput(output).matchAll(labelPattern)) {
840
- const label = match[0];
841
- if (!LAUNCHD_AKM_LABEL_RE.test(label))
842
- continue;
843
- labels.add(label);
844
- if (labels.size >= MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
845
- break;
246
+ // `[^\s"{}=,()]` stops the token at whatever punctuation the surrounding
247
+ // launchctl syntax uses, so a label works whether it appears as a bare
248
+ // table cell, a quoted string, or a dictionary key.
249
+ for (const match of output.matchAll(/com\.akm\.task\.[^\s"{}=,()]+/gu)) {
250
+ if (LAUNCHD_AKM_LABEL_RE.test(match[0]))
251
+ labels.add(match[0]);
846
252
  }
847
253
  return labels;
848
254
  }
849
- function boundLaunchdOutput(output) {
850
- return output.length > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES ? output.slice(0, MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES) : output;
851
- }
255
+ /** `print-disabled` failing or looking unfamiliar degrades to "nothing is disabled". */
852
256
  function readDisabledLabels(exec) {
853
257
  try {
854
258
  const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
@@ -856,35 +260,25 @@ function readDisabledLabels(exec) {
856
260
  warn("[akm] launchctl print-disabled exited %d; assuming no akm task is disabled.", result.status);
857
261
  return new Set();
858
262
  }
859
- return parseDisabledLabels(result.stdout);
263
+ const disabled = new Set();
264
+ const entryPattern = /"(com\.akm\.task\.[^"\r\n]+)"\s*=>\s*(true|false|enabled|disabled)/gu;
265
+ for (const match of result.stdout.matchAll(entryPattern)) {
266
+ const label = match[1];
267
+ if (LAUNCHD_AKM_LABEL_RE.test(label) && (match[2] === "true" || match[2] === "disabled"))
268
+ disabled.add(label);
269
+ }
270
+ return disabled;
860
271
  }
861
272
  catch (error) {
862
273
  warn("[akm] launchctl print-disabled could not be run; assuming no akm task is disabled: %s", error instanceof Error ? error.message : String(error));
863
274
  return new Set();
864
275
  }
865
276
  }
866
- function parseDisabledLabels(output) {
867
- const disabled = new Set();
868
- const entryPattern = /"(com\.akm\.task\.[^"\r\n]+)"\s*=>\s*(true|false|enabled|disabled)/gu;
869
- for (const match of boundLaunchdOutput(output).matchAll(entryPattern)) {
870
- const label = match[1];
871
- if (!LAUNCHD_AKM_LABEL_RE.test(label))
872
- continue;
873
- if (match[2] === "true" || match[2] === "disabled")
874
- disabled.add(label);
875
- if (disabled.size >= MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
876
- break;
877
- }
878
- return disabled;
879
- }
880
277
  function isServiceNotFoundResult(result) {
881
278
  const output = `${result.stderr ?? ""}\n${result.stdout ?? ""}`;
882
279
  return /could not find service\b|service\b.*\bnot found\b|\bno such process\b/i.test(output);
883
280
  }
884
281
  function defaultAgentsDir() {
885
- // launchd's per-user LaunchAgents live under the user's home directory.
886
- // If we can't determine HOME, refuse rather than silently producing a
887
- // relative path that would write somewhere unexpected.
888
282
  const home = os.homedir();
889
283
  if (!home) {
890
284
  throw new ConfigError("Cannot determine user home directory; launchd backend requires HOME to locate ~/Library/LaunchAgents.", "INVALID_CONFIG_FILE", "Set $HOME (POSIX) or the equivalent before running `akm task` on macOS.");
@@ -917,9 +311,8 @@ function defaultLaunchdFs() {
917
311
  return fs.readdirSync(dir);
918
312
  }
919
313
  catch (error) {
920
- // Genuinely absent is an empty listing; anything else (e.g. EACCES) is
921
- // not "no plists" — surfacing it as such could make inspectBindings
922
- // think there is nothing to reconcile.
314
+ // Genuinely absent is an empty listing; anything else (e.g. EACCES)
315
+ // is not "no plists".
923
316
  if (hasErrnoCode(error, "ENOENT"))
924
317
  return [];
925
318
  throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");