akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.5

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 (346) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/lint/base-linter.js +19 -5
  53. package/dist/commands/proposal/drain.js +251 -644
  54. package/dist/commands/proposal/proposal-cli.js +3 -18
  55. package/dist/commands/proposal/proposal-types.js +20 -41
  56. package/dist/commands/proposal/proposal.js +1 -2
  57. package/dist/commands/proposal/propose.js +134 -160
  58. package/dist/commands/proposal/repository.js +502 -1487
  59. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  60. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  61. package/dist/commands/proposal/validators/proposals.js +13 -89
  62. package/dist/commands/read/curate.js +63 -413
  63. package/dist/commands/read/search-cli.js +16 -33
  64. package/dist/commands/read/search.js +17 -23
  65. package/dist/commands/read/show.js +2 -13
  66. package/dist/commands/sources/bundle-cli.js +25 -2
  67. package/dist/commands/sources/bundle-config-ops.js +4 -0
  68. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  69. package/dist/commands/sources/info.js +2 -11
  70. package/dist/commands/sources/installed-stashes.js +197 -746
  71. package/dist/commands/sources/schema-repair.js +98 -129
  72. package/dist/commands/sources/source-add.js +62 -12
  73. package/dist/commands/sources/source-manage.js +9 -2
  74. package/dist/commands/sources/stash-cli.js +1 -1
  75. package/dist/commands/tasks/explain.js +10 -13
  76. package/dist/commands/tasks/tasks-cli.js +9 -8
  77. package/dist/commands/tasks/tasks.js +326 -930
  78. package/dist/commands/tasks/validate.js +42 -21
  79. package/dist/commands/workflow/plan.js +22 -29
  80. package/dist/commands/workflow-cli.js +4 -4
  81. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  82. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  83. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  84. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  85. package/dist/core/adapter/execution-source.js +17 -29
  86. package/dist/core/asset/asset-placement.js +4 -13
  87. package/dist/core/asset/resolve-ref.js +1 -1
  88. package/dist/core/bundle-id.js +42 -5
  89. package/dist/core/bundle-rename.js +291 -0
  90. package/dist/core/config/config-io.js +1 -2
  91. package/dist/core/config/config-schema.js +1 -33
  92. package/dist/core/config/config-walker.js +1 -1
  93. package/dist/core/config/config.js +163 -68
  94. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  95. package/dist/core/config/schema/embedding.js +20 -5
  96. package/dist/core/config/schema/engines.js +5 -0
  97. package/dist/core/config/schema/execution.js +1 -1
  98. package/dist/core/config/schema/experimental.js +1 -1
  99. package/dist/core/config/schema/improve-processes.js +21 -95
  100. package/dist/core/config/schema/improve.js +4 -42
  101. package/dist/core/config/schema/scheduler.js +12 -12
  102. package/dist/core/config/schema/search.js +6 -22
  103. package/dist/core/env-secret-ref.js +0 -1
  104. package/dist/core/errors.js +8 -9
  105. package/dist/core/file-lock.js +76 -173
  106. package/dist/core/logs-db.js +2 -2
  107. package/dist/core/paths.js +0 -27
  108. package/dist/core/redaction.js +109 -2
  109. package/dist/core/run-lock.js +2 -5
  110. package/dist/core/spawn-env.js +1 -1
  111. package/dist/core/state/migrations.js +108 -61
  112. package/dist/core/state-db-scope.js +2 -4
  113. package/dist/core/state-db.js +126 -692
  114. package/dist/core/type-presentation.js +1 -9
  115. package/dist/core/write-source.js +293 -1012
  116. package/dist/execution/input-contract.js +1 -1
  117. package/dist/execution/resolved-request.js +135 -689
  118. package/dist/execution/source.js +63 -257
  119. package/dist/execution/target-ref.js +1 -1
  120. package/dist/indexer/bundle-identity-guard.js +2 -2
  121. package/dist/indexer/db/graph-db.js +106 -46
  122. package/dist/indexer/ensure-index.js +44 -85
  123. package/dist/indexer/graph/graph-extraction.js +340 -562
  124. package/dist/indexer/graph/graph-related.js +130 -0
  125. package/dist/indexer/index-rebuild-lock.js +3 -11
  126. package/dist/indexer/index-writer-lock.js +8 -17
  127. package/dist/indexer/index-written-assets.js +139 -151
  128. package/dist/indexer/indexer.js +524 -846
  129. package/dist/indexer/materialize-embeddings.js +60 -397
  130. package/dist/indexer/passes/memory-inference.js +81 -90
  131. package/dist/indexer/passes/metadata.js +132 -200
  132. package/dist/indexer/read-preflight.js +0 -7
  133. package/dist/indexer/scan/doc-to-entry.js +1 -3
  134. package/dist/indexer/scan/drain-dir.js +1 -1
  135. package/dist/indexer/search/db-search.js +181 -590
  136. package/dist/indexer/search/fts-query.js +30 -41
  137. package/dist/indexer/search/ranking.js +28 -154
  138. package/dist/indexer/search/search-attribution.js +12 -32
  139. package/dist/indexer/search/search-fields.js +11 -15
  140. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  141. package/dist/indexer/search/search-source.js +1 -4
  142. package/dist/indexer/usage/usage-events.js +2 -7
  143. package/dist/integrations/agent/engine-fallback.js +23 -40
  144. package/dist/integrations/agent/engine-resolution.js +93 -183
  145. package/dist/integrations/agent/execution.js +507 -0
  146. package/dist/integrations/agent/model-map.js +28 -156
  147. package/dist/integrations/agent/request-lowering.js +66 -141
  148. package/dist/integrations/agent/runner-dispatch.js +143 -321
  149. package/dist/integrations/agent/runner.js +54 -14
  150. package/dist/integrations/lockfile.js +53 -101
  151. package/dist/llm/embedders/deterministic.js +2 -3
  152. package/dist/llm/embedders/profile.js +71 -0
  153. package/dist/llm/embedders/remote.js +10 -15
  154. package/dist/llm/graph-extract.js +3 -12
  155. package/dist/llm/index-passes.js +3 -5
  156. package/dist/llm/memory-infer.js +1 -2
  157. package/dist/llm/metadata-enhance.js +1 -2
  158. package/dist/llm/structured-call.js +5 -24
  159. package/dist/output/generic-render.js +23 -11
  160. package/dist/output/html-render.js +13 -10
  161. package/dist/output/render-registry.js +3 -32
  162. package/dist/output/shapes/helpers.js +2 -34
  163. package/dist/output/shapes/passthrough.js +1 -9
  164. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  165. package/dist/output/text/command-format.js +60 -23
  166. package/dist/output/text/helpers.js +1 -1
  167. package/dist/output/text/migrate.js +5 -14
  168. package/dist/output/text/proposal-format.js +1 -2
  169. package/dist/output/text/workflow-format.js +0 -32
  170. package/dist/output/text.js +2 -0
  171. package/dist/registry/factory.js +4 -19
  172. package/dist/registry/network.js +66 -220
  173. package/dist/registry/providers/index.js +0 -2
  174. package/dist/registry/providers/skills-sh.js +3 -14
  175. package/dist/registry/providers/static-index.js +24 -26
  176. package/dist/registry/resolve.js +55 -131
  177. package/dist/scripts/akm-migrate-node.js +43940 -93320
  178. package/dist/scripts/akm-migrate.js +43700 -93078
  179. package/dist/setup/registry-stash-loader.js +4 -13
  180. package/dist/setup/semantic-assets.js +3 -44
  181. package/dist/setup/setup.js +1 -1
  182. package/dist/setup/steps/tasks.js +25 -15
  183. package/dist/sources/provider-factory.js +17 -18
  184. package/dist/sources/providers/filesystem.js +2 -3
  185. package/dist/sources/providers/git-install.js +7 -1
  186. package/dist/sources/providers/git-provider.js +0 -3
  187. package/dist/sources/providers/git-stash.js +0 -17
  188. package/dist/sources/providers/npm.js +2 -4
  189. package/dist/sources/providers/provider-utils.js +5 -10
  190. package/dist/sources/providers/website.js +0 -2
  191. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  192. package/dist/sources/website-url.js +2 -2
  193. package/dist/storage/database.js +9 -35
  194. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  195. package/dist/storage/repositories/index-connection.js +34 -70
  196. package/dist/storage/repositories/index-entries-repository.js +69 -111
  197. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  198. package/dist/storage/repositories/index-entry-schema.js +83 -269
  199. package/dist/storage/repositories/index-fts-repository.js +86 -256
  200. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  201. package/dist/storage/repositories/index-meta-repository.js +6 -4
  202. package/dist/storage/repositories/index-schema.js +192 -220
  203. package/dist/storage/repositories/index-utility-repository.js +8 -29
  204. package/dist/storage/repositories/index-vec-repository.js +133 -414
  205. package/dist/storage/repositories/outcome-repository.js +2 -1
  206. package/dist/storage/repositories/proposals-repository.js +35 -0
  207. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  208. package/dist/storage/repositories/task-history-repository.js +26 -4
  209. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  210. package/dist/storage/sqlite-migrations.js +136 -0
  211. package/dist/storage/sqlite-pragmas.js +11 -9
  212. package/dist/storage/sqlite-transaction.js +170 -0
  213. package/dist/storage/state-db-integrity.js +34 -27
  214. package/dist/tasks/activation-config.js +134 -62
  215. package/dist/tasks/backends/cron.js +129 -277
  216. package/dist/tasks/backends/exec-utils.js +2 -5
  217. package/dist/tasks/backends/launchd.js +125 -745
  218. package/dist/tasks/backends/schtasks.js +101 -620
  219. package/dist/tasks/prepare/prepare-support.js +5 -15
  220. package/dist/tasks/prepare/prepare.js +0 -2
  221. package/dist/tasks/resolve-akm-bin.js +20 -79
  222. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  223. package/dist/tasks/scheduler-binding.js +18 -238
  224. package/dist/tasks/scheduler-invocation.js +52 -52
  225. package/dist/tasks/scheduler-lock.js +53 -0
  226. package/dist/tasks/scheduler-sync.js +361 -751
  227. package/dist/tasks/source/parse-task-source.js +160 -10
  228. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  229. package/dist/tasks/source/task-to-v4.js +2 -2
  230. package/dist/workflows/authoring/authoring.js +3 -12
  231. package/dist/workflows/compile.js +211 -0
  232. package/dist/workflows/concurrency-policy.js +13 -74
  233. package/dist/workflows/exec/child-invocation.js +3 -17
  234. package/dist/workflows/exec/child-workflow.js +32 -141
  235. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  236. package/dist/workflows/exec/environment.js +98 -0
  237. package/dist/workflows/exec/exec-unit.js +33 -140
  238. package/dist/workflows/exec/frozen-judge.js +7 -59
  239. package/dist/workflows/exec/native-executor.js +82 -341
  240. package/dist/workflows/exec/param-secrets.js +29 -47
  241. package/dist/workflows/exec/run-workflow.js +154 -387
  242. package/dist/workflows/exec/scheduler.js +9 -36
  243. package/dist/workflows/exec/step-work.js +127 -430
  244. package/dist/workflows/exec/unit-dispatch.js +11 -63
  245. package/dist/workflows/exec/unit-writer.js +8 -52
  246. package/dist/workflows/exec/worktree.js +39 -273
  247. package/dist/workflows/freeze/child-output-references.js +4 -15
  248. package/dist/workflows/freeze/environment.js +99 -92
  249. package/dist/workflows/freeze/freeze.js +172 -0
  250. package/dist/workflows/freeze/step-values.js +19 -21
  251. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  252. package/dist/workflows/freeze/targets/command.js +10 -33
  253. package/dist/workflows/freeze/targets/script.js +5 -12
  254. package/dist/workflows/freeze/targets/shell.js +3 -6
  255. package/dist/workflows/freeze/targets/task.js +25 -80
  256. package/dist/workflows/freeze/task-bindings.js +20 -67
  257. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  258. package/dist/workflows/ir/params.js +6 -51
  259. package/dist/workflows/ir/plan-hash.js +2 -34
  260. package/dist/workflows/parser.js +140 -43
  261. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  262. package/dist/workflows/renderer.js +36 -69
  263. package/dist/workflows/resource-limits.js +12 -120
  264. package/dist/workflows/runtime/agent-identity.js +8 -40
  265. package/dist/workflows/runtime/run-outputs.js +3 -6
  266. package/dist/workflows/runtime/run-plan.js +316 -0
  267. package/dist/workflows/runtime/runs.js +48 -200
  268. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  269. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  270. package/dist/workflows/validate-summary.js +2 -7
  271. package/docs/integration/bundling-akm.md +49 -42
  272. package/docs/migration/README.md +1 -0
  273. package/docs/migration/release-notes/0.9.17.md +41 -0
  274. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  275. package/docs/reference/cli.md +182 -125
  276. package/docs/reference/configuration.md +49 -56
  277. package/docs/reference/data-and-telemetry.md +19 -20
  278. package/docs/reference/tasks.md +86 -38
  279. package/docs/reference/workflow-schema.md +14 -18
  280. package/docs/reference/workflows.md +6 -9
  281. package/package.json +1 -1
  282. package/schemas/akm-config.json +87 -406
  283. package/dist/commands/health/advisories.js +0 -150
  284. package/dist/commands/health/metrics.js +0 -329
  285. package/dist/commands/health/surfaces.js +0 -102
  286. package/dist/commands/improve/anti-collapse.js +0 -83
  287. package/dist/commands/improve/collapse-detector.js +0 -432
  288. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  289. package/dist/commands/improve/consolidate/merge.js +0 -146
  290. package/dist/commands/improve/distill/promote-memory.js +0 -329
  291. package/dist/commands/improve/distill/quality-gate.js +0 -500
  292. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  293. package/dist/commands/improve/proposal-envelope.js +0 -31
  294. package/dist/commands/improve/run-context.js +0 -123
  295. package/dist/commands/improve/shared.js +0 -21
  296. package/dist/commands/improve/source-identity.js +0 -28
  297. package/dist/commands/improve/triage.js +0 -96
  298. package/dist/commands/proposal/drain-policies.js +0 -151
  299. package/dist/commands/sources/update-transaction.js +0 -220
  300. package/dist/core/action-contributors.js +0 -28
  301. package/dist/core/config/config-version-shim.js +0 -101
  302. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  303. package/dist/core/fs-txn.js +0 -405
  304. package/dist/core/lexical-score.js +0 -25
  305. package/dist/core/maintenance-barrier.js +0 -167
  306. package/dist/execution/executable-identity.js +0 -105
  307. package/dist/execution/guarded-source.js +0 -441
  308. package/dist/indexer/graph/graph-boost.js +0 -427
  309. package/dist/indexer/graph/graph-dedup.js +0 -95
  310. package/dist/indexer/search/name-match.js +0 -35
  311. package/dist/indexer/search/ranking-contributors.js +0 -515
  312. package/dist/indexer/walk/project-context.js +0 -192
  313. package/dist/integrations/agent/execution-cascade.js +0 -566
  314. package/dist/integrations/agent/execution-definitions.js +0 -202
  315. package/dist/integrations/agent/execution-lowering.js +0 -841
  316. package/dist/integrations/agent/execution-preparation.js +0 -98
  317. package/dist/integrations/agent/inline-execution.js +0 -74
  318. package/dist/registry/create-provider-registry.js +0 -29
  319. package/dist/registry/pinned-request-helper.js +0 -247
  320. package/dist/registry/pinned-transport.js +0 -717
  321. package/dist/sources/providers/index.js +0 -14
  322. package/dist/storage/engines/sqlite-migrations.js +0 -271
  323. package/dist/storage/repositories/canaries-repository.js +0 -107
  324. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  325. package/dist/storage/repositories/registry-cache.js +0 -113
  326. package/dist/tasks/scheduler-sync-preview.js +0 -52
  327. package/dist/workflows/freeze/resolve-steps.js +0 -86
  328. package/dist/workflows/freeze/source-freeze.js +0 -64
  329. package/dist/workflows/ir/compile.js +0 -321
  330. package/dist/workflows/ir/environment-v4.js +0 -330
  331. package/dist/workflows/ir/freeze-v4.js +0 -153
  332. package/dist/workflows/ir/schema-v4.js +0 -745
  333. package/dist/workflows/ir/schema.js +0 -354
  334. package/dist/workflows/program/schema.js +0 -78
  335. package/dist/workflows/runtime/checkin.js +0 -57
  336. package/dist/workflows/runtime/plan-classifier.js +0 -196
  337. package/dist/workflows/runtime/unit-checkin.js +0 -45
  338. package/dist/workflows/runtime/unit-phases.js +0 -20
  339. package/dist/workflows/schema.js +0 -4
  340. package/dist/workflows/source-ir/compile.js +0 -200
  341. package/dist/workflows/source-ir/program.js +0 -50
  342. package/dist/workflows/source-ir/result.js +0 -26
  343. package/dist/workflows/source-ir/schema.js +0 -786
  344. package/dist/workflows/source-ir/triggers.js +0 -79
  345. package/dist/workflows/source-ir/uses.js +0 -40
  346. package/dist/workflows/validator.js +0 -60
@@ -11,12 +11,13 @@
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.
20
21
  *
21
22
  * Tests inject a fake exec + filesystem so the backend can be unit-tested
22
23
  * without touching the host launchctl.
@@ -31,14 +32,11 @@ import { getTaskLogDir } from "../../core/paths.js";
31
32
  import { warn } from "../../core/warn.js";
32
33
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
33
34
  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 { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
35
36
  import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
36
37
  import { escapeXml, nodeExec, nodeFs, runOrThrow } from "./exec-utils.js";
37
38
  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
39
  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
40
  export function LAUNCHD_BACKEND(options = {}) {
43
41
  const exec = options.exec ?? defaultLaunchdExec();
44
42
  const fsLike = options.fs ?? defaultLaunchdFs();
@@ -46,691 +44,106 @@ export function LAUNCHD_BACKEND(options = {}) {
46
44
  const logDir = options.logDir ?? getTaskLogDir();
47
45
  const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
48
46
  const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
47
+ const envPath = options.envPath === false ? undefined : typeof options.envPath === "string" ? options.envPath : process.env.PATH;
48
+ const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext));
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?.contextPath ?? defaultContextPath, 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
+ contextPath: parsed.contextPath,
134
+ invocation: parsed.invocation,
135
+ });
154
136
  }
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);
137
+ return rows;
168
138
  },
169
139
  expectedSignature(task, opts) {
170
- return launchdFingerprint(buildPlistXml(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target), task.enabled, true);
140
+ return launchdFingerprint(xmlFor(task, opts), task.enabled, true);
171
141
  },
172
142
  };
173
143
  }
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
144
  function launchdFingerprint(raw, enabled, loaded) {
713
145
  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
- });
146
+ return `${signed.replace(/\r\n/g, "\n").trim()}:loaded=${loaded}`;
734
147
  }
735
148
  export function extractPlistInvocation(xml) {
736
149
  const block = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
@@ -748,25 +161,31 @@ function decodeXmlEntities(value) {
748
161
  .replaceAll("&amp;", "&");
749
162
  }
750
163
  // ── 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);
164
+ function renderPlistEnvironment(envPath) {
165
+ if (!envPath)
166
+ return "";
167
+ return [
168
+ " <key>EnvironmentVariables</key>",
169
+ " <dict>",
170
+ " <key>PATH</key>",
171
+ ` <string>${escapeXml(envPath)}</string>`,
172
+ " </dict>",
173
+ "",
174
+ ].join("\n");
175
+ }
176
+ export function buildPlistXml(task, akmArgv, logDir, contextPath, envPath) {
177
+ const trigger = translateToLaunchd(parseSchedule(task.cron, "launchd"));
754
178
  const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
755
- const argv = invocation.argv;
756
- const programArgs = argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
179
+ const programArgs = invocation.argv.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
757
180
  const nativeId = schedulerBindingNativeId(task);
758
181
  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
182
  const xml = launchdTemplate
764
- .replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n${executionEvidence}`)
183
+ .replace("<dict>\n", `<dict>\n <!-- akm-enabled:${task.enabled} -->\n`)
765
184
  .replace("{{LABEL}}", LAUNCHD_LABEL_PREFIX + escapeXml(nativeId))
766
185
  .replace("{{PROGRAM_ARGS}}", programArgs)
767
186
  .replaceAll("{{LOG_PATH}}", escapeXml(logPath))
768
- .replace("{{ENV_VARS}}", "")
769
- .replace("{{TRIGGER_XML}}", triggerXml);
187
+ .replace("{{ENV_VARS}}", renderPlistEnvironment(envPath))
188
+ .replace("{{TRIGGER_XML}}", renderLaunchdTrigger(trigger));
770
189
  for (const char of xml) {
771
190
  const code = char.codePointAt(0) ?? 0;
772
191
  if (code <= 0x08 || code === 0x0b || code === 0x0c || (code >= 0x0e && code <= 0x1f)) {
@@ -778,22 +197,18 @@ export function buildPlistXml(task, akmArgv, logDir, contextPath, _target) {
778
197
  function renderLaunchdTrigger(trigger) {
779
198
  if (trigger.calendars !== undefined) {
780
199
  const lines = [" <key>StartCalendarInterval</key>", " <array>"];
781
- for (const calendar of trigger.calendars) {
200
+ for (const calendar of trigger.calendars)
782
201
  lines.push(...renderCalendar(calendar, " "));
783
- }
784
202
  lines.push(" </array>");
785
203
  return lines.join("\n");
786
204
  }
787
- const cal = trigger.calendar ?? {};
788
- const lines = [" <key>StartCalendarInterval</key>", ...renderCalendar(cal, " ")];
789
- return lines.join("\n");
205
+ return [" <key>StartCalendarInterval</key>", ...renderCalendar(trigger.calendar ?? {}, " ")].join("\n");
790
206
  }
791
207
  function renderCalendar(calendar, indent) {
792
208
  const valueIndent = `${indent} `;
793
209
  const lines = [`${indent}<dict>`];
794
- if (calendar.Minute !== undefined) {
210
+ if (calendar.Minute !== undefined)
795
211
  lines.push(`${valueIndent}<key>Minute</key><integer>${calendar.Minute}</integer>`);
796
- }
797
212
  if (calendar.Hour !== undefined)
798
213
  lines.push(`${valueIndent}<key>Hour</key><integer>${calendar.Hour}</integer>`);
799
214
  if (calendar.Day !== undefined)
@@ -806,49 +221,25 @@ function renderCalendar(calendar, indent) {
806
221
  lines.push(`${indent}</dict>`);
807
222
  return lines;
808
223
  }
809
- function normalizeSignature(xml) {
810
- return xml.replace(/\r\n/g, "\n").trim();
811
- }
812
224
  /**
813
225
  * Collect the akm-owned service labels present in `launchctl` output.
814
226
  *
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`.
227
+ * This reads the CURRENT USER'S OWN scheduler inventory, so it is parsed
228
+ * permissively: scan for labels in our own `com.akm.task.` namespace and
229
+ * ignore everything else.
831
230
  */
832
231
  export function parseLaunchdLoadedLabels(output) {
833
232
  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;
233
+ // `[^\s"{}=,()]` stops the token at whatever punctuation the surrounding
234
+ // launchctl syntax uses, so a label works whether it appears as a bare
235
+ // table cell, a quoted string, or a dictionary key.
236
+ for (const match of output.matchAll(/com\.akm\.task\.[^\s"{}=,()]+/gu)) {
237
+ if (LAUNCHD_AKM_LABEL_RE.test(match[0]))
238
+ labels.add(match[0]);
846
239
  }
847
240
  return labels;
848
241
  }
849
- function boundLaunchdOutput(output) {
850
- return output.length > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES ? output.slice(0, MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES) : output;
851
- }
242
+ /** `print-disabled` failing or looking unfamiliar degrades to "nothing is disabled". */
852
243
  function readDisabledLabels(exec) {
853
244
  try {
854
245
  const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
@@ -856,35 +247,25 @@ function readDisabledLabels(exec) {
856
247
  warn("[akm] launchctl print-disabled exited %d; assuming no akm task is disabled.", result.status);
857
248
  return new Set();
858
249
  }
859
- return parseDisabledLabels(result.stdout);
250
+ const disabled = new Set();
251
+ const entryPattern = /"(com\.akm\.task\.[^"\r\n]+)"\s*=>\s*(true|false|enabled|disabled)/gu;
252
+ for (const match of result.stdout.matchAll(entryPattern)) {
253
+ const label = match[1];
254
+ if (LAUNCHD_AKM_LABEL_RE.test(label) && (match[2] === "true" || match[2] === "disabled"))
255
+ disabled.add(label);
256
+ }
257
+ return disabled;
860
258
  }
861
259
  catch (error) {
862
260
  warn("[akm] launchctl print-disabled could not be run; assuming no akm task is disabled: %s", error instanceof Error ? error.message : String(error));
863
261
  return new Set();
864
262
  }
865
263
  }
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
264
  function isServiceNotFoundResult(result) {
881
265
  const output = `${result.stderr ?? ""}\n${result.stdout ?? ""}`;
882
266
  return /could not find service\b|service\b.*\bnot found\b|\bno such process\b/i.test(output);
883
267
  }
884
268
  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
269
  const home = os.homedir();
889
270
  if (!home) {
890
271
  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 +298,8 @@ function defaultLaunchdFs() {
917
298
  return fs.readdirSync(dir);
918
299
  }
919
300
  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.
301
+ // Genuinely absent is an empty listing; anything else (e.g. EACCES)
302
+ // is not "no plists".
923
303
  if (hasErrnoCode(error, "ENOENT"))
924
304
  return [];
925
305
  throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");