akm-cli 0.9.2-alpha.4 → 0.9.2

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 (143) hide show
  1. package/CHANGELOG.md +493 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/windows.js +8 -4
  19. package/dist/commands/health.js +8 -4
  20. package/dist/commands/lint/index.js +1 -1
  21. package/dist/commands/migrate-cli.js +130 -24
  22. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  23. package/dist/commands/tasks/explain.js +304 -0
  24. package/dist/commands/tasks/tasks-cli.js +185 -3
  25. package/dist/commands/tasks/tasks.js +265 -52
  26. package/dist/commands/workflow/plan.js +159 -0
  27. package/dist/commands/workflow-cli.js +94 -2
  28. package/dist/core/activation-policy.js +2 -12
  29. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  30. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  31. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  32. package/dist/core/errors.js +45 -0
  33. package/dist/core/json-schema.js +15 -5
  34. package/dist/core/state/migrations.js +57 -0
  35. package/dist/core/state-db.js +16 -14
  36. package/dist/core/subprocess.js +47 -13
  37. package/dist/execution/guarded-source.js +44 -0
  38. package/dist/execution/input-contract.js +250 -0
  39. package/dist/execution/target-ref.js +63 -0
  40. package/dist/indexer/usage/usage-events.js +14 -3
  41. package/dist/integrations/agent/execution-lowering.js +12 -1
  42. package/dist/output/shapes/passthrough.js +2 -0
  43. package/dist/output/text/helpers.js +1 -1
  44. package/dist/output/text/migrate.js +12 -3
  45. package/dist/output/text/workflow-format.js +192 -10
  46. package/dist/output/text/workflow.js +2 -1
  47. package/dist/runtime.js +1 -0
  48. package/dist/scripts/akm-migrate-node.js +11838 -10118
  49. package/dist/scripts/akm-migrate.js +11828 -10117
  50. package/dist/setup/steps/tasks.js +34 -17
  51. package/dist/storage/repositories/task-history-repository.js +5 -1
  52. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  53. package/dist/tasks/backends/launchd.js +31 -84
  54. package/dist/tasks/embedded.js +13 -7
  55. package/dist/tasks/model/invocation.js +4 -0
  56. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  57. package/dist/tasks/prepare/prepare-support.js +154 -0
  58. package/dist/tasks/prepare/prepare.js +117 -0
  59. package/dist/tasks/prepare/prepared-execution.js +4 -0
  60. package/dist/tasks/prepare/script-capture.js +80 -0
  61. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  62. package/dist/tasks/run/load-task.js +117 -0
  63. package/dist/tasks/run/provenance.js +20 -0
  64. package/dist/tasks/run/run-command-task.js +92 -0
  65. package/dist/tasks/run/run-native-task.js +222 -0
  66. package/dist/tasks/run/run-task.js +99 -0
  67. package/dist/tasks/run/run-workflow-task.js +222 -0
  68. package/dist/tasks/run/task-history.js +134 -0
  69. package/dist/tasks/run/task-log.js +179 -0
  70. package/dist/tasks/run/task-result.js +19 -0
  71. package/dist/tasks/scheduler-binding.js +66 -2
  72. package/dist/tasks/scheduler-invocation.js +63 -3
  73. package/dist/tasks/scheduler-sync.js +77 -14
  74. package/dist/tasks/source/bounded-document.js +455 -0
  75. package/dist/tasks/source/parse-task-source.js +59 -0
  76. package/dist/tasks/source/project-v4.js +62 -0
  77. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  78. package/dist/tasks/source/task-source-v4.js +626 -0
  79. package/dist/tasks/source-v3.js +10 -733
  80. package/dist/tasks/task-run-reserved-flags.js +79 -0
  81. package/dist/workflows/authoring/authoring.js +17 -8
  82. package/dist/workflows/exec/child-invocation.js +34 -0
  83. package/dist/workflows/exec/child-workflow.js +370 -0
  84. package/dist/workflows/exec/exec-unit.js +50 -170
  85. package/dist/workflows/exec/frozen-judge.js +19 -2
  86. package/dist/workflows/exec/native-executor.js +49 -27
  87. package/dist/workflows/exec/param-secrets.js +12 -0
  88. package/dist/workflows/exec/run-workflow.js +48 -59
  89. package/dist/workflows/exec/step-work.js +222 -80
  90. package/dist/workflows/exec/unit-dispatch.js +72 -0
  91. package/dist/workflows/freeze/child-output-references.js +94 -0
  92. package/dist/workflows/freeze/environment.js +174 -0
  93. package/dist/workflows/freeze/identity.js +22 -0
  94. package/dist/workflows/freeze/resolve-steps.js +78 -0
  95. package/dist/workflows/freeze/source-freeze.js +57 -0
  96. package/dist/workflows/freeze/step-values.js +68 -0
  97. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  98. package/dist/workflows/freeze/targets/command.js +81 -0
  99. package/dist/workflows/freeze/targets/script.js +57 -0
  100. package/dist/workflows/freeze/targets/shell.js +31 -0
  101. package/dist/workflows/freeze/targets/task.js +179 -0
  102. package/dist/workflows/freeze/task-bindings.js +180 -0
  103. package/dist/workflows/ir/compile.js +59 -11
  104. package/dist/workflows/ir/environment-v4.js +3 -3
  105. package/dist/workflows/ir/freeze-v4.js +41 -7
  106. package/dist/workflows/ir/params.js +58 -131
  107. package/dist/workflows/ir/plan-hash.js +3 -3
  108. package/dist/workflows/ir/schema-v4.js +246 -17
  109. package/dist/workflows/parser.js +74 -2
  110. package/dist/workflows/program/schema.js +5 -2
  111. package/dist/workflows/resource-limits.js +20 -0
  112. package/dist/workflows/runtime/plan-classifier.js +24 -7
  113. package/dist/workflows/runtime/run-outputs.js +103 -0
  114. package/dist/workflows/runtime/runs.js +114 -9
  115. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  116. package/dist/workflows/source-files.js +5 -5
  117. package/dist/workflows/source-ir/compare.js +17 -0
  118. package/dist/workflows/source-ir/compile.js +7 -3
  119. package/dist/workflows/source-ir/github-yaml.js +64 -17
  120. package/dist/workflows/source-ir/schema.js +69 -21
  121. package/dist/workflows/source-ir/semantics.js +7 -25
  122. package/dist/workflows/source-ir/triggers.js +79 -0
  123. package/dist/workflows/source-ir/uses.js +33 -7
  124. package/docs/migration/README.md +1 -1
  125. package/docs/migration/release-notes/0.9.2.md +87 -11
  126. package/docs/migration/release-notes/README.md +3 -2
  127. package/docs/migration/v0.8-to-v0.9.md +13 -11
  128. package/docs/migration/v0.9.0-troubleshooting.md +20 -13
  129. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  130. package/docs/reference/README.md +1 -1
  131. package/docs/reference/cli.md +140 -46
  132. package/docs/reference/configuration.md +6 -5
  133. package/docs/reference/supported-formats.md +9 -5
  134. package/docs/reference/tasks.md +338 -75
  135. package/docs/reference/workflow-schema.md +290 -16
  136. package/docs/reference/workflows.md +57 -7
  137. package/package.json +1 -1
  138. package/schemas/akm-task.json +173 -118
  139. package/schemas/akm-workflow.json +28 -0
  140. package/dist/tasks/runner.js +0 -941
  141. package/dist/tasks/runtime-v3.js +0 -281
  142. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  143. package/dist/workflows/source-ir/ordering.js +0 -38
@@ -0,0 +1,92 @@
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
+ import { dispatchPreparedCommandInvocation } from "../../commands/command/command-execution.js";
5
+ import { finishAttempt } from "./attempt-lifecycle.js";
6
+ import { appendHistory } from "./task-history.js";
7
+ import { persistRunLog, scrubTaskOutput, streamLines } from "./task-log.js";
8
+ export async function runPreparedCommandTask(input) {
9
+ const { task, logPath, startedAt, now, agentOptions, provenance } = input;
10
+ const dispatchOptions = {
11
+ ...(input.runAgentImpl ? { runAgent: input.runAgentImpl } : {}),
12
+ ...(input.chatCompletionImpl ? { chat: input.chatCompletionImpl } : {}),
13
+ ...(agentOptions ? { runOptions: agentOptions } : {}),
14
+ // F-1 (R-07 fix, spec §5.2 point 3): threads the resolved event source
15
+ // into the dispatched engine's child env and the recorded usage events.
16
+ eventSource: provenance.eventSource,
17
+ };
18
+ const result = await dispatchPreparedCommandInvocation(task.invocation, dispatchOptions);
19
+ const engineName = result.engine;
20
+ const finishedAt = finishAttempt(startedAt, now());
21
+ const log = renderPromptLog({ task, engineName, result, notices: result.notices, warnings: result.warnings });
22
+ persistRunLog({
23
+ taskId: task.taskId,
24
+ startedAtIso: startedAt.toISOString(),
25
+ finishedAtIso: finishedAt.toISOString(),
26
+ logPath,
27
+ fileText: log.fileText,
28
+ dbLines: log.dbLines,
29
+ redactNames: task.redact,
30
+ environment: task.environment,
31
+ });
32
+ const status = result.ok ? "completed" : "failed";
33
+ const out = {
34
+ id: task.taskId,
35
+ status,
36
+ startedAt: startedAt.toISOString(),
37
+ finishedAt: finishedAt.toISOString(),
38
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
39
+ log: logPath,
40
+ target: { kind: "command", engine: engineName },
41
+ detail: result.ok
42
+ ? { exitCode: result.exitCode }
43
+ : {
44
+ reason: result.reason === undefined ? undefined : scrubTaskOutput(task, result.reason),
45
+ error: result.error === undefined ? undefined : scrubTaskOutput(task, result.error),
46
+ exitCode: result.exitCode,
47
+ },
48
+ ...(result.notices && result.notices.length > 0
49
+ ? {
50
+ notices: result.notices.map((notice) => ({
51
+ ...notice,
52
+ message: scrubTaskOutput(task, notice.message),
53
+ })),
54
+ }
55
+ : {}),
56
+ };
57
+ appendHistory(out, input.historyReserved);
58
+ return out;
59
+ }
60
+ function renderPromptLog(input) {
61
+ const lines = [];
62
+ const dbLines = [];
63
+ const header = `[akm task] task=${input.task.taskId} kind=prompt engine=${input.engineName}`;
64
+ const summary = `ok=${input.result.ok} exit_code=${input.result.exitCode ?? "null"} duration_ms=${input.result.durationMs}`;
65
+ lines.push(header, summary);
66
+ dbLines.push({ line: header }, { level: input.result.ok ? "info" : "error", line: summary });
67
+ for (const warning of input.warnings ?? []) {
68
+ lines.push(warning);
69
+ dbLines.push({ level: "warn", line: warning });
70
+ }
71
+ for (const notice of input.notices ?? []) {
72
+ const line = `lowering_notice=${notice.code} adapter=${notice.adapter} field=${notice.field ?? ""} message=${notice.message}`;
73
+ lines.push(line);
74
+ dbLines.push({ level: notice.severity === "warning" ? "warn" : "info", line });
75
+ }
76
+ if (!input.result.ok) {
77
+ const failure = `reason=${input.result.reason ?? ""} error=${input.result.error ?? ""}`;
78
+ lines.push(failure);
79
+ dbLines.push({ level: "error", line: failure });
80
+ }
81
+ if (input.result.stdout) {
82
+ lines.push("--- agent stdout ---");
83
+ lines.push(input.result.stdout);
84
+ dbLines.push(...streamLines(input.result.stdout, "stdout", "info"));
85
+ }
86
+ if (input.result.stderr) {
87
+ lines.push("--- agent stderr ---");
88
+ lines.push(input.result.stderr);
89
+ dbLines.push(...streamLines(input.result.stderr, "stderr", "error"));
90
+ }
91
+ return { fileText: `${lines.join("\n")}\n`, dbLines };
92
+ }
@@ -0,0 +1,222 @@
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
+ * The shell + frozen-script dispatch arm: `runNativeTask`, plus its
6
+ * shell-command builders (`shellCommand`, `resolveLeadingBareAkmCommand`,
7
+ * `quoteShellArgument`).
8
+ *
9
+ * Moved from src/tasks/runner.ts (spec docs/plans/specs/p1b-model-extraction.md
10
+ * §5.1, §9, runner.ts:294-455).
11
+ *
12
+ * F-1 (spec §5.2 point 1): the child env's `AKM_EVENT_SOURCE` stamp now reads
13
+ * `provenance.eventSource` instead of the hardcoded literal `"task"` — with
14
+ * the default provenance context (run/provenance.ts) this is byte-equivalent
15
+ * to before (P-06 stays green unchanged).
16
+ *
17
+ * F-2 (D8, spec §5.3): the result's `target` is now `{kind:"shell", cmd}` or
18
+ * `{kind:"script", cmd}` — formerly one shared `{kind:"command", cmd}`.
19
+ */
20
+ import path from "node:path";
21
+ import { assertNever } from "../../core/assert.js";
22
+ import { runManagedSubprocess, streamCaptureFailure } from "../../core/subprocess.js";
23
+ import { assertFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
24
+ import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../frozen-script.js";
25
+ import { resolveAkmInvocation } from "../resolve-akm-bin.js";
26
+ import { finishAttempt } from "./attempt-lifecycle.js";
27
+ import { DEFAULT_SCHEDULED_TASK_TIMEOUT_MS } from "./run-workflow-task.js";
28
+ import { appendHistory } from "./task-history.js";
29
+ import { persistRunLog, streamLines } from "./task-log.js";
30
+ /**
31
+ * Resolve the host shell to something spawnable in a scheduler-fired process.
32
+ *
33
+ * A scheduled run restores the PATH captured at install time
34
+ * (scheduler-invocation.ts), which can be minimal — the native-scheduler CI
35
+ * gate installs with PATH = System32 + SystemRoot, modeling what a real
36
+ * scheduler hands a job. powershell.exe does not live in System32 itself but
37
+ * in the WindowsPowerShell\v1.0 subdirectory, so a bare "powershell" spawn
38
+ * resolves only when that PATH happens to carry the extra entry; cmd.exe has
39
+ * a canonical ComSpec location for the same reason. Resolve both absolutely
40
+ * on Windows. pwsh has no fixed install location and the POSIX shells always
41
+ * live on the scheduler's default /bin:/usr/bin, so those stay PATH-resolved.
42
+ *
43
+ * Exported for direct unit testing with an explicit platform/env.
44
+ */
45
+ export function shellExecutable(shell, platform = process.platform, env = process.env) {
46
+ if (platform !== "win32")
47
+ return shell;
48
+ const systemRoot = env.SystemRoot ?? "C:\\Windows";
49
+ if (shell === "powershell") {
50
+ return path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
51
+ }
52
+ if (shell === "cmd") {
53
+ return env.ComSpec ?? path.win32.join(systemRoot, "System32", "cmd.exe");
54
+ }
55
+ return shell;
56
+ }
57
+ /** Exported for direct unit testing with an explicit platform/env. */
58
+ export function shellCommand(task, platform = process.platform, env = process.env) {
59
+ const command = resolveLeadingBareAkmCommand(task.command, task.shell);
60
+ switch (task.shell) {
61
+ case "sh":
62
+ case "bash":
63
+ case "zsh":
64
+ return [task.shell, "-c", command];
65
+ case "pwsh":
66
+ case "powershell":
67
+ return [shellExecutable(task.shell, platform, env), "-NoProfile", "-NonInteractive", "-Command", command];
68
+ case "cmd":
69
+ return [shellExecutable("cmd", platform, env), "/d", "/s", "/c", command];
70
+ default:
71
+ return assertNever(task.shell, "shellCommand");
72
+ }
73
+ }
74
+ /**
75
+ * Bind an unambiguous leading bare `akm` (including the task-v2 migrator's
76
+ * quoted form) to this installation. Explicit paths and arbitrary shell
77
+ * fragments remain author-controlled.
78
+ */
79
+ function resolveLeadingBareAkmCommand(command, shell) {
80
+ const leadingBareAkm = /^(\s*)(?:akm(?:\.exe)?|'akm(?:\.exe)?'|"akm(?:\.exe)?")(?=$|[\s;|&])/i;
81
+ if (!leadingBareAkm.test(command))
82
+ return command;
83
+ const quoted = resolveAkmInvocation()
84
+ .argv.map((part) => quoteShellArgument(part, shell))
85
+ .join(" ");
86
+ // PowerShell parses a quoted string in command position as a string
87
+ // EXPRESSION, not an invocation — `'C:\akm.exe' --version` is a parse
88
+ // error. The call operator makes it a command. sh and cmd both treat a
89
+ // quoted word in command position as the command, so only PowerShell
90
+ // needs the prefix.
91
+ const invocation = shell === "pwsh" || shell === "powershell" ? `& ${quoted}` : quoted;
92
+ return command.replace(leadingBareAkm, (_match, leadingWhitespace) => `${leadingWhitespace}${invocation}`);
93
+ }
94
+ function quoteShellArgument(value, shell) {
95
+ switch (shell) {
96
+ case "sh":
97
+ case "bash":
98
+ case "zsh":
99
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
100
+ case "pwsh":
101
+ case "powershell":
102
+ return `'${value.replaceAll("'", "''")}'`;
103
+ case "cmd":
104
+ return `"${value.replaceAll('"', '""')}"`;
105
+ default:
106
+ return assertNever(shell, "quoteShellArgument");
107
+ }
108
+ }
109
+ export async function runNativeTask(input) {
110
+ const { task, logPath, startedAt, now, historyReserved, provenance } = input;
111
+ let materialized;
112
+ let cmd = task.kind === "shell" ? shellCommand(task) : [];
113
+ // Unset → the unattended default; `null` → the explicit no-timeout opt-out.
114
+ const timeoutMs = task.timeoutMs !== undefined ? task.timeoutMs : DEFAULT_SCHEDULED_TASK_TIMEOUT_MS;
115
+ const header = task.kind === "shell"
116
+ ? `[akm task] task=${task.taskId} kind=run shell=${task.shell}`
117
+ : `[akm task] task=${task.taskId} kind=script ref=${task.sourceRef} sha256=${task.sha256}`;
118
+ const logLines = [header];
119
+ const dbLines = [{ line: header }];
120
+ let exitCode = null;
121
+ try {
122
+ // The projector froze both canonical paths and filesystem identities before
123
+ // history mutation. Re-resolve the authored root/cwd immediately before
124
+ // spawn so a symlink, ancestor, bundle-root, or directory/file swap cannot
125
+ // redirect execution outside that physical workspace.
126
+ assertFrozenDirectoryIdentity(task.cwdIdentity);
127
+ if (task.kind === "script") {
128
+ materialized = materializeFrozenScript(task);
129
+ cmd = frozenScriptCommand(task, materialized.file);
130
+ }
131
+ // Managed spawn (src/core/subprocess.ts): process-GROUP kill so a timeout
132
+ // reaps the whole command tree (no orphans), and a SIGTERM→SIGKILL ladder
133
+ // so a child that ignores SIGTERM can't wedge the run forever.
134
+ const result = await runManagedSubprocess(cmd, {
135
+ capture: true,
136
+ cwd: task.cwd,
137
+ // Stamp task-runner provenance so any akm invocation in the command tree
138
+ // records usage events as machine traffic, not user demand (DRIFT-6).
139
+ // A more specific stamp already in the environment (e.g. improve's
140
+ // AKM_EVENT_SOURCE=improve on its child spawns) still wins in children.
141
+ env: {
142
+ ...process.env,
143
+ ...task.environment,
144
+ AKM_EVENT_SOURCE: process.env.AKM_EVENT_SOURCE ?? provenance.eventSource,
145
+ },
146
+ // cmd.exe's `/S /C` reads its tail as one hand-quoted command line, not
147
+ // standard argv — shellCommand() already built and quoted it for that.
148
+ // The default per-argument escaping would add an incompatible second
149
+ // layer on top and break any resolved path containing a space.
150
+ windowsVerbatimArguments: task.kind === "shell" && task.shell === "cmd",
151
+ timeoutMs,
152
+ ...(input.spawnFn ? { spawnFn: input.spawnFn } : {}),
153
+ ...(input.setTimeoutFn ? { setTimeoutFn: input.setTimeoutFn } : {}),
154
+ ...(input.clearTimeoutFn ? { clearTimeoutFn: input.clearTimeoutFn } : {}),
155
+ });
156
+ // A synchronous spawn throw / exit rejection surfaces as spawn_error below.
157
+ if (result.spawnError)
158
+ throw result.spawnError;
159
+ const { stdout, stderr, timedOut } = result;
160
+ exitCode = result.exitCode ?? (timedOut ? 143 : 1);
161
+ // A pipe that errored or stopped draining yields EMPTY output that is
162
+ // otherwise indistinguishable from a command that printed nothing — the
163
+ // log reads like a clean success. Say so instead, the way the workflow
164
+ // (exec-unit.ts) and agent (agent/spawn.ts) capture paths already do.
165
+ const captureFailure = streamCaptureFailure(result.stdoutRead, result.stderrRead);
166
+ if (captureFailure) {
167
+ const line = `output_capture_incomplete=${captureFailure}`;
168
+ logLines.push(line);
169
+ dbLines.push({ level: "warn", line });
170
+ }
171
+ if (timedOut) {
172
+ logLines.push(`timed_out=true timeout_ms=${timeoutMs}`);
173
+ dbLines.push({ level: "error", line: `timed_out=true timeout_ms=${timeoutMs}` });
174
+ }
175
+ logLines.push(`exit_code=${exitCode}`);
176
+ dbLines.push({ level: exitCode === 0 ? "info" : "error", line: `exit_code=${exitCode}` });
177
+ if (stdout) {
178
+ logLines.push("--- stdout ---");
179
+ logLines.push(stdout);
180
+ dbLines.push(...streamLines(stdout, "stdout", "info"));
181
+ }
182
+ if (stderr) {
183
+ logLines.push("--- stderr ---");
184
+ logLines.push(stderr);
185
+ dbLines.push(...streamLines(stderr, "stderr", "error"));
186
+ }
187
+ }
188
+ catch (e) {
189
+ const msg = e instanceof Error ? e.message : String(e);
190
+ logLines.push(`spawn_error=${msg}`);
191
+ dbLines.push({ level: "error", line: `spawn_error=${msg}` });
192
+ exitCode = 1;
193
+ }
194
+ finally {
195
+ if (materialized)
196
+ cleanupFrozenScript(materialized);
197
+ }
198
+ const finishedAt = finishAttempt(startedAt, now());
199
+ persistRunLog({
200
+ taskId: task.taskId,
201
+ startedAtIso: startedAt.toISOString(),
202
+ finishedAtIso: finishedAt.toISOString(),
203
+ logPath,
204
+ fileText: `${logLines.join("\n")}\n`,
205
+ dbLines,
206
+ redactNames: task.redact,
207
+ environment: task.environment,
208
+ });
209
+ const status = exitCode === 0 ? "completed" : "failed";
210
+ const result = {
211
+ id: task.taskId,
212
+ status,
213
+ startedAt: startedAt.toISOString(),
214
+ finishedAt: finishedAt.toISOString(),
215
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
216
+ log: logPath,
217
+ target: task.kind === "shell" ? { kind: "shell", cmd } : { kind: "script", cmd },
218
+ detail: { exitCode },
219
+ };
220
+ appendHistory(result, historyReserved);
221
+ return result;
222
+ }
@@ -0,0 +1,99 @@
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 task run <id>` — what cron / launchd / schtasks invoke at the
6
+ * scheduled moment.
7
+ *
8
+ * The durable boundary is intentional: id/bundle resolution, source read,
9
+ * strict task source parsing, target resolution, command
10
+ * authorization/lowering, workflow projectability, and frozen script-byte
11
+ * capture (load-task.ts) all finish before an attempt is reserved or a log is
12
+ * created (attempt-lifecycle.ts). Once prepared, the runner dispatches the
13
+ * immutable command, workflow, shell, or script projection to its own arm
14
+ * module (run-command-task.ts, run-workflow-task.ts, run-native-task.ts) and
15
+ * records that actual attempt. Task source v4 has no document-level disabled
16
+ * state to skip at fire time (P4-N6) — a schedule binding that should not
17
+ * fire is never installed with the OS scheduler in the first place
18
+ * (scheduler-sync.ts), so every invocation that reaches `runTask` dispatches.
19
+ *
20
+ * Orchestration only.
21
+ *
22
+ * D5 (spec §1.2/§5.2, "Threading"): resolves `options.provenance` against the
23
+ * default context (run/provenance.ts) ONCE per call, and threads the result
24
+ * into every dispatch arm.
25
+ */
26
+ import { runWorkflowSteps } from "../../workflows/exec/run-workflow.js";
27
+ import { recordTaskAttemptFailure, reserveTaskAttempt } from "./attempt-lifecycle.js";
28
+ import { loadPreparedTask } from "./load-task.js";
29
+ import { resolveProvenanceContext } from "./provenance.js";
30
+ import { runPreparedCommandTask } from "./run-command-task.js";
31
+ import { runNativeTask } from "./run-native-task.js";
32
+ import { runWorkflowTask } from "./run-workflow-task.js";
33
+ import { resolveTaskLogPath } from "./task-log.js";
34
+ export async function runTask(id, options) {
35
+ const runWorkflowStepsImpl = options.runWorkflowStepsImpl ?? runWorkflowSteps;
36
+ const now = options.now ?? (() => new Date());
37
+ const requestedStartedAt = now();
38
+ const provenance = resolveProvenanceContext(options.provenance, options.scheduled === true);
39
+ const task = await loadPreparedTask(id, options);
40
+ // All validation, parsing, source resolution, command cascade preparation,
41
+ // and frozen-byte capture above is non-mutating. Only a fully projectable
42
+ // task may reserve durable history or create a log.
43
+ const attempt = reserveTaskAttempt(id, requestedStartedAt);
44
+ const startedAt = attempt.startedAt;
45
+ const startedIso = startedAt.toISOString();
46
+ const logPath = resolveTaskLogPath(options.logDir, id, startedIso);
47
+ try {
48
+ if (task.kind === "workflow") {
49
+ return await runWorkflowTask({
50
+ task,
51
+ logPath,
52
+ startedAt,
53
+ now,
54
+ runWorkflowStepsImpl,
55
+ historyReserved: attempt.historyReserved,
56
+ provenance,
57
+ ...(options.setTimeoutFn ? { setTimeoutFn: options.setTimeoutFn } : {}),
58
+ ...(options.clearTimeoutFn ? { clearTimeoutFn: options.clearTimeoutFn } : {}),
59
+ });
60
+ }
61
+ if (task.kind === "command") {
62
+ return await runPreparedCommandTask({
63
+ task,
64
+ logPath,
65
+ startedAt,
66
+ now,
67
+ historyReserved: attempt.historyReserved,
68
+ provenance,
69
+ runAgentImpl: options.runAgentImpl,
70
+ agentOptions: options.agentOptions,
71
+ chatCompletionImpl: options.chatCompletionImpl,
72
+ });
73
+ }
74
+ options.beforeNativeDispatch?.(task);
75
+ return await runNativeTask({
76
+ task,
77
+ logPath,
78
+ startedAt,
79
+ now,
80
+ historyReserved: attempt.historyReserved,
81
+ provenance,
82
+ ...(options.spawnFn ? { spawnFn: options.spawnFn } : {}),
83
+ ...(options.setTimeoutFn ? { setTimeoutFn: options.setTimeoutFn } : {}),
84
+ ...(options.clearTimeoutFn ? { clearTimeoutFn: options.clearTimeoutFn } : {}),
85
+ });
86
+ }
87
+ catch (failure) {
88
+ recordTaskAttemptFailure({
89
+ taskId: id,
90
+ reason: "task_dispatch_failed",
91
+ failure,
92
+ startedAt,
93
+ finishedAt: now(),
94
+ logDir: options.logDir,
95
+ historyReserved: attempt.historyReserved,
96
+ });
97
+ throw failure;
98
+ }
99
+ }
@@ -0,0 +1,222 @@
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
+ * The workflow dispatch arm: `runWorkflowTask`, `mapWorkflowStatus`,
6
+ * `renderWorkflowLog`, plus the shared unattended-timeout defaults
7
+ * (`DEFAULT_WORKFLOW_TASK_TIMEOUT_MS`, `DEFAULT_SCHEDULED_TASK_TIMEOUT_MS`)
8
+ * every arm reads.
9
+ *
10
+ * Moved from src/tasks/runner.ts (spec docs/plans/specs/p1b-model-extraction.md
11
+ * §5.1, §9, runner.ts:456-675).
12
+ *
13
+ * F-1 (spec §5.2 point 2): the global `process.env.AKM_EVENT_SOURCE` stamp
14
+ * and its `finally` restore are DELETED outright — `process.env` is never
15
+ * written by this arm any more. The resolved event source is instead passed
16
+ * into `runWorkflowStepsImpl` via a new optional `eventSource` option,
17
+ * `process.env.AKM_EVENT_SOURCE ?? provenance.eventSource` (ambient still
18
+ * wins, matching the native arm's own precedence — D5 clause d). Threading
19
+ * that option to an exec-unit child's env is owned by
20
+ * src/workflows/exec/run-workflow.ts / step-work.ts / exec-unit.ts, not this
21
+ * file.
22
+ */
23
+ import { armAbortDeadline } from "../../core/abort-deadline.js";
24
+ import { assertNever } from "../../core/assert.js";
25
+ import { AkmError } from "../../core/errors.js";
26
+ import { finishAttempt } from "./attempt-lifecycle.js";
27
+ import { appendHistory } from "./task-history.js";
28
+ import { persistRunLog, scrubTaskOutput } from "./task-log.js";
29
+ /**
30
+ * Whole-run timeout applied to a workflow-bound task that does not declare its
31
+ * own `timeoutMs` — six hours.
32
+ *
33
+ * `akm workflow run` deliberately has NO default `--timeout`: a human is
34
+ * watching, and Ctrl-C aborts the very same signal the flag's timer would.
35
+ * A scheduled task has nobody watching. Without a default, its only bound is
36
+ * the per-unit timeout — and a frozen plan may set `timeout: null` (unbounded),
37
+ * so one wedged agent unit hangs the run until the machine reboots, holding the
38
+ * run lease and silently skipping every later firing (issue 11).
39
+ *
40
+ * Six hours is deliberately generous rather than tight: the abort is graceful
41
+ * (the engine breaks at the next step boundary and the run stays resumable), so
42
+ * the cost of over-waiting is bounded while the cost of cutting a legitimate
43
+ * long run short is a lost step. It matches the 6h idle window `akm health`
44
+ * already uses to call a run stale (`commands/health/report-view-model.ts`),
45
+ * and it lands well inside a `@daily` cadence, so a wedged run can never still
46
+ * be holding the lease when the next day's firing arrives.
47
+ *
48
+ * An explicit `timeoutMs:` in the task file always wins; `timeoutMs: null` is
49
+ * the explicit opt-out back to unbounded.
50
+ */
51
+ export const DEFAULT_WORKFLOW_TASK_TIMEOUT_MS = 6 * 60 * 60 * 1000;
52
+ /**
53
+ * The same unattended default for command and prompt tasks.
54
+ *
55
+ * The reasoning above is about SCHEDULED runs, not about workflows: nobody is
56
+ * watching, and one wedged run silently stops the schedule. Command tasks
57
+ * defaulted to `null` (no kill timer) and prompt tasks inherited
58
+ * DEFAULT_AGENT_TIMEOUT_MS, also null — so a hung `curl`, a prompting agent
59
+ * waiting on stdin, or a stuck engine wedged the task forever while the
60
+ * workflow arm was protected. Same value, same opt-out: an explicit
61
+ * `timeoutMs:` wins, and `timeoutMs: null` restores unbounded.
62
+ */
63
+ export const DEFAULT_SCHEDULED_TASK_TIMEOUT_MS = DEFAULT_WORKFLOW_TASK_TIMEOUT_MS;
64
+ export async function runWorkflowTask(input) {
65
+ const { task, logPath, startedAt, now, runWorkflowStepsImpl, historyReserved, provenance } = input;
66
+ // Unset → the unattended default; `null` → the explicit no-timeout opt-out.
67
+ const timeoutMs = task.timeoutMs === undefined ? DEFAULT_WORKFLOW_TASK_TIMEOUT_MS : task.timeoutMs;
68
+ // The shared deadline `akm workflow run --timeout` also arms
69
+ // ({@link armAbortDeadline}): one AbortController for the run's lifetime,
70
+ // aborted by a timer. The engine reads `options.signal` at every step
71
+ // boundary and breaks GRACEFULLY — in-flight units are cancelled, the journal
72
+ // and the run lease are retained, and the run is left `active`, i.e.
73
+ // resumable with `akm workflow resume`.
74
+ const controller = new AbortController();
75
+ const deadline = armAbortDeadline(controller, {
76
+ timeoutMs,
77
+ reason: `Workflow task "${task.taskId}" timed out after ${timeoutMs}ms.`,
78
+ ...(input.setTimeoutFn ? { setTimeoutFn: input.setTimeoutFn } : {}),
79
+ ...(input.clearTimeoutFn ? { clearTimeoutFn: input.clearTimeoutFn } : {}),
80
+ });
81
+ let detail;
82
+ let gateError;
83
+ let error;
84
+ // The prompt path logs the engine-fallback announcement; a workflow-backed
85
+ // task must leave the same trace rather than silently using a chosen engine.
86
+ let runWarnings = [];
87
+ // F-1 (D5, spec §5.2 point 2): thread the resolved event source through
88
+ // explicitly instead of stamping process.env — this arm executes IN-PROCESS,
89
+ // so a global stamp used to be how child akm invocations made by workflow
90
+ // steps inherited it; the explicit eventSource option (threaded to
91
+ // src/workflows/exec/run-workflow.ts -> step-work.ts -> exec-unit.ts's
92
+ // childEnv) now does that job without ever mutating process.env. A more
93
+ // specific ambient stamp already present still wins, matching the native arm.
94
+ const eventSource = process.env.AKM_EVENT_SOURCE ?? provenance.eventSource;
95
+ try {
96
+ const execution = await runWorkflowStepsImpl({
97
+ target: task.ref,
98
+ params: task.params,
99
+ signal: controller.signal,
100
+ eventSource,
101
+ ...(task.maxSteps !== undefined ? { maxSteps: task.maxSteps } : {}),
102
+ ...(task.maxRetries !== undefined ? { maxRetries: task.maxRetries } : {}),
103
+ });
104
+ detail = execution.run;
105
+ runWarnings = execution.warnings ?? [];
106
+ if (execution.gateRejection) {
107
+ gateError = `Verification rejected step "${execution.gateRejection.stepId}": ${execution.gateRejection.feedback}`;
108
+ }
109
+ }
110
+ catch (e) {
111
+ if (e instanceof AkmError && e.kind === "config")
112
+ throw e;
113
+ error = e instanceof Error ? e : new Error(String(e));
114
+ }
115
+ finally {
116
+ deadline.disarm();
117
+ }
118
+ // A timeout is a failed ATTEMPT even though the engine stopped cleanly: the
119
+ // aborted run comes back `active` (resumable), which on its own would map to
120
+ // task status "active" and a 0 exit code, telling the OS scheduler nothing
121
+ // went wrong. Surface it like the command target's `timed_out=true` instead.
122
+ //
123
+ // Unless the run COMPLETED anyway. The abort is observed between steps, so a
124
+ // deadline landing in the run's final bookkeeping can set the flag on a run
125
+ // that then finishes — and reporting that as a failure would tell an operator
126
+ // to resume a run with nothing left to resume.
127
+ const ranToCompletion = detail?.status === "completed";
128
+ const timedOutAfterMs = deadline.timedOut() && timeoutMs !== null && !ranToCompletion ? timeoutMs : undefined;
129
+ const timeoutError = timedOutAfterMs === undefined
130
+ ? undefined
131
+ : new Error(`Workflow run timed out after ${timedOutAfterMs}ms and was aborted at a step boundary` +
132
+ (detail?.id ? ` — resume it with \`akm workflow resume ${detail.id}\`.` : "."));
133
+ // One failure value for the three sinks below (status, log line, history
134
+ // detail): a thrown error outranks a gate rejection, which outranks the
135
+ // deadline. Re-laddering per sink is how a log line ends up naming a
136
+ // different cause than the history row it was written beside.
137
+ const failure = error ?? (gateError ? new Error(gateError) : timeoutError);
138
+ const finishedAt = finishAttempt(startedAt, now());
139
+ const status = failure ? "failed" : mapWorkflowStatus(detail?.status);
140
+ const log = renderWorkflowLog({
141
+ task,
142
+ detail,
143
+ error: failure,
144
+ warnings: runWarnings,
145
+ ...(timedOutAfterMs !== undefined ? { timedOutAfterMs } : {}),
146
+ });
147
+ persistRunLog({
148
+ taskId: task.taskId,
149
+ startedAtIso: startedAt.toISOString(),
150
+ finishedAtIso: finishedAt.toISOString(),
151
+ logPath,
152
+ fileText: log.fileText,
153
+ dbLines: log.dbLines,
154
+ redactNames: task.redact,
155
+ environment: task.environment,
156
+ });
157
+ const result = {
158
+ id: task.taskId,
159
+ status,
160
+ startedAt: startedAt.toISOString(),
161
+ finishedAt: finishedAt.toISOString(),
162
+ durationMs: finishedAt.getTime() - startedAt.getTime(),
163
+ log: logPath,
164
+ target: { kind: "workflow", ref: task.ref },
165
+ detail: {
166
+ runId: detail?.id,
167
+ ...(failure ? { error: scrubTaskOutput(task, failure.message) } : {}),
168
+ },
169
+ };
170
+ appendHistory(result, historyReserved);
171
+ // Don't re-throw on workflow failure: the OS scheduler reads exit codes,
172
+ // not exceptions, and the CLI maps `status: "failed"` to a non-zero exit
173
+ // via exitCodeForStatus(). Throwing here would route through the generic
174
+ // runWithJsonErrors path and lose the structured result/history we just
175
+ // recorded.
176
+ return result;
177
+ }
178
+ /**
179
+ * Map the workflow runtime's status into the task-runner status space.
180
+ * A workflow normally reaches completed or failed in one orchestration call.
181
+ * Active remains representable for explicit engine stops such as a gate.
182
+ *
183
+ * The parameter is typed as the runtime's `WorkflowRunStatus` union (plus the
184
+ * `undefined` that `detail?.run.status` can produce when no detail is present).
185
+ * Every union member is handled explicitly and the `default` arm calls
186
+ * `assertNever`, so adding a new `WorkflowRunStatus` variant without mapping it
187
+ * here is a *compile* error rather than silently collapsing to "completed".
188
+ * The previous silent `default: "completed"` is preserved only for the
189
+ * `undefined` (no-detail) case, which is handled up front.
190
+ */
191
+ function mapWorkflowStatus(status) {
192
+ // No run detail → treat as completed (unchanged from the prior silent default).
193
+ if (status === undefined)
194
+ return "completed";
195
+ switch (status) {
196
+ case "completed":
197
+ case "blocked":
198
+ case "failed":
199
+ case "active":
200
+ return status;
201
+ default:
202
+ return assertNever(status, "mapWorkflowStatus");
203
+ }
204
+ }
205
+ function renderWorkflowLog(input) {
206
+ const dbLines = [
207
+ { line: `[akm task] task=${input.task.taskId} kind=workflow ref=${input.task.ref}` },
208
+ ];
209
+ for (const warning of input.warnings ?? [])
210
+ dbLines.push({ level: "warn", line: warning });
211
+ if (input.timedOutAfterMs !== undefined) {
212
+ dbLines.push({ level: "error", line: `timed_out=true timeout_ms=${input.timedOutAfterMs}` });
213
+ }
214
+ if (input.detail) {
215
+ dbLines.push({ line: `run_id=${input.detail.id} status=${input.detail.status}` });
216
+ dbLines.push({ line: `workflow_title=${input.detail.workflowTitle}` });
217
+ }
218
+ if (input.error) {
219
+ dbLines.push({ level: "error", line: `error=${input.error.message}` });
220
+ }
221
+ return { fileText: `${dbLines.map((entry) => entry.line).join("\n")}\n`, dbLines };
222
+ }