akm-cli 0.7.5 → 0.8.0-rc.6

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 (236) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +113 -2
  2. package/README.md +20 -4
  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.js +1995 -551
  9. package/dist/commands/agent-dispatch.js +110 -0
  10. package/dist/commands/agent-support.js +68 -0
  11. package/dist/commands/completions.js +3 -0
  12. package/dist/commands/config-cli.js +130 -534
  13. package/dist/commands/consolidate.js +1531 -0
  14. package/dist/commands/curate.js +44 -3
  15. package/dist/commands/db-cli.js +23 -0
  16. package/dist/commands/distill-promotion-policy.js +660 -0
  17. package/dist/commands/distill.js +990 -75
  18. package/dist/commands/eval-cases.js +43 -0
  19. package/dist/commands/events.js +5 -23
  20. package/dist/commands/graph.js +477 -0
  21. package/dist/commands/health.js +400 -0
  22. package/dist/commands/help/help-accept.md +9 -0
  23. package/dist/commands/help/help-improve.md +77 -0
  24. package/dist/commands/help/help-proposals.md +15 -0
  25. package/dist/commands/help/help-propose.md +17 -0
  26. package/dist/commands/help/help-reject.md +8 -0
  27. package/dist/commands/history.js +54 -46
  28. package/dist/commands/improve-profiles.js +146 -0
  29. package/dist/commands/improve-result-file.js +103 -0
  30. package/dist/commands/improve.js +2175 -0
  31. package/dist/commands/info.js +5 -2
  32. package/dist/commands/init.js +50 -2
  33. package/dist/commands/installed-stashes.js +102 -139
  34. package/dist/commands/knowledge.js +136 -0
  35. package/dist/commands/lint/agent-linter.js +49 -0
  36. package/dist/commands/lint/base-linter.js +479 -0
  37. package/dist/commands/lint/command-linter.js +49 -0
  38. package/dist/commands/lint/default-linter.js +16 -0
  39. package/dist/commands/lint/index.js +183 -0
  40. package/dist/commands/lint/knowledge-linter.js +16 -0
  41. package/dist/commands/lint/markdown-insertion.js +343 -0
  42. package/dist/commands/lint/memory-linter.js +61 -0
  43. package/dist/commands/lint/registry.js +36 -0
  44. package/dist/commands/lint/skill-linter.js +45 -0
  45. package/dist/commands/lint/task-linter.js +50 -0
  46. package/dist/commands/lint/types.js +4 -0
  47. package/dist/commands/lint/vault-key-rules.js +139 -0
  48. package/dist/commands/lint/workflow-linter.js +56 -0
  49. package/dist/commands/lint.js +4 -0
  50. package/dist/commands/migration-help.js +5 -2
  51. package/dist/commands/proposal.js +66 -12
  52. package/dist/commands/propose.js +86 -31
  53. package/dist/commands/reflect.js +1119 -73
  54. package/dist/commands/registry-search.js +5 -2
  55. package/dist/commands/remember.js +69 -6
  56. package/dist/commands/schema-repair.js +203 -0
  57. package/dist/commands/search.js +115 -14
  58. package/dist/commands/self-update.js +3 -0
  59. package/dist/commands/show.js +144 -25
  60. package/dist/commands/source-add.js +17 -45
  61. package/dist/commands/source-clone.js +3 -0
  62. package/dist/commands/source-manage.js +14 -19
  63. package/dist/commands/tasks.js +438 -0
  64. package/dist/commands/url-checker.js +42 -0
  65. package/dist/commands/vault.js +130 -77
  66. package/dist/core/action-contributors.js +28 -0
  67. package/dist/core/asset-ref.js +7 -0
  68. package/dist/core/asset-registry.js +7 -16
  69. package/dist/core/asset-serialize.js +88 -0
  70. package/dist/core/asset-spec.js +22 -0
  71. package/dist/core/common.js +157 -0
  72. package/dist/core/concurrent.js +25 -0
  73. package/dist/core/config-io.js +347 -0
  74. package/dist/core/config-migration.js +625 -0
  75. package/dist/core/config-schema.js +501 -0
  76. package/dist/core/config-sources.js +108 -0
  77. package/dist/core/config-types.js +4 -0
  78. package/dist/core/config-walker.js +337 -0
  79. package/dist/core/config.js +327 -987
  80. package/dist/core/errors.js +40 -19
  81. package/dist/core/events.js +91 -138
  82. package/dist/core/file-lock.js +104 -0
  83. package/dist/core/frontmatter.js +3 -6
  84. package/dist/core/lesson-lint.js +3 -0
  85. package/dist/core/markdown.js +20 -0
  86. package/dist/core/memory-belief.js +62 -0
  87. package/dist/core/memory-contradiction-detect.js +274 -0
  88. package/dist/core/memory-improve.js +806 -0
  89. package/dist/core/parse.js +158 -0
  90. package/dist/core/paths.js +326 -14
  91. package/dist/core/proposal-quality-validators.js +364 -0
  92. package/dist/core/proposal-validators.js +69 -0
  93. package/dist/core/proposals.js +498 -42
  94. package/dist/core/state-db.js +927 -0
  95. package/dist/core/text-truncation.js +107 -0
  96. package/dist/core/time.js +54 -0
  97. package/dist/core/warn.js +62 -1
  98. package/dist/core/write-source.js +3 -0
  99. package/dist/indexer/db-backup.js +391 -0
  100. package/dist/indexer/db-search.js +152 -253
  101. package/dist/indexer/db.js +933 -103
  102. package/dist/indexer/ensure-index.js +64 -0
  103. package/dist/indexer/file-context.js +3 -0
  104. package/dist/indexer/graph-boost.js +376 -101
  105. package/dist/indexer/graph-db.js +391 -0
  106. package/dist/indexer/graph-dedup.js +95 -0
  107. package/dist/indexer/graph-extraction.js +550 -124
  108. package/dist/indexer/index-context.js +4 -0
  109. package/dist/indexer/indexer.js +506 -291
  110. package/dist/indexer/llm-cache.js +47 -0
  111. package/dist/indexer/manifest.js +3 -0
  112. package/dist/indexer/matchers.js +148 -160
  113. package/dist/indexer/memory-inference.js +99 -74
  114. package/dist/indexer/metadata-contributors.js +29 -0
  115. package/dist/indexer/metadata.js +255 -196
  116. package/dist/indexer/path-resolver.js +92 -0
  117. package/dist/indexer/project-context.js +192 -0
  118. package/dist/indexer/ranking-contributors.js +331 -0
  119. package/dist/indexer/ranking.js +81 -0
  120. package/dist/indexer/search-fields.js +5 -9
  121. package/dist/indexer/search-hit-enrichers.js +111 -0
  122. package/dist/indexer/search-source.js +44 -10
  123. package/dist/indexer/semantic-status.js +5 -16
  124. package/dist/indexer/staleness-detect.js +447 -0
  125. package/dist/indexer/usage-events.js +12 -9
  126. package/dist/indexer/walker.js +28 -0
  127. package/dist/integrations/agent/builders.js +135 -0
  128. package/dist/integrations/agent/config.js +122 -230
  129. package/dist/integrations/agent/detect.js +3 -0
  130. package/dist/integrations/agent/index.js +7 -13
  131. package/dist/integrations/agent/model-aliases.js +55 -0
  132. package/dist/integrations/agent/profiles.js +70 -5
  133. package/dist/integrations/agent/prompts.js +150 -74
  134. package/dist/integrations/agent/runner.js +151 -0
  135. package/dist/integrations/agent/sdk-runner.js +126 -0
  136. package/dist/integrations/agent/spawn.js +118 -23
  137. package/dist/integrations/github.js +3 -0
  138. package/dist/integrations/lockfile.js +32 -69
  139. package/dist/integrations/session-logs/index.js +68 -0
  140. package/dist/integrations/session-logs/providers/claude-code.js +59 -0
  141. package/dist/integrations/session-logs/providers/opencode.js +55 -0
  142. package/dist/integrations/session-logs/types.js +4 -0
  143. package/dist/llm/call-ai.js +62 -0
  144. package/dist/llm/client.js +72 -124
  145. package/dist/llm/embedder.js +3 -19
  146. package/dist/llm/embedders/cache.js +3 -7
  147. package/dist/llm/embedders/local.js +3 -0
  148. package/dist/llm/embedders/remote.js +20 -8
  149. package/dist/llm/embedders/types.js +3 -7
  150. package/dist/llm/feature-gate.js +89 -48
  151. package/dist/llm/graph-extract.js +676 -70
  152. package/dist/llm/index-passes.js +9 -23
  153. package/dist/llm/memory-infer.js +52 -71
  154. package/dist/llm/metadata-enhance.js +42 -29
  155. package/dist/llm/prompts/graph-extract-user-prompt.md +35 -0
  156. package/dist/output/cli-hints-full.md +281 -0
  157. package/dist/output/cli-hints-short.md +65 -0
  158. package/dist/output/cli-hints.js +5 -318
  159. package/dist/output/context.js +3 -0
  160. package/dist/output/renderers.js +223 -256
  161. package/dist/output/shapes.js +150 -105
  162. package/dist/output/text.js +318 -30
  163. package/dist/registry/build-index.js +3 -0
  164. package/dist/registry/create-provider-registry.js +3 -0
  165. package/dist/registry/factory.js +3 -0
  166. package/dist/registry/origin-resolve.js +3 -0
  167. package/dist/registry/providers/index.js +3 -0
  168. package/dist/registry/providers/skills-sh.js +70 -49
  169. package/dist/registry/providers/static-index.js +53 -48
  170. package/dist/registry/providers/types.js +3 -24
  171. package/dist/registry/resolve.js +11 -16
  172. package/dist/registry/types.js +3 -0
  173. package/dist/scripts/migrate-storage.js +17307 -0
  174. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8900 -0
  175. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  176. package/dist/setup/detect.js +3 -0
  177. package/dist/setup/ripgrep-install.js +3 -0
  178. package/dist/setup/ripgrep-resolve.js +3 -0
  179. package/dist/setup/setup.js +775 -37
  180. package/dist/setup/steps.js +3 -15
  181. package/dist/sources/include.js +3 -0
  182. package/dist/sources/provider-factory.js +5 -12
  183. package/dist/sources/provider.js +3 -20
  184. package/dist/sources/providers/filesystem.js +19 -23
  185. package/dist/sources/providers/git.js +7 -5
  186. package/dist/sources/providers/index.js +3 -0
  187. package/dist/sources/providers/install-types.js +3 -13
  188. package/dist/sources/providers/npm.js +3 -4
  189. package/dist/sources/providers/provider-utils.js +3 -0
  190. package/dist/sources/providers/sync-from-ref.js +3 -11
  191. package/dist/sources/providers/tar-utils.js +3 -0
  192. package/dist/sources/providers/website.js +18 -22
  193. package/dist/sources/resolve.js +3 -0
  194. package/dist/sources/types.js +3 -0
  195. package/dist/sources/website-ingest.js +7 -0
  196. package/dist/tasks/backends/cron.js +203 -0
  197. package/dist/tasks/backends/exec-utils.js +28 -0
  198. package/dist/tasks/backends/index.js +24 -0
  199. package/dist/tasks/backends/launchd-template.xml +19 -0
  200. package/dist/tasks/backends/launchd.js +187 -0
  201. package/dist/tasks/backends/schtasks-template.xml +29 -0
  202. package/dist/tasks/backends/schtasks.js +215 -0
  203. package/dist/tasks/parser.js +211 -0
  204. package/dist/tasks/resolveAkmBin.js +87 -0
  205. package/dist/tasks/runner.js +458 -0
  206. package/dist/tasks/schedule.js +211 -0
  207. package/dist/tasks/schema.js +15 -0
  208. package/dist/tasks/validator.js +62 -0
  209. package/dist/version.js +3 -0
  210. package/dist/wiki/index-template.md +12 -0
  211. package/dist/wiki/ingest-workflow-template.md +54 -0
  212. package/dist/wiki/log-template.md +8 -0
  213. package/dist/wiki/schema-template.md +61 -0
  214. package/dist/wiki/wiki-templates.js +15 -0
  215. package/dist/wiki/wiki.js +13 -61
  216. package/dist/workflows/authoring.js +8 -25
  217. package/dist/workflows/cli.js +3 -0
  218. package/dist/workflows/db.js +140 -10
  219. package/dist/workflows/document-cache.js +3 -10
  220. package/dist/workflows/parser.js +3 -0
  221. package/dist/workflows/renderer.js +11 -3
  222. package/dist/workflows/runs.js +62 -91
  223. package/dist/workflows/schema.js +3 -0
  224. package/dist/workflows/scope-key.js +3 -0
  225. package/dist/workflows/validator.js +4 -8
  226. package/dist/workflows/workflow-template.md +24 -0
  227. package/docs/README.md +9 -2
  228. package/docs/data-and-telemetry.md +225 -0
  229. package/docs/migration/release-notes/0.7.0.md +1 -1
  230. package/docs/migration/release-notes/0.7.5.md +2 -2
  231. package/docs/migration/release-notes/0.8.0.md +48 -0
  232. package/docs/migration/v0.7-to-v0.8.md +1307 -0
  233. package/package.json +20 -8
  234. package/.github/LICENSE +0 -374
  235. package/dist/commands/install-audit.js +0 -381
  236. package/dist/templates/wiki-templates.js +0 -100
@@ -0,0 +1,87 @@
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
+ * Resolve the absolute invocation that the OS scheduler should run.
6
+ *
7
+ * cron / launchd / schtasks all execute jobs with a stripped environment and
8
+ * a minimal PATH, so the registered command must be an absolute path.
9
+ *
10
+ * Resolution order:
11
+ *
12
+ * 1. `$AKM_BIN` (explicit override; takes precedence everywhere).
13
+ * 2. `process.execPath` + the resolved CLI script — works when running
14
+ * from a development checkout (`bun /repo/src/cli.ts`) and from a
15
+ * compiled install (`bun /opt/akm/dist/cli.js`).
16
+ * 3. `which akm` / `where akm` — last resort when the binary is on PATH
17
+ * but neither override applies.
18
+ *
19
+ * Returns the argv array the scheduler should execute (e.g.
20
+ * `["/usr/local/bin/bun", "/repo/dist/cli.js"]`). The caller appends
21
+ * subcommand args (`"tasks", "run", "<id>"`).
22
+ */
23
+ import { spawnSync } from "node:child_process";
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import { ConfigError } from "../core/errors";
28
+ export function resolveAkmInvocation(options = {}) {
29
+ const env = options.env ?? process.env;
30
+ const override = env.AKM_BIN?.trim();
31
+ if (override) {
32
+ return { argv: [override], via: "AKM_BIN" };
33
+ }
34
+ const cliPath = resolveCliEntry(options.cliEntryUrl ?? import.meta.url);
35
+ if (cliPath && process.execPath) {
36
+ return { argv: [process.execPath, cliPath], via: "execPath" };
37
+ }
38
+ const whichBin = findOnPath("akm", env);
39
+ if (whichBin) {
40
+ return { argv: [whichBin], via: "which" };
41
+ }
42
+ throw new ConfigError("Cannot resolve absolute path to the akm binary for scheduler registration.", "INVALID_CONFIG_FILE", "Set AKM_BIN to the absolute path of the akm binary, or ensure `akm` is on PATH.");
43
+ }
44
+ /**
45
+ * From the URL of a module inside `src/tasks/` figure out the CLI entry.
46
+ *
47
+ * • dev `…/src/tasks/resolveAkmBin.ts` → `…/src/cli.ts`
48
+ * • build `…/dist/tasks/resolveAkmBin.js` → `…/dist/cli.js`
49
+ */
50
+ function resolveCliEntry(moduleUrl) {
51
+ let modulePath;
52
+ try {
53
+ modulePath = fileURLToPath(moduleUrl);
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ const dir = path.dirname(modulePath); // .../tasks
59
+ const parent = path.dirname(dir); // .../src or .../dist
60
+ const ext = path.extname(modulePath); // .ts | .js
61
+ const candidate = path.join(parent, `cli${ext}`);
62
+ if (fs.existsSync(candidate))
63
+ return candidate;
64
+ // Fallback: try the other extension.
65
+ const alt = path.join(parent, ext === ".ts" ? "cli.js" : "cli.ts");
66
+ if (fs.existsSync(alt))
67
+ return alt;
68
+ return undefined;
69
+ }
70
+ function findOnPath(bin, env) {
71
+ const tool = process.platform === "win32" ? "where" : "which";
72
+ try {
73
+ const out = spawnSync(tool, [bin], { encoding: "utf8", env });
74
+ if (out.status === 0 && typeof out.stdout === "string") {
75
+ const first = out.stdout
76
+ .split(/\r?\n/)
77
+ .map((s) => s.trim())
78
+ .find(Boolean);
79
+ if (first && fs.existsSync(first))
80
+ return first;
81
+ }
82
+ }
83
+ catch {
84
+ // ignore — caller will throw a ConfigError
85
+ }
86
+ return undefined;
87
+ }
@@ -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
+ }