akm-cli 0.7.4 → 0.8.0-rc.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 (300) hide show
  1. package/CHANGELOG.md +224 -1
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/cli/config-migrate.js +144 -0
  5. package/dist/cli/config-validate.js +39 -0
  6. package/dist/cli/confirm.js +73 -0
  7. package/dist/cli/parse-args.js +133 -0
  8. package/dist/cli/shared.js +129 -0
  9. package/dist/cli.js +2631 -1440
  10. package/dist/commands/add-cli.js +279 -0
  11. package/dist/commands/agent-dispatch.js +110 -0
  12. package/dist/commands/agent-support.js +68 -0
  13. package/dist/commands/completions.js +3 -0
  14. package/dist/commands/config-cli.js +130 -534
  15. package/dist/commands/consolidate.js +2122 -0
  16. package/dist/commands/curate.js +45 -3
  17. package/dist/commands/db-cli.js +23 -0
  18. package/dist/commands/distill-promotion-policy.js +660 -0
  19. package/dist/commands/distill.js +1081 -73
  20. package/dist/commands/env.js +213 -0
  21. package/dist/commands/eval-cases.js +43 -0
  22. package/dist/commands/events.js +15 -24
  23. package/dist/commands/extract-cli.js +127 -0
  24. package/dist/commands/extract-prompt.js +204 -0
  25. package/dist/commands/extract.js +477 -0
  26. package/dist/commands/feedback-cli.js +331 -0
  27. package/dist/commands/graph.js +477 -0
  28. package/dist/commands/health.js +1302 -0
  29. package/dist/commands/help/help-accept.md +12 -0
  30. package/dist/commands/help/help-improve.md +69 -0
  31. package/dist/commands/help/help-proposals.md +18 -0
  32. package/dist/commands/help/help-propose.md +17 -0
  33. package/dist/commands/help/help-reject.md +11 -0
  34. package/dist/commands/history.js +54 -46
  35. package/dist/commands/improve-auto-accept.js +97 -0
  36. package/dist/commands/improve-cli.js +217 -0
  37. package/dist/commands/improve-profiles.js +166 -0
  38. package/dist/commands/improve-result-file.js +167 -0
  39. package/dist/commands/improve.js +2373 -0
  40. package/dist/commands/info.js +5 -2
  41. package/dist/commands/init.js +50 -2
  42. package/dist/commands/installed-stashes.js +102 -139
  43. package/dist/commands/knowledge.js +136 -0
  44. package/dist/commands/lint/agent-linter.js +49 -0
  45. package/dist/commands/lint/base-linter.js +479 -0
  46. package/dist/commands/lint/command-linter.js +49 -0
  47. package/dist/commands/lint/default-linter.js +16 -0
  48. package/dist/commands/lint/env-key-rules.js +154 -0
  49. package/dist/commands/lint/index.js +196 -0
  50. package/dist/commands/lint/knowledge-linter.js +16 -0
  51. package/dist/commands/lint/markdown-insertion.js +343 -0
  52. package/dist/commands/lint/memory-linter.js +61 -0
  53. package/dist/commands/lint/registry.js +36 -0
  54. package/dist/commands/lint/skill-linter.js +45 -0
  55. package/dist/commands/lint/task-linter.js +50 -0
  56. package/dist/commands/lint/types.js +4 -0
  57. package/dist/commands/lint/workflow-linter.js +56 -0
  58. package/dist/commands/lint.js +4 -0
  59. package/dist/commands/migration-help.js +3 -0
  60. package/dist/commands/proposal.js +67 -12
  61. package/dist/commands/propose.js +120 -45
  62. package/dist/commands/reflect.js +1104 -60
  63. package/dist/commands/registry-cli.js +150 -0
  64. package/dist/commands/registry-search.js +5 -2
  65. package/dist/commands/remember-cli.js +257 -0
  66. package/dist/commands/remember.js +70 -7
  67. package/dist/commands/schema-repair.js +203 -0
  68. package/dist/commands/search.js +115 -14
  69. package/dist/commands/secret.js +173 -0
  70. package/dist/commands/self-update.js +3 -0
  71. package/dist/commands/show.js +158 -60
  72. package/dist/commands/source-add.js +17 -45
  73. package/dist/commands/source-clone.js +3 -0
  74. package/dist/commands/source-manage.js +14 -19
  75. package/dist/commands/tasks.js +437 -0
  76. package/dist/commands/url-checker.js +42 -0
  77. package/dist/core/action-contributors.js +28 -0
  78. package/dist/core/asset-ref.js +17 -2
  79. package/dist/core/asset-registry.js +12 -17
  80. package/dist/core/asset-serialize.js +88 -0
  81. package/dist/core/asset-spec.js +67 -1
  82. package/dist/core/common.js +182 -0
  83. package/dist/core/concurrent.js +25 -0
  84. package/dist/core/config-io.js +347 -0
  85. package/dist/core/config-migration.js +622 -0
  86. package/dist/core/config-schema.js +534 -0
  87. package/dist/core/config-sources.js +108 -0
  88. package/dist/core/config-types.js +4 -0
  89. package/dist/core/config-walker.js +337 -0
  90. package/dist/core/config.js +364 -968
  91. package/dist/core/errors.js +42 -20
  92. package/dist/core/events.js +105 -135
  93. package/dist/core/file-lock.js +104 -0
  94. package/dist/core/frontmatter.js +75 -8
  95. package/dist/core/lesson-lint.js +3 -0
  96. package/dist/core/markdown.js +20 -0
  97. package/dist/core/memory-belief.js +62 -0
  98. package/dist/core/memory-contradiction-detect.js +274 -0
  99. package/dist/core/memory-improve.js +806 -0
  100. package/dist/core/parse.js +158 -0
  101. package/dist/core/paths.js +280 -14
  102. package/dist/core/proposal-quality-validators.js +380 -0
  103. package/dist/core/proposal-validators.js +69 -0
  104. package/dist/core/proposals.js +512 -42
  105. package/dist/core/state-db.js +1068 -0
  106. package/dist/core/text-truncation.js +107 -0
  107. package/dist/core/time.js +54 -0
  108. package/dist/core/tty.js +59 -0
  109. package/dist/core/warn.js +64 -1
  110. package/dist/core/write-source.js +3 -0
  111. package/dist/indexer/db-backup.js +391 -0
  112. package/dist/indexer/db-search.js +198 -489
  113. package/dist/indexer/db.js +990 -108
  114. package/dist/indexer/ensure-index.js +136 -0
  115. package/dist/indexer/file-context.js +3 -0
  116. package/dist/indexer/graph-boost.js +376 -101
  117. package/dist/indexer/graph-db.js +391 -0
  118. package/dist/indexer/graph-dedup.js +95 -0
  119. package/dist/indexer/graph-extraction.js +550 -114
  120. package/dist/indexer/index-context.js +4 -0
  121. package/dist/indexer/indexer.js +547 -309
  122. package/dist/indexer/llm-cache.js +52 -0
  123. package/dist/indexer/manifest.js +3 -0
  124. package/dist/indexer/matchers.js +167 -160
  125. package/dist/indexer/memory-inference.js +152 -74
  126. package/dist/indexer/metadata-contributors.js +29 -0
  127. package/dist/indexer/metadata.js +275 -196
  128. package/dist/indexer/path-resolver.js +92 -0
  129. package/dist/indexer/project-context.js +192 -0
  130. package/dist/indexer/ranking-contributors.js +331 -0
  131. package/dist/indexer/ranking.js +81 -0
  132. package/dist/indexer/search-fields.js +5 -9
  133. package/dist/indexer/search-hit-enrichers.js +111 -0
  134. package/dist/indexer/search-source.js +44 -10
  135. package/dist/indexer/semantic-status.js +6 -17
  136. package/dist/indexer/staleness-detect.js +447 -0
  137. package/dist/indexer/usage-events.js +12 -9
  138. package/dist/indexer/walker.js +28 -0
  139. package/dist/integrations/agent/builders.js +135 -0
  140. package/dist/integrations/agent/config.js +122 -230
  141. package/dist/integrations/agent/detect.js +3 -0
  142. package/dist/integrations/agent/index.js +7 -13
  143. package/dist/integrations/agent/model-aliases.js +55 -0
  144. package/dist/integrations/agent/profiles.js +70 -5
  145. package/dist/integrations/agent/prompts.js +250 -36
  146. package/dist/integrations/agent/runner.js +151 -0
  147. package/dist/integrations/agent/sdk-runner.js +126 -0
  148. package/dist/integrations/agent/spawn.js +183 -35
  149. package/dist/integrations/github.js +3 -0
  150. package/dist/integrations/lockfile.js +32 -69
  151. package/dist/integrations/session-logs/index.js +69 -0
  152. package/dist/integrations/session-logs/inline-refs.js +35 -0
  153. package/dist/integrations/session-logs/pre-filter.js +152 -0
  154. package/dist/integrations/session-logs/providers/claude-code.js +282 -0
  155. package/dist/integrations/session-logs/providers/opencode.js +258 -0
  156. package/dist/integrations/session-logs/types.js +4 -0
  157. package/dist/llm/call-ai.js +62 -0
  158. package/dist/llm/client.js +79 -88
  159. package/dist/llm/embedder.js +20 -29
  160. package/dist/llm/embedders/cache.js +3 -7
  161. package/dist/llm/embedders/local.js +42 -1
  162. package/dist/llm/embedders/remote.js +20 -8
  163. package/dist/llm/embedders/types.js +3 -7
  164. package/dist/llm/feature-gate.js +95 -48
  165. package/dist/llm/graph-extract.js +676 -72
  166. package/dist/llm/index-passes.js +44 -29
  167. package/dist/llm/memory-infer.js +80 -71
  168. package/dist/llm/metadata-enhance.js +42 -29
  169. package/dist/llm/prompts/extract-session.md +80 -0
  170. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  171. package/dist/output/cli-hints-full.md +292 -0
  172. package/dist/output/cli-hints-short.md +66 -0
  173. package/dist/output/cli-hints.js +7 -311
  174. package/dist/output/context.js +60 -8
  175. package/dist/output/renderers.js +306 -258
  176. package/dist/output/shapes/curate.js +56 -0
  177. package/dist/output/shapes/distill.js +10 -0
  178. package/dist/output/shapes/env-list.js +19 -0
  179. package/dist/output/shapes/events.js +11 -0
  180. package/dist/output/shapes/helpers.js +424 -0
  181. package/dist/output/shapes/history.js +7 -0
  182. package/dist/output/shapes/passthrough.js +102 -0
  183. package/dist/output/shapes/proposal-accept.js +7 -0
  184. package/dist/output/shapes/proposal-diff.js +7 -0
  185. package/dist/output/shapes/proposal-list.js +7 -0
  186. package/dist/output/shapes/proposal-producer.js +11 -0
  187. package/dist/output/shapes/proposal-reject.js +7 -0
  188. package/dist/output/shapes/proposal-show.js +7 -0
  189. package/dist/output/shapes/registry-search.js +6 -0
  190. package/dist/output/shapes/registry.js +30 -0
  191. package/dist/output/shapes/search.js +6 -0
  192. package/dist/output/shapes/secret-list.js +19 -0
  193. package/dist/output/shapes/show.js +6 -0
  194. package/dist/output/shapes/vault-list.js +19 -0
  195. package/dist/output/shapes.js +51 -511
  196. package/dist/output/text/add.js +6 -0
  197. package/dist/output/text/clone.js +6 -0
  198. package/dist/output/text/config.js +6 -0
  199. package/dist/output/text/curate.js +6 -0
  200. package/dist/output/text/distill.js +7 -0
  201. package/dist/output/text/enable-disable.js +7 -0
  202. package/dist/output/text/events.js +10 -0
  203. package/dist/output/text/feedback.js +6 -0
  204. package/dist/output/text/helpers.js +1039 -0
  205. package/dist/output/text/history.js +7 -0
  206. package/dist/output/text/import.js +6 -0
  207. package/dist/output/text/index.js +6 -0
  208. package/dist/output/text/info.js +6 -0
  209. package/dist/output/text/init.js +6 -0
  210. package/dist/output/text/list.js +6 -0
  211. package/dist/output/text/proposal-producer.js +8 -0
  212. package/dist/output/text/proposal.js +11 -0
  213. package/dist/output/text/registry-commands.js +11 -0
  214. package/dist/output/text/registry.js +30 -0
  215. package/dist/output/text/remember.js +6 -0
  216. package/dist/output/text/remove.js +6 -0
  217. package/dist/output/text/save.js +6 -0
  218. package/dist/output/text/search.js +6 -0
  219. package/dist/output/text/show.js +6 -0
  220. package/dist/output/text/update.js +6 -0
  221. package/dist/output/text/upgrade.js +6 -0
  222. package/dist/output/text/vault.js +16 -0
  223. package/dist/output/text/wiki.js +15 -0
  224. package/dist/output/text/workflow.js +14 -0
  225. package/dist/output/text.js +44 -1093
  226. package/dist/registry/build-index.js +3 -0
  227. package/dist/registry/create-provider-registry.js +3 -0
  228. package/dist/registry/factory.js +4 -1
  229. package/dist/registry/origin-resolve.js +3 -0
  230. package/dist/registry/providers/index.js +3 -0
  231. package/dist/registry/providers/skills-sh.js +71 -50
  232. package/dist/registry/providers/static-index.js +53 -48
  233. package/dist/registry/providers/types.js +3 -24
  234. package/dist/registry/resolve.js +11 -16
  235. package/dist/registry/types.js +3 -0
  236. package/dist/scripts/migrate-storage.js +17750 -0
  237. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  238. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  239. package/dist/setup/detect.js +3 -0
  240. package/dist/setup/ripgrep-install.js +3 -0
  241. package/dist/setup/ripgrep-resolve.js +3 -0
  242. package/dist/setup/setup.js +775 -37
  243. package/dist/setup/steps.js +3 -15
  244. package/dist/sources/include.js +3 -0
  245. package/dist/sources/provider-factory.js +5 -12
  246. package/dist/sources/provider.js +3 -20
  247. package/dist/sources/providers/filesystem.js +19 -23
  248. package/dist/sources/providers/git.js +179 -20
  249. package/dist/sources/providers/index.js +3 -0
  250. package/dist/sources/providers/install-types.js +3 -13
  251. package/dist/sources/providers/npm.js +3 -4
  252. package/dist/sources/providers/provider-utils.js +3 -0
  253. package/dist/sources/providers/sync-from-ref.js +3 -11
  254. package/dist/sources/providers/tar-utils.js +3 -0
  255. package/dist/sources/providers/website.js +18 -22
  256. package/dist/sources/resolve.js +3 -0
  257. package/dist/sources/types.js +3 -0
  258. package/dist/sources/website-ingest.js +7 -0
  259. package/dist/tasks/backends/cron.js +203 -0
  260. package/dist/tasks/backends/exec-utils.js +28 -0
  261. package/dist/tasks/backends/index.js +24 -0
  262. package/dist/tasks/backends/launchd-template.xml +19 -0
  263. package/dist/tasks/backends/launchd.js +187 -0
  264. package/dist/tasks/backends/schtasks-template.xml +29 -0
  265. package/dist/tasks/backends/schtasks.js +215 -0
  266. package/dist/tasks/parser.js +211 -0
  267. package/dist/tasks/resolveAkmBin.js +87 -0
  268. package/dist/tasks/runner.js +458 -0
  269. package/dist/tasks/schedule.js +227 -0
  270. package/dist/tasks/schema.js +15 -0
  271. package/dist/tasks/validator.js +62 -0
  272. package/dist/version.js +3 -0
  273. package/dist/wiki/index-template.md +12 -0
  274. package/dist/wiki/ingest-workflow-template.md +54 -0
  275. package/dist/wiki/log-template.md +8 -0
  276. package/dist/wiki/schema-template.md +61 -0
  277. package/dist/wiki/wiki-templates.js +15 -0
  278. package/dist/wiki/wiki.js +13 -61
  279. package/dist/workflows/authoring.js +8 -25
  280. package/dist/workflows/cli.js +3 -0
  281. package/dist/workflows/db.js +141 -2
  282. package/dist/workflows/document-cache.js +3 -10
  283. package/dist/workflows/parser.js +3 -0
  284. package/dist/workflows/renderer.js +11 -3
  285. package/dist/workflows/runs.js +91 -89
  286. package/dist/workflows/schema.js +3 -0
  287. package/dist/workflows/scope-key.js +79 -0
  288. package/dist/workflows/validator.js +4 -8
  289. package/dist/workflows/workflow-template.md +24 -0
  290. package/docs/README.md +10 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.0.md +1 -1
  293. package/docs/migration/release-notes/0.7.4.md +1 -1
  294. package/docs/migration/release-notes/0.7.5.md +20 -0
  295. package/docs/migration/release-notes/0.8.0.md +48 -0
  296. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  297. package/package.json +29 -11
  298. package/dist/commands/install-audit.js +0 -381
  299. package/dist/commands/vault.js +0 -333
  300. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,458 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * `akm tasks run <id>` — what cron / launchd / schtasks invoke at the
6
+ * scheduled moment.
7
+ *
8
+ * Responsibilities:
9
+ *
10
+ * 1. Resolve the task file via `resolveAssetPath(stashDir, "task", id)`.
11
+ * 2. Parse the task document. (Validation runs at `tasks add` /
12
+ * `tasks sync` time, not here — at run time we still want to attempt
13
+ * execution and surface the actual failure rather than re-fail on a
14
+ * validation error that the user already knows about.)
15
+ * 3. Refuse to run when `enabled === false` (defense-in-depth).
16
+ * 4. Dispatch by target kind:
17
+ * • workflow → `startWorkflowRun(ref, params)`
18
+ * • prompt → `runAgent(profile, prompt, { stdio: "captured" })`
19
+ * 5. Capture stdout / stderr to `<cacheDir>/tasks/logs/<id>/<ts>.log`.
20
+ * 6. Write a history row to state.db task_history table.
21
+ *
22
+ * Returns a structured result so the CLI handler can shape it for `output()`
23
+ * and so tests can assert against it without scraping stdout.
24
+ */
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { parseAssetRef } from "../core/asset-ref";
28
+ import { resolveStashDir } from "../core/common";
29
+ import { loadConfig } from "../core/config";
30
+ import { NotFoundError, rethrowIfTestIsolationError } from "../core/errors";
31
+ import { getTaskLogDir } from "../core/paths";
32
+ import { getTaskHistory, openStateDatabase, queryTaskHistory, upsertTaskHistory } from "../core/state-db";
33
+ import { error } from "../core/warn";
34
+ import { requireAgentProfile, runAgent } from "../integrations/agent";
35
+ import { resolveProcessAgentProfile } from "../integrations/agent/config";
36
+ import { resolveRunner } from "../integrations/agent/runner";
37
+ import { resolveAssetPath } from "../sources/resolve";
38
+ import { startWorkflowRun } from "../workflows/runs";
39
+ import { parseTaskDocument } from "./parser";
40
+ export async function runTask(id, options = {}) {
41
+ const stashDir = options.stashDir ?? resolveStashDir();
42
+ const runAgentImpl = options.runAgentImpl ?? runAgent;
43
+ const startWorkflowRunImpl = options.startWorkflowRunImpl ?? startWorkflowRun;
44
+ const now = options.now ?? (() => new Date());
45
+ const logDir = options.logDir ?? getTaskLogDir();
46
+ const filePath = await resolveAssetPath(stashDir, "task", id);
47
+ const yaml = fs.readFileSync(filePath, "utf8");
48
+ const task = parseTaskDocument({ yaml, filePath, id });
49
+ const startedAt = now();
50
+ const startedIso = startedAt.toISOString();
51
+ const tsSlug = startedIso.replace(/[:.]/g, "-");
52
+ const taskLogDir = path.join(logDir, id);
53
+ fs.mkdirSync(taskLogDir, { recursive: true });
54
+ const logPath = path.join(taskLogDir, `${tsSlug}.log`);
55
+ if (!task.enabled) {
56
+ const finishedAt = now();
57
+ const disabledTarget = task.target.kind === "workflow"
58
+ ? { kind: "workflow", ref: task.target.ref }
59
+ : task.target.kind === "command"
60
+ ? { kind: "prompt", profile: undefined }
61
+ : { kind: "prompt", profile: task.target.profile };
62
+ const result = {
63
+ id,
64
+ status: "disabled",
65
+ startedAt: startedIso,
66
+ finishedAt: finishedAt.toISOString(),
67
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
68
+ log: logPath,
69
+ target: disabledTarget,
70
+ };
71
+ fs.writeFileSync(logPath, `[akm tasks] task "${id}" is disabled — skipping run.\n`);
72
+ appendHistory(result);
73
+ return result;
74
+ }
75
+ if (task.target.kind === "workflow") {
76
+ return await runWorkflowTask({
77
+ task,
78
+ logPath,
79
+ startedAt,
80
+ now,
81
+ startWorkflowRunImpl,
82
+ });
83
+ }
84
+ if (task.target.kind === "command") {
85
+ return await runCommandTask({ task, logPath, startedAt, now });
86
+ }
87
+ // Resolve config once here so runPromptTask does not call loadConfig()
88
+ // on every dispatch in a batch run (Fix C6).
89
+ const config = loadConfig();
90
+ return await runPromptTask({
91
+ task,
92
+ stashDir,
93
+ logPath,
94
+ startedAt,
95
+ now,
96
+ runAgentImpl,
97
+ agentOptions: options.agentOptions,
98
+ agentConfig: config,
99
+ agentTimeoutMs: undefined,
100
+ });
101
+ }
102
+ // ── command target ──────────────────────────────────────────────────────────
103
+ async function runCommandTask(input) {
104
+ const { task, logPath, startedAt, now } = input;
105
+ if (task.target.kind !== "command")
106
+ throw new Error("invariant: command target");
107
+ const { cmd } = task.target;
108
+ const timeoutMs = task.timeoutMs !== undefined ? task.timeoutMs : null;
109
+ const logLines = [`[akm tasks] task=${task.id} kind=command cmd=${cmd.join(" ")}`];
110
+ let stdout = "";
111
+ let stderr = "";
112
+ let exitCode = null;
113
+ try {
114
+ const proc = Bun.spawn(cmd, {
115
+ stdin: "ignore",
116
+ stdout: "pipe",
117
+ stderr: "pipe",
118
+ cwd: process.env.HOME ?? "/tmp",
119
+ });
120
+ let timer;
121
+ let timedOut = false;
122
+ if (timeoutMs !== null) {
123
+ timer = setTimeout(() => {
124
+ timedOut = true;
125
+ try {
126
+ proc.kill("SIGTERM");
127
+ }
128
+ catch {
129
+ /* ignore */
130
+ }
131
+ }, timeoutMs);
132
+ }
133
+ const [stdoutBuf, stderrBuf] = await Promise.all([
134
+ new Response(proc.stdout).text(),
135
+ new Response(proc.stderr).text(),
136
+ ]);
137
+ await proc.exited;
138
+ if (timer !== undefined)
139
+ clearTimeout(timer);
140
+ stdout = stdoutBuf;
141
+ stderr = stderrBuf;
142
+ exitCode = proc.exitCode ?? (timedOut ? 143 : 1);
143
+ if (timedOut) {
144
+ logLines.push(`timed_out=true timeout_ms=${timeoutMs}`);
145
+ }
146
+ logLines.push(`exit_code=${exitCode}`);
147
+ if (stdout) {
148
+ logLines.push("--- stdout ---");
149
+ logLines.push(stdout);
150
+ }
151
+ if (stderr) {
152
+ logLines.push("--- stderr ---");
153
+ logLines.push(stderr);
154
+ }
155
+ }
156
+ catch (e) {
157
+ const msg = e instanceof Error ? e.message : String(e);
158
+ logLines.push(`spawn_error=${msg}`);
159
+ exitCode = 1;
160
+ }
161
+ fs.writeFileSync(logPath, `${logLines.join("\n")}\n`);
162
+ const finishedAt = now();
163
+ const status = exitCode === 0 ? "completed" : "failed";
164
+ const result = {
165
+ id: task.id,
166
+ status,
167
+ startedAt: startedAt.toISOString(),
168
+ finishedAt: finishedAt.toISOString(),
169
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
170
+ log: logPath,
171
+ target: { kind: "prompt", profile: undefined },
172
+ detail: { exitCode },
173
+ };
174
+ appendHistory(result);
175
+ return result;
176
+ }
177
+ // ── workflow target ─────────────────────────────────────────────────────────
178
+ async function runWorkflowTask(input) {
179
+ const { task, logPath, startedAt, now, startWorkflowRunImpl } = input;
180
+ if (task.target.kind !== "workflow")
181
+ throw new Error("invariant: workflow target");
182
+ const ref = parseAssetRef(task.target.ref);
183
+ if (ref.type !== "workflow") {
184
+ throw new NotFoundError(`Task "${task.id}" workflow target must be a workflow ref (got "${task.target.ref}").`, "WORKFLOW_NOT_FOUND");
185
+ }
186
+ let detail;
187
+ let error;
188
+ try {
189
+ detail = await startWorkflowRunImpl(task.target.ref, task.target.params);
190
+ }
191
+ catch (e) {
192
+ error = e instanceof Error ? e : new Error(String(e));
193
+ }
194
+ const finishedAt = now();
195
+ const status = error ? "failed" : mapWorkflowStatus(detail?.run.status);
196
+ const log = renderWorkflowLog({ task, detail, error });
197
+ fs.writeFileSync(logPath, log);
198
+ const result = {
199
+ id: task.id,
200
+ status,
201
+ startedAt: startedAt.toISOString(),
202
+ finishedAt: finishedAt.toISOString(),
203
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
204
+ log: logPath,
205
+ target: { kind: "workflow", ref: task.target.ref },
206
+ detail: {
207
+ runId: detail?.run.id,
208
+ ...(error ? { error: error.message } : {}),
209
+ },
210
+ };
211
+ appendHistory(result);
212
+ // Don't re-throw on workflow failure: the OS scheduler reads exit codes,
213
+ // not exceptions, and the CLI maps `status: "failed"` to a non-zero exit
214
+ // via exitCodeForStatus(). Throwing here would route through the generic
215
+ // runWithJsonErrors path and lose the structured result/history we just
216
+ // recorded.
217
+ return result;
218
+ }
219
+ /**
220
+ * Map the workflow runtime's status into the task-runner status space.
221
+ * Workflows can legitimately remain `active` after `startWorkflowRun`
222
+ * returns (multi-step workflows pause for user input); recording them as
223
+ * "completed" would be misleading. We preserve "active" as a first-class
224
+ * task status with exit code 0 — the OS scheduler treats it as success.
225
+ */
226
+ function mapWorkflowStatus(status) {
227
+ switch (status) {
228
+ case "completed":
229
+ case "blocked":
230
+ case "failed":
231
+ case "active":
232
+ return status;
233
+ default:
234
+ return "completed";
235
+ }
236
+ }
237
+ function renderWorkflowLog(input) {
238
+ const lines = [];
239
+ lines.push(`[akm tasks] task=${input.task.id} kind=workflow ref=${input.task.target.ref}`);
240
+ if (input.detail) {
241
+ lines.push(`run_id=${input.detail.run.id} status=${input.detail.run.status}`);
242
+ lines.push(`workflow_title=${input.detail.run.workflowTitle}`);
243
+ }
244
+ if (input.error) {
245
+ lines.push(`error=${input.error.message}`);
246
+ }
247
+ return `${lines.join("\n")}\n`;
248
+ }
249
+ // ── prompt target ───────────────────────────────────────────────────────────
250
+ async function runPromptTask(input) {
251
+ const { task, stashDir, logPath, startedAt, now, runAgentImpl, agentOptions } = input;
252
+ if (task.target.kind !== "prompt")
253
+ throw new Error("invariant: prompt target");
254
+ // Use pre-resolved agent config when available to avoid redundant loadConfig()
255
+ // calls in batch task runs (Fix C6). Fall back to loadConfig() for callers
256
+ // that invoke runPromptTask directly without threading config.
257
+ const fullConfig = loadConfig();
258
+ const agentCfg = input.agentConfig !== undefined ? input.agentConfig : fullConfig;
259
+ // Resolve the profile for this task. When the task doc specifies a profile,
260
+ // use it directly. Otherwise fall back to the per-process config for "task"
261
+ // (agent.processes["task"]), which itself falls back to agent.default.
262
+ let profile;
263
+ let processTimeoutMs;
264
+ if (task.target.profile) {
265
+ // v2: if profiles.agent is configured, resolve through new runner
266
+ if (fullConfig.profiles?.agent) {
267
+ const mode = task.target.mode ?? "agent";
268
+ if (mode !== "llm") {
269
+ const runnerSpec = resolveRunner(mode, task.target.profile, fullConfig);
270
+ if (runnerSpec.kind === "agent" || runnerSpec.kind === "sdk") {
271
+ profile = runnerSpec.profile;
272
+ processTimeoutMs = runnerSpec.timeoutMs;
273
+ }
274
+ else {
275
+ profile = requireAgentProfile(agentCfg, task.target.profile);
276
+ }
277
+ }
278
+ else {
279
+ profile = requireAgentProfile(agentCfg, task.target.profile);
280
+ }
281
+ }
282
+ else {
283
+ // v1: Task doc explicitly names a profile — honour it directly.
284
+ profile = requireAgentProfile(agentCfg, task.target.profile);
285
+ }
286
+ }
287
+ else {
288
+ // No per-task profile: use process config for "task" as a fallback.
289
+ const resolved = resolveProcessAgentProfile("task", agentCfg);
290
+ profile = resolved.profile;
291
+ processTimeoutMs = resolved.timeoutMs;
292
+ }
293
+ // Task-level timeoutMs (including null = disabled) wins over global config.
294
+ // Resolution: task.timeoutMs → process entry timeoutMs → input.agentTimeoutMs → agentCfg.timeoutMs.
295
+ const agentTimeoutMs = task.timeoutMs !== undefined
296
+ ? task.timeoutMs
297
+ : processTimeoutMs !== undefined
298
+ ? processTimeoutMs
299
+ : input.agentTimeoutMs !== undefined
300
+ ? input.agentTimeoutMs
301
+ : undefined;
302
+ const promptText = await resolvePromptText(task, stashDir);
303
+ const result = await runAgentImpl(profile, promptText, {
304
+ stdio: "captured",
305
+ timeoutMs: agentTimeoutMs,
306
+ cwd: stashDir,
307
+ ...agentOptions,
308
+ });
309
+ const finishedAt = now();
310
+ const log = renderPromptLog({ task, profileName: profile.name, result });
311
+ fs.writeFileSync(logPath, log);
312
+ const status = result.ok ? "completed" : "failed";
313
+ const out = {
314
+ id: task.id,
315
+ status,
316
+ startedAt: startedAt.toISOString(),
317
+ finishedAt: finishedAt.toISOString(),
318
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
319
+ log: logPath,
320
+ target: { kind: "prompt", profile: profile.name },
321
+ detail: result.ok
322
+ ? { exitCode: result.exitCode }
323
+ : { reason: result.reason, error: result.error, exitCode: result.exitCode },
324
+ };
325
+ appendHistory(out);
326
+ return out;
327
+ }
328
+ async function resolvePromptText(task, stashDir) {
329
+ if (task.target.kind !== "prompt")
330
+ throw new Error("invariant: prompt target");
331
+ const src = task.target.source;
332
+ if (src.kind === "inline")
333
+ return src.text;
334
+ if (src.kind === "file") {
335
+ const taskDir = path.dirname(task.source.path);
336
+ const filePath = path.isAbsolute(src.path) ? src.path : path.resolve(taskDir, src.path);
337
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
338
+ throw new NotFoundError(`Prompt file not found: ${filePath}`, "FILE_NOT_FOUND");
339
+ }
340
+ return fs.readFileSync(filePath, "utf8");
341
+ }
342
+ // asset
343
+ const ref = parseAssetRef(src.ref);
344
+ const assetPath = await resolveAssetPath(stashDir, ref.type, ref.name);
345
+ return fs.readFileSync(assetPath, "utf8");
346
+ }
347
+ function renderPromptLog(input) {
348
+ const lines = [];
349
+ lines.push(`[akm tasks] task=${input.task.id} kind=prompt profile=${input.profileName}`);
350
+ lines.push(`ok=${input.result.ok} exit_code=${input.result.exitCode ?? "null"} duration_ms=${input.result.durationMs}`);
351
+ if (!input.result.ok) {
352
+ lines.push(`reason=${input.result.reason ?? ""} error=${input.result.error ?? ""}`);
353
+ }
354
+ if (input.result.stdout) {
355
+ lines.push("--- agent stdout ---");
356
+ lines.push(input.result.stdout);
357
+ }
358
+ if (input.result.stderr) {
359
+ lines.push("--- agent stderr ---");
360
+ lines.push(input.result.stderr);
361
+ }
362
+ return `${lines.join("\n")}\n`;
363
+ }
364
+ // ── history ─────────────────────────────────────────────────────────────────
365
+ function appendHistory(result) {
366
+ try {
367
+ const db = openStateDatabase();
368
+ try {
369
+ upsertTaskHistory(db, {
370
+ task_id: result.id,
371
+ status: result.status,
372
+ started_at: result.startedAt,
373
+ completed_at: result.finishedAt,
374
+ failed_at: result.status === "failed" ? result.finishedAt : null,
375
+ log_path: result.log,
376
+ target_kind: result.target.kind,
377
+ target_ref: result.target.kind === "workflow" ? result.target.ref : null,
378
+ metadata_json: JSON.stringify({
379
+ durationMs: result.durationMs,
380
+ detail: result.detail ?? null,
381
+ profile: result.target.kind === "prompt" ? result.target.profile : undefined,
382
+ }),
383
+ });
384
+ }
385
+ finally {
386
+ db.close();
387
+ }
388
+ }
389
+ catch (err) {
390
+ rethrowIfTestIsolationError(err);
391
+ error(`[akm] task history DB write failed: ${String(err)}`);
392
+ }
393
+ }
394
+ export function readTaskHistory(options = {}) {
395
+ const db = openStateDatabase();
396
+ try {
397
+ let rows;
398
+ if (options.id) {
399
+ const row = getTaskHistory(db, options.id);
400
+ rows = row ? [taskHistoryRowToResult(row)] : [];
401
+ }
402
+ else {
403
+ rows = queryTaskHistory(db, {}).map(taskHistoryRowToResult);
404
+ }
405
+ rows.sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
406
+ if (options.limit !== undefined && options.limit >= 0) {
407
+ return rows.slice(0, options.limit);
408
+ }
409
+ return rows;
410
+ }
411
+ finally {
412
+ db.close();
413
+ }
414
+ }
415
+ /**
416
+ * Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
417
+ * that callers of `readTaskHistory()` expect.
418
+ */
419
+ function taskHistoryRowToResult(row) {
420
+ let meta = {};
421
+ try {
422
+ meta = JSON.parse(row.metadata_json);
423
+ }
424
+ catch {
425
+ // ignore corrupt JSON
426
+ }
427
+ const target = row.target_kind === "workflow"
428
+ ? { kind: "workflow", ref: row.target_ref ?? "" }
429
+ : { kind: "prompt", profile: meta.profile };
430
+ return {
431
+ id: row.task_id,
432
+ status: row.status,
433
+ startedAt: row.started_at,
434
+ finishedAt: row.completed_at ?? row.failed_at ?? row.started_at,
435
+ durationMs: meta.durationMs ?? 0,
436
+ log: row.log_path ?? "",
437
+ target,
438
+ ...(meta.detail !== undefined ? { detail: meta.detail } : {}),
439
+ };
440
+ }
441
+ /**
442
+ * The exit code surfaced to the OS scheduler. Mapped from {@link TaskRunStatus}
443
+ * so cron / launchd / schtasks see a useful return value.
444
+ */
445
+ export function exitCodeForStatus(status) {
446
+ switch (status) {
447
+ case "completed":
448
+ return 0;
449
+ case "active":
450
+ return 0;
451
+ case "blocked":
452
+ return 1;
453
+ case "failed":
454
+ return 1;
455
+ case "disabled":
456
+ return 0;
457
+ }
458
+ }
@@ -0,0 +1,227 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Cross-platform schedule parsing and translation.
6
+ *
7
+ * Users always type cron-style expressions:
8
+ *
9
+ * • `m h dom mon dow` — five-field cron (UNIX minute/hour/dom/mon/dow)
10
+ * • `@hourly` `@daily` `@weekly` `@monthly`
11
+ *
12
+ * Each {@link ScheduleSpec} can then be translated to:
13
+ *
14
+ * • a verbatim cron line (Linux backend),
15
+ * • a launchd plist `<StartCalendarInterval>` / `<StartInterval>` (macOS),
16
+ * • Task Scheduler XML triggers (Windows).
17
+ *
18
+ * The shared subset is `*`, single integers, `*\/N`, plus the `@hourly /
19
+ * @daily / @weekly / @monthly` aliases. Patterns outside that — multi-value
20
+ * lists, ranges, step values other than `*\/N`, day-of-month AND
21
+ * day-of-week combinations — are rejected with a {@link UsageError}.
22
+ *
23
+ * Cron is the most permissive of the three backends; some patterns it
24
+ * accepts (e.g. `@hourly` = `0 * * * *`) have no clean schtasks primitive.
25
+ * Validation runs against the *active* backend, so a task authored on
26
+ * Linux may fail to translate when copied to macOS/Windows. `tasks sync`
27
+ * re-validates against the local backend and surfaces any incompatibility.
28
+ */
29
+ import { UsageError } from "../core/errors";
30
+ const ALIAS_TO_CRON = {
31
+ "@hourly": "0 * * * *",
32
+ "@daily": "0 0 * * *",
33
+ "@midnight": "0 0 * * *",
34
+ "@weekly": "0 0 * * 0",
35
+ "@monthly": "0 0 1 * *",
36
+ };
37
+ const FIELD_LIMITS = {
38
+ minute: { min: 0, max: 59 },
39
+ hour: { min: 0, max: 23 },
40
+ dom: { min: 1, max: 31 },
41
+ month: { min: 1, max: 12 },
42
+ dow: { min: 0, max: 6 },
43
+ };
44
+ const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), step-on-star (`*/N`), and comma lists (`7,37`). " +
45
+ "Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`. " +
46
+ "Lists, ranges, and named days/months are not supported.";
47
+ export function parseSchedule(input, backend) {
48
+ const cron = expandAlias(input);
49
+ const fields = parseCronFields(cron, input);
50
+ const spec = { raw: input, cron, fields };
51
+ // Validate translatability for the active backend so the caller does not
52
+ // silently accept expressions that backend cannot run. Note: cron is the
53
+ // most permissive of the three (e.g. `@hourly` is `0 * * * *` which has
54
+ // no clean schtasks primitive), so a task authored on Linux may not be
55
+ // portable to macOS/Windows. `tasks sync` on the destination platform
56
+ // re-runs this with the local backend and will surface any incompatibility.
57
+ if (backend === "launchd")
58
+ translateToLaunchd(spec);
59
+ if (backend === "schtasks")
60
+ translateToSchtasks(spec);
61
+ return spec;
62
+ }
63
+ function expandAlias(raw) {
64
+ const trimmed = raw.trim();
65
+ if (!trimmed) {
66
+ throw new UsageError("Schedule is empty.", "MISSING_REQUIRED_ARGUMENT");
67
+ }
68
+ const lower = trimmed.toLowerCase();
69
+ if (lower in ALIAS_TO_CRON)
70
+ return ALIAS_TO_CRON[lower];
71
+ return trimmed;
72
+ }
73
+ function parseCronFields(cron, original) {
74
+ const parts = cron.split(/\s+/);
75
+ if (parts.length !== 5) {
76
+ throw new UsageError(`Invalid schedule "${original}": expected 5 fields, got ${parts.length}. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE");
77
+ }
78
+ const [m, h, dom, mon, dow] = parts;
79
+ return {
80
+ minute: parseField(m, "minute", FIELD_LIMITS.minute, original),
81
+ hour: parseField(h, "hour", FIELD_LIMITS.hour, original),
82
+ dom: parseField(dom, "day-of-month", FIELD_LIMITS.dom, original),
83
+ month: parseField(mon, "month", FIELD_LIMITS.month, original),
84
+ dow: parseField(dow, "day-of-week", FIELD_LIMITS.dow, original),
85
+ };
86
+ }
87
+ function parseField(raw, name, limit, original) {
88
+ if (raw === "*")
89
+ return { kind: "star" };
90
+ const stepMatch = raw.match(/^\*\/(\d+)$/);
91
+ if (stepMatch) {
92
+ const step = Number(stepMatch[1]);
93
+ // Step must be ≥1 and ≤ the field's range size, not just `max`. For
94
+ // 1-based fields like day-of-month (1-31) and month (1-12) the previous
95
+ // `max + 1` bound let invalid steps like `*/32` or `*/13` slip through.
96
+ const range = limit.max - limit.min + 1;
97
+ if (!Number.isInteger(step) || step <= 0 || step > range) {
98
+ throw new UsageError(`Invalid ${name} step "${raw}" in schedule "${original}". ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE");
99
+ }
100
+ return { kind: "step", step };
101
+ }
102
+ if (/^\d+$/.test(raw)) {
103
+ const value = Number(raw);
104
+ if (value < limit.min || value > limit.max) {
105
+ throw new UsageError(`Invalid ${name} value "${raw}" in schedule "${original}" (allowed ${limit.min}-${limit.max}).`, "INVALID_FLAG_VALUE");
106
+ }
107
+ return { kind: "value", value };
108
+ }
109
+ // Comma-separated list: `7,37` or `0,15,30,45`. Each element must be an
110
+ // integer within [limit.min, limit.max]. Duplicates and unsorted input
111
+ // are accepted but the parsed form is deduped + ascending so downstream
112
+ // consumers can rely on a canonical shape.
113
+ if (/^\d+(,\d+)+$/.test(raw)) {
114
+ const values = [...new Set(raw.split(",").map((s) => Number(s)))].sort((a, b) => a - b);
115
+ for (const v of values) {
116
+ if (!Number.isInteger(v) || v < limit.min || v > limit.max) {
117
+ throw new UsageError(`Invalid ${name} list value "${v}" in schedule "${original}" (allowed ${limit.min}-${limit.max}).`, "INVALID_FLAG_VALUE");
118
+ }
119
+ }
120
+ return { kind: "list", values };
121
+ }
122
+ throw new UsageError(`Unsupported ${name} expression "${raw}" in schedule "${original}". ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE");
123
+ }
124
+ // ── Backend translators ─────────────────────────────────────────────────────
125
+ /** Verbatim cron line, alias-expanded. */
126
+ export function translateToCron(spec) {
127
+ return spec.cron;
128
+ }
129
+ export function translateToLaunchd(spec) {
130
+ const f = spec.fields;
131
+ // `*/N` minute (everything else `*`) → StartInterval = N*60 seconds.
132
+ if (f.minute.kind === "step" &&
133
+ f.hour.kind === "star" &&
134
+ f.dom.kind === "star" &&
135
+ f.month.kind === "star" &&
136
+ f.dow.kind === "star") {
137
+ return { intervalSeconds: f.minute.step * 60 };
138
+ }
139
+ // `*/N` hour → StartInterval = N*3600.
140
+ if (f.minute.kind === "value" &&
141
+ f.minute.value === 0 &&
142
+ f.hour.kind === "step" &&
143
+ f.dom.kind === "star" &&
144
+ f.month.kind === "star" &&
145
+ f.dow.kind === "star") {
146
+ return { intervalSeconds: f.hour.step * 3600 };
147
+ }
148
+ // Otherwise build a calendar dict from concrete values. launchd treats any
149
+ // omitted key as "every value", so a `*` field translates to "no key".
150
+ // Exception: launchd does not support arbitrary step values inside a
151
+ // calendar dict — reject those.
152
+ const calendar = {};
153
+ rejectStepInsideCalendar(f.minute, "minute", spec);
154
+ rejectStepInsideCalendar(f.hour, "hour", spec);
155
+ rejectStepInsideCalendar(f.dom, "day-of-month", spec);
156
+ rejectStepInsideCalendar(f.month, "month", spec);
157
+ rejectStepInsideCalendar(f.dow, "day-of-week", spec);
158
+ if (f.minute.kind === "value")
159
+ calendar.Minute = f.minute.value;
160
+ if (f.hour.kind === "value")
161
+ calendar.Hour = f.hour.value;
162
+ if (f.dom.kind === "value")
163
+ calendar.Day = f.dom.value;
164
+ if (f.month.kind === "value")
165
+ calendar.Month = f.month.value;
166
+ if (f.dow.kind === "value")
167
+ calendar.Weekday = f.dow.value;
168
+ // launchd's CalendarInterval requires at least one specific key. If every
169
+ // field is `*` the schedule has no anchor and we'd need a StartInterval
170
+ // instead — treat this as "every minute".
171
+ if (Object.keys(calendar).length === 0) {
172
+ return { intervalSeconds: 60 };
173
+ }
174
+ return { calendar };
175
+ }
176
+ function rejectStepInsideCalendar(field, name, spec) {
177
+ if (field.kind === "step") {
178
+ throw new UsageError(`Schedule "${spec.raw}" uses step (${name} = */N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either restrict the step to the minute or hour field only, or rewrite the schedule with concrete values.");
179
+ }
180
+ if (field.kind === "list") {
181
+ throw new UsageError(`Schedule "${spec.raw}" uses comma list (${name} = a,b,...) which macOS launchd cannot express as a single trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either install one task per list element, or rewrite the schedule with a step (`*/N`) or single value.");
182
+ }
183
+ }
184
+ export function translateToSchtasks(spec) {
185
+ const f = spec.fields;
186
+ // `*/N` minute → MINUTE, every N.
187
+ if (f.minute.kind === "step" &&
188
+ f.hour.kind === "star" &&
189
+ f.dom.kind === "star" &&
190
+ f.month.kind === "star" &&
191
+ f.dow.kind === "star") {
192
+ return { kind: "minute", everyMinutes: f.minute.step };
193
+ }
194
+ // `0 */N * * *` → HOURLY, every N.
195
+ if (f.minute.kind === "value" &&
196
+ f.minute.value === 0 &&
197
+ f.hour.kind === "step" &&
198
+ f.dom.kind === "star" &&
199
+ f.month.kind === "star" &&
200
+ f.dow.kind === "star") {
201
+ return { kind: "hour", everyHours: f.hour.step };
202
+ }
203
+ // `M H * * *` → DAILY at H:M.
204
+ if (f.minute.kind === "value" &&
205
+ f.hour.kind === "value" &&
206
+ f.dom.kind === "star" &&
207
+ f.month.kind === "star" &&
208
+ f.dow.kind === "star") {
209
+ return { kind: "daily", atHour: f.hour.value, atMinute: f.minute.value };
210
+ }
211
+ // `M H * * D` → WEEKLY at H:M on day D.
212
+ if (f.minute.kind === "value" &&
213
+ f.hour.kind === "value" &&
214
+ f.dom.kind === "star" &&
215
+ f.month.kind === "star" &&
216
+ f.dow.kind === "value") {
217
+ return {
218
+ kind: "weekly",
219
+ atHour: f.hour.value,
220
+ atMinute: f.minute.value,
221
+ daysOfWeek: [f.dow.value],
222
+ };
223
+ }
224
+ throw new UsageError(`Schedule "${spec.raw}" cannot be expressed as a Windows Task Scheduler trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Use one of: */N minutes, every N hours (0 */N * * *), daily at HH:MM, or weekly on a single weekday.");
225
+ }
226
+ /** Human-readable summary used by `tasks doctor`. */
227
+ export const SCHEDULE_SUPPORTED_SUBSET_HINT = SUPPORTED_HINT;