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,134 @@
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 task_history read/write boundary: `appendHistory` (write) and
6
+ * `readTaskHistory` / `taskHistoryRowToResult` (read).
7
+ *
8
+ * Moved from src/tasks/runner.ts (spec docs/plans/specs/p1b-model-extraction.md
9
+ * §5.1, §9, runner.ts:1071-1158). `appendHistory` was module-private at head;
10
+ * it is exported here because every dispatch arm (run-native-task.ts,
11
+ * run-workflow-task.ts, run-command-task.ts), attempt-lifecycle.ts, and
12
+ * run-task.ts's disabled-task path all call it (spec §5.1's module split
13
+ * moves what was one file's internal call graph across several files).
14
+ *
15
+ * D8 result-vocabulary re-code (why, and the WRITE side's exact shape): see
16
+ * docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md.
17
+ * The READ side's mapping rule is a PERMANENT invariant kept here, not moved
18
+ * — a maintainer touching `taskHistoryRowToResult` needs it right here, and
19
+ * row B-51 (docs/plans/specs/p4-deletions-closeout.md) makes deleting it a
20
+ * review-blocking violation: it reads rows written by every previous
21
+ * release, forever.
22
+ *
23
+ * A legacy row (no `targetVocab` marker — written before D8) maps:
24
+ * `"prompt"` -> `{kind:"command", engine}`, `"command"` -> `{kind:"shell"}`,
25
+ * `"workflow"` unchanged, anything else (including the new vocabulary's own
26
+ * "shell"/"script"/"prompt" written WITHOUT a marker, which no production
27
+ * writer ever does) -> `"unknown"`. The P0-pinned null fallbacks survive:
28
+ * workflow `ref` falls back to `""`, the command/prompt arm's `engine`
29
+ * falls back to `null`. A row carrying `targetVocab: 2` reads `target_kind`
30
+ * directly in the current vocabulary — no mapping needed.
31
+ *
32
+ * A DAG leaf with respect to the rest of src/tasks/run/**: this module
33
+ * imports TaskRunResult/TaskRunStatus's TYPE from ./task-result but no VALUE
34
+ * from it, and nothing at all from ./task-log or ./attempt-lifecycle — so
35
+ * nothing here risks closing an import cycle with a module that itself
36
+ * imports appendHistory or readTaskHistory from here.
37
+ */
38
+ import { rethrowIfTestIsolationError } from "../../core/errors.js";
39
+ import { withStateDb } from "../../core/state-db.js";
40
+ import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, upsertTaskHistory, } from "../../storage/repositories/task-history-repository.js";
41
+ /** Append (or finalize a reserved attempt into) one task_history row. */
42
+ export function appendHistory(result, historyReserved = false) {
43
+ const row = {
44
+ task_id: result.id,
45
+ status: result.status,
46
+ started_at: result.startedAt,
47
+ completed_at: result.finishedAt,
48
+ failed_at: result.status === "failed" ? result.finishedAt : null,
49
+ log_path: result.log || null,
50
+ target_kind: result.target.kind === "unknown" ? null : result.target.kind,
51
+ target_ref: result.target.kind === "workflow" ? result.target.ref : null,
52
+ metadata_json: JSON.stringify({
53
+ metadataVersion: 2,
54
+ durationMs: result.durationMs,
55
+ detail: result.detail ?? null,
56
+ // D8 (spec §5.3): every NEW row carries the vocabulary marker.
57
+ targetVocab: 2,
58
+ ...(result.target.kind === "command" ? { engine: result.target.engine } : {}),
59
+ }),
60
+ };
61
+ try {
62
+ withStateDb((db) => {
63
+ if (historyReserved && finalizeTaskHistoryAttempt(db, row))
64
+ return;
65
+ upsertTaskHistory(db, row);
66
+ });
67
+ }
68
+ catch (error) {
69
+ rethrowIfTestIsolationError(error);
70
+ // History recording is fully best-effort and must not alter CLI output.
71
+ }
72
+ }
73
+ export function readTaskHistory(options = {}) {
74
+ return withStateDb((db) => {
75
+ if (options.limit === 0)
76
+ return [];
77
+ if (options.id) {
78
+ // An id-scoped query used the single-row helper, so `--limit` was silently
79
+ // discarded and `akm task history --id X --limit 20` always returned one
80
+ // run. The CLI documents --limit as "Maximum rows to return"; honour it.
81
+ if (options.limit !== undefined && options.limit > 0) {
82
+ return getTaskHistoryRuns(db, options.id, options.limit).map(taskHistoryRowToResult);
83
+ }
84
+ const row = getTaskHistory(db, options.id);
85
+ return row ? [taskHistoryRowToResult(row)] : [];
86
+ }
87
+ return queryTaskHistory(db, options.limit !== undefined && options.limit > 0 ? { limit: options.limit } : {}).map(taskHistoryRowToResult);
88
+ });
89
+ }
90
+ /**
91
+ * Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
92
+ * that callers of `readTaskHistory()` expect.
93
+ *
94
+ * D8 read boundary (spec §5.3): branches on the decoded metadata's
95
+ * `targetVocab` marker — see the module header's table.
96
+ */
97
+ function taskHistoryRowToResult(row) {
98
+ const meta = decodeTaskHistoryMetadata(row.metadata_json);
99
+ const marked = meta.targetVocab === 2;
100
+ const target = (() => {
101
+ switch (row.target_kind) {
102
+ case "workflow":
103
+ // PRESERVED for both vintages (incl. the null-ref fallback).
104
+ return { kind: "workflow", ref: row.target_ref ?? "" };
105
+ case "command":
106
+ // NEW vocabulary: a prepared command (agent/LLM) result.
107
+ // LEGACY vocabulary: the native shell/script arm's shared string.
108
+ return marked ? { kind: "command", engine: meta.engine ?? null } : { kind: "shell" };
109
+ case "shell":
110
+ // Only the NEW vocabulary ever writes this string; an unmarked
111
+ // "shell" row is unreachable from any production writer.
112
+ return marked ? { kind: "shell" } : { kind: "unknown" };
113
+ case "script":
114
+ // Only the NEW vocabulary ever writes this string; an unmarked
115
+ // "script" row is unreachable from any production writer.
116
+ return marked ? { kind: "script" } : { kind: "unknown" };
117
+ case "prompt":
118
+ // Only LEGACY rows (pre-P1b) ever wrote this string.
119
+ return marked ? { kind: "unknown" } : { kind: "command", engine: meta.engine ?? null };
120
+ default:
121
+ return { kind: "unknown" };
122
+ }
123
+ })();
124
+ return {
125
+ id: row.task_id,
126
+ status: row.status,
127
+ startedAt: row.started_at,
128
+ finishedAt: row.completed_at ?? row.failed_at ?? row.started_at,
129
+ durationMs: meta.durationMs,
130
+ log: row.log_path ?? "",
131
+ target,
132
+ ...(meta.detail ? { detail: meta.detail } : {}),
133
+ };
134
+ }
@@ -0,0 +1,179 @@
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
+ * Task-run log persistence: log path resolution, the flat per-run log file,
6
+ * structured logs.db rows, and the shared redaction pass both sinks go
7
+ * through before either is written.
8
+ *
9
+ * Moved body-intact from src/tasks/runner.ts (spec
10
+ * docs/plans/specs/p1b-model-extraction.md §5.1, runner.ts:779-968).
11
+ *
12
+ * A DAG leaf with respect to the rest of src/tasks/run/**: nothing here
13
+ * imports ./task-result, ./task-history, or ./attempt-lifecycle, so every
14
+ * other run/** module can depend on this one without risking an import
15
+ * cycle (tests/architecture/import-cycle-ratchet.test.ts counts type-only
16
+ * imports as real edges too).
17
+ */
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { loadConfig } from "../../core/config/config.js";
21
+ import { rethrowIfTestIsolationError } from "../../core/errors.js";
22
+ import { buildTaskRunId, insertTaskLogLines, openLogsDatabase, } from "../../core/logs-db.js";
23
+ import { getTaskLogDir } from "../../core/paths.js";
24
+ import { redactCredentialPatterns, redactSensitiveText } from "../../core/redaction.js";
25
+ import { collectTaskLogSensitiveValues } from "../log-redaction.js";
26
+ function taskLogPath(logDir, taskId, startedAtIso) {
27
+ const tsSlug = startedAtIso.replace(/[:.]/g, "-");
28
+ return path.join(logDir, taskId, `${tsSlug}.log`);
29
+ }
30
+ export function resolveTaskLogPath(logDir, taskId, startedAtIso) {
31
+ try {
32
+ return taskLogPath(logDir ?? getTaskLogDir(), taskId, startedAtIso);
33
+ }
34
+ catch (error) {
35
+ rethrowIfTestIsolationError(error);
36
+ return "";
37
+ }
38
+ }
39
+ /**
40
+ * Redact logs.db rows against the SAME contiguous text the file sink sees.
41
+ *
42
+ * The rows arrive already split on "\n" (see {@link streamLines}), but the
43
+ * redaction needles are whole env values — and a needle containing a newline
44
+ * can never match inside a single line. Scrubbing row-by-row therefore left
45
+ * multi-line secrets (PEM keys, multi-line service-account credentials) intact
46
+ * in logs.db while the flat .log was correctly scrubbed, defeating all three
47
+ * tiers including the explicit `redact:` opt-in.
48
+ *
49
+ * Consecutive rows sharing a stream and level are rejoined, scrubbed as one
50
+ * string, and re-split, so a needle spanning lines matches. Collapsing a
51
+ * multi-line secret into a single [REDACTED] row is the intended outcome.
52
+ */
53
+ export function scrubDbLines(dbLines, scrub) {
54
+ const out = [];
55
+ for (let i = 0; i < dbLines.length;) {
56
+ const { stream, level } = dbLines[i];
57
+ let end = i;
58
+ while (end < dbLines.length && dbLines[end].stream === stream && dbLines[end].level === level)
59
+ end++;
60
+ const joined = dbLines
61
+ .slice(i, end)
62
+ .map((entry) => entry.line)
63
+ .join("\n");
64
+ for (const line of scrub(joined).split("\n")) {
65
+ if (line.length > 0)
66
+ out.push({ stream, level, line });
67
+ }
68
+ i = end;
69
+ }
70
+ return out;
71
+ }
72
+ /** Split captured pipe output into per-line logs.db rows (blank lines dropped). */
73
+ export function streamLines(text, stream, level) {
74
+ return (text
75
+ .split("\n")
76
+ // Windows child output is CRLF-terminated. Splitting on "\n" alone left a
77
+ // trailing "\r" on every row and turned blank CRLF lines into phantom
78
+ // rows containing just "\r" (length 1 passes the filter below).
79
+ .map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line))
80
+ .filter((line) => line.length > 0)
81
+ .map((line) => ({ stream, level, line })));
82
+ }
83
+ /**
84
+ * Exact secret values to scrub from this run's persisted output (#755).
85
+ *
86
+ * Best-effort by construction: this runs on the persistence path of a run that
87
+ * has already finished, so a config that will not load must degrade to
88
+ * "pattern-based redaction only" rather than fail the run. It does NOT degrade
89
+ * to "log it anyway with no redaction at all" — `redactCredentialPatterns`
90
+ * still runs unconditionally in the caller.
91
+ */
92
+ function taskLogSensitiveValues(redactNames, environment) {
93
+ const env = { ...process.env, ...environment };
94
+ try {
95
+ return collectTaskLogSensitiveValues({
96
+ env,
97
+ config: loadConfig(),
98
+ declaredNames: redactNames,
99
+ });
100
+ }
101
+ catch (error) {
102
+ rethrowIfTestIsolationError(error);
103
+ // No config — the name heuristic and the task's own `redact:` list still apply.
104
+ try {
105
+ return collectTaskLogSensitiveValues({ env, declaredNames: redactNames });
106
+ }
107
+ catch (fallbackError) {
108
+ rethrowIfTestIsolationError(fallbackError);
109
+ return [];
110
+ }
111
+ }
112
+ }
113
+ /** Scrub one piece of durable-result text against a prepared task's own secrets. */
114
+ export function scrubTaskOutput(task, text) {
115
+ const patterned = redactCredentialPatterns(text);
116
+ const sensitive = taskLogSensitiveValues(task.redact, task.environment);
117
+ return sensitive.length > 0 ? redactSensitiveText(patterned, sensitive) : patterned;
118
+ }
119
+ /**
120
+ * Persist a finished run's log: the flat text file (so `log_path` in
121
+ * task_history keeps resolving for humans and older consumers) plus
122
+ * structured rows in logs.db keyed by `buildTaskRunId(taskId, startedAt)`.
123
+ *
124
+ * Both sinks are pattern-redacted (`redactCredentialPatterns`) before being
125
+ * written — task output is raw command/agent/LLM text that can echo a
126
+ * credential-bearing URL (e.g. a Discord webhook) nothing upstream expects to
127
+ * scrub.
128
+ *
129
+ * The DB write is best-effort, mirroring history recording: an unwritable
130
+ * logs.db must never fail a task run.
131
+ */
132
+ export function persistRunLog(input) {
133
+ // Two arms, and both are needed. `redactCredentialPatterns` catches
134
+ // credential SHAPES nobody listed; the exact-value pass catches configured
135
+ // secrets whose value is shaped like nothing in particular (#755). The
136
+ // command target had only the first, so a scheduled command that echoed an
137
+ // ordinary-looking secret persisted it verbatim to both sinks. Applying the
138
+ // exact pass here — the one sink all three target kinds funnel through —
139
+ // covers every arm once rather than per-arm; prompt/workflow runs already
140
+ // scrub upstream, and redaction is idempotent, so the overlap is free.
141
+ const sensitive = taskLogSensitiveValues(input.redactNames, input.environment);
142
+ const scrub = (text) => sensitive.length > 0
143
+ ? redactSensitiveText(redactCredentialPatterns(text), sensitive)
144
+ : redactCredentialPatterns(text);
145
+ const fileText = scrub(input.fileText);
146
+ const dbLines = scrubDbLines(input.dbLines, scrub);
147
+ if (input.logPath) {
148
+ try {
149
+ // Written at the process umask. #756 pinned 0600/0700 here; that went out
150
+ // with the rest of akm's permission enforcement (#791) — the operator owns
151
+ // the mode of their own data directory, and akm neither sets nor reports
152
+ // on it.
153
+ fs.mkdirSync(path.dirname(input.logPath), { recursive: true });
154
+ fs.writeFileSync(input.logPath, fileText);
155
+ }
156
+ catch (error) {
157
+ rethrowIfTestIsolationError(error);
158
+ // Transitional file logging is fully best-effort.
159
+ }
160
+ }
161
+ try {
162
+ const db = openLogsDatabase();
163
+ try {
164
+ insertTaskLogLines(db, {
165
+ taskId: input.taskId,
166
+ runId: buildTaskRunId(input.taskId, input.startedAtIso),
167
+ ts: input.finishedAtIso,
168
+ lines: dbLines,
169
+ });
170
+ }
171
+ finally {
172
+ db.close();
173
+ }
174
+ }
175
+ catch (error) {
176
+ rethrowIfTestIsolationError(error);
177
+ // Structured logging is fully best-effort and must not alter CLI output.
178
+ }
179
+ }
@@ -0,0 +1,19 @@
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 exit code surfaced to the OS scheduler. Mapped from {@link TaskRunStatus}
6
+ * so cron / launchd / schtasks see a useful return value.
7
+ */
8
+ export function exitCodeForStatus(status) {
9
+ switch (status) {
10
+ case "completed":
11
+ return 0;
12
+ case "active":
13
+ return 0;
14
+ case "blocked":
15
+ return 1;
16
+ case "failed":
17
+ return 1;
18
+ }
19
+ }
@@ -12,6 +12,7 @@
12
12
  import { createHash } from "node:crypto";
13
13
  import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
14
14
  import { UsageError } from "../core/errors.js";
15
+ import { canonicalInputJson } from "../execution/input-contract.js";
15
16
  import { normaliseTaskConceptId } from "./task-id.js";
16
17
  export function compileTaskSchedulerBindings(input) {
17
18
  const id = normaliseTaskConceptId(input.id);
@@ -19,9 +20,20 @@ export function compileTaskSchedulerBindings(input) {
19
20
  const bundle = parseBundleRef(ref).bundle;
20
21
  if (!bundle)
21
22
  throw new Error("invariant: qualified scheduler task ref lost its bundle");
22
- const invocation = Object.freeze(["task", "run", id, "--bundle", bundle, "--scheduled"]);
23
+ // P2b Lane B (B-N3): the invocation is compiled PER schedule entry now —
24
+ // each entry's own `inputs` produces its own trailing flag tail, so it can
25
+ // no longer be hoisted as one binding shared by every entry.
23
26
  return Object.freeze(input.schedules.map((schedule) => {
24
27
  const bindingId = schedule.ordinal === 0 ? id : digestBindingId("task", ref, schedule.ordinal);
28
+ const invocation = Object.freeze([
29
+ "task",
30
+ "run",
31
+ id,
32
+ "--bundle",
33
+ bundle,
34
+ "--scheduled",
35
+ ...schedulerInputFlagTail(schedule.inputs),
36
+ ]);
25
37
  return freezeBinding({
26
38
  id: bindingId,
27
39
  nativeId: schedulerNativeBindingId(bindingId),
@@ -29,11 +41,63 @@ export function compileTaskSchedulerBindings(input) {
29
41
  cron: schedule.cron,
30
42
  source: schedule.source,
31
43
  ordinal: schedule.ordinal,
32
- enabled: input.enabled,
44
+ enabled: schedule.enabled ?? input.enabled,
33
45
  invocation,
34
46
  });
35
47
  }));
36
48
  }
49
+ /**
50
+ * The canonically-sorted `--<name> <value>` flag tail for one schedule
51
+ * entry's `inputs` (spec §1.7 B-N3). Empty/absent `inputs` yields an empty
52
+ * tail — the fixed six-token invocation, byte-identical to every schedule
53
+ * entry before P2b (B-03).
54
+ *
55
+ * Code-review finding (scheduler-binding.ts:536): a value whose exact text
56
+ * begins with `-` (a negative number, or a string that just happens to
57
+ * start with a dash) is indistinguishable from a NEW flag in the two-token
58
+ * `--<name> <value>` form — `isValidSchedulerInputFlagTail`
59
+ * (scheduler-invocation.ts) refuses it outright, and even a looser parser
60
+ * would still be wrong for a non-numeric dash-leading string: the real `akm
61
+ * task run` flag parser (`parseTaskInputFlags`, tasks-cli.ts) only
62
+ * special-cases a dash-DIGIT lead, so it would silently treat `--scope
63
+ * -urgent` as the boolean flag `--scope` followed by an orphaned,
64
+ * dropped `-urgent` token. The inline `--<name>=<value>` form has no such
65
+ * ambiguity on EITHER side — `isValidSchedulerInputFlagTail` accepts it
66
+ * unconditionally, and `parseTaskInputFlags`'s own inline-`=` branch splits
67
+ * on the first `=` without ever inspecting the value's leading character —
68
+ * so it is used whenever the value's exact text would otherwise be
69
+ * ambiguous. Every other value keeps the existing two-token form
70
+ * byte-identical to before this fix (B-03/B-45).
71
+ */
72
+ function schedulerInputFlagTail(inputs) {
73
+ if (!inputs)
74
+ return [];
75
+ const names = Object.keys(inputs).sort();
76
+ const tail = [];
77
+ for (const name of names) {
78
+ const text = schedulerInputFlagValueText(inputs[name]);
79
+ if (text.startsWith("-"))
80
+ tail.push(`--${name}=${text}`);
81
+ else
82
+ tail.push(`--${name}`, text);
83
+ }
84
+ return tail;
85
+ }
86
+ /**
87
+ * One input value's argv text. A scalar is its exact text — `String(value)`
88
+ * for a number/boolean (a `true` boolean is `"true"`, never a bare flag, so
89
+ * the tail round-trips through one parser), the string itself for a string.
90
+ * An object/array value is its `canonicalInputJson` text, which
91
+ * `materializeInputFlags`'s JSON-shorthand path already coerces back through
92
+ * the declaration.
93
+ */
94
+ function schedulerInputFlagValueText(value) {
95
+ if (typeof value === "string")
96
+ return value;
97
+ if (typeof value === "number" || typeof value === "boolean")
98
+ return String(value);
99
+ return canonicalInputJson(value);
100
+ }
37
101
  export function compileWorkflowSchedulerBindings(input) {
38
102
  const ref = assertQualifiedRef(input.qualifiedRef, "workflow");
39
103
  const invocation = Object.freeze(["workflow", "run", ref]);
@@ -8,6 +8,7 @@ import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
8
8
  import { resolveStashDir } from "../core/common.js";
9
9
  import { ConfigError } from "../core/errors.js";
10
10
  import { getCacheDir, getConfigDir, getDataDir, getTaskContextDir } from "../core/paths.js";
11
+ import { INPUT_NAME_PATTERN } from "../execution/input-contract.js";
11
12
  import { normaliseTaskConceptId } from "./task-id.js";
12
13
  export const SCHEDULED_TASK_CONTEXT_KEYS = [
13
14
  "AKM_BUNDLE_DIR",
@@ -205,12 +206,20 @@ function parsePublicSchedulerInvocation(invocation) {
205
206
  return undefined;
206
207
  index += 2;
207
208
  }
208
- if (invocation[index] !== "--scheduled" || index !== invocation.length - 1)
209
+ if (invocation[index] !== "--scheduled")
210
+ return undefined;
211
+ // P2b Lane B (spec §4.4, §1.7 B-N3): zero or more `--<name> <value>`
212
+ // schedule-supplied input flags may follow `--scheduled` — the same
213
+ // trailing tail `compileTaskSchedulerBindings` compiles from
214
+ // `schedule[i].inputs`. Absent/empty is the pre-P2b shape, byte-identical
215
+ // (B-03).
216
+ if (!isValidSchedulerInputFlagTail(invocation.slice(index + 1)))
209
217
  return undefined;
210
218
  return { invocation: [...invocation], ...(target !== undefined ? { target } : {}) };
211
219
  }
212
- if (invocation[0] !== "workflow" || invocation[1] !== "run" || invocation.length !== 3)
220
+ if (invocation[0] !== "workflow" || invocation[1] !== "run" || invocation.length !== 3) {
213
221
  return undefined;
222
+ }
214
223
  const ref = invocation[2];
215
224
  if (!ref)
216
225
  return undefined;
@@ -224,6 +233,56 @@ function parsePublicSchedulerInvocation(invocation) {
224
233
  return undefined;
225
234
  }
226
235
  }
236
+ /**
237
+ * Validate an OPTIONAL trailing schedule-input flag tail (spec §1.7 B-N3):
238
+ * empty is valid (the pre-P2b shape). Otherwise the tail is a sequence of
239
+ * entries, each EITHER:
240
+ *
241
+ * - a single inline `--<name>=<value>` token (code-review finding,
242
+ * scheduler-binding.ts:536 — the ONLY encoding a dash-leading value's
243
+ * exact text can round-trip through, since `<value>` here is everything
244
+ * after the first `=`, whatever its leading character); or
245
+ * - a `(--<name>, <value>)` pair, where `<value>` is a single non-flag
246
+ * token (does not start with `-`) — the pre-P2b shape.
247
+ *
248
+ * In both forms `<name>` matches {@link INPUT_NAME_PATTERN} and no name
249
+ * repeats. A bare flag, a repeated name, a flag-shaped value in the pair
250
+ * form, or a malformed token are all refused. This validates SHAPE only;
251
+ * the real materialization against the task's declared contract happens
252
+ * through the same `parseTaskInputFlags` + `materializeInputFlags` path
253
+ * `akm task run --<name>` already uses (B-48) — `parseTaskInputFlags`
254
+ * accepts both forms natively (its inline-`=` branch never inspects the
255
+ * value's leading character).
256
+ */
257
+ function isValidSchedulerInputFlagTail(tail) {
258
+ if (tail.length === 0)
259
+ return true;
260
+ const seen = new Set();
261
+ let index = 0;
262
+ while (index < tail.length) {
263
+ const token = tail[index];
264
+ if (!token || !token.startsWith("--"))
265
+ return false;
266
+ const body = token.slice(2);
267
+ const equalsAt = body.indexOf("=");
268
+ if (equalsAt !== -1) {
269
+ const name = body.slice(0, equalsAt);
270
+ if (!INPUT_NAME_PATTERN.test(name) || seen.has(name))
271
+ return false;
272
+ seen.add(name);
273
+ index += 1;
274
+ continue;
275
+ }
276
+ if (!INPUT_NAME_PATTERN.test(body) || seen.has(body))
277
+ return false;
278
+ const value = tail[index + 1];
279
+ if (value === undefined || value.startsWith("-"))
280
+ return false;
281
+ seen.add(body);
282
+ index += 2;
283
+ }
284
+ return true;
285
+ }
227
286
  function canonicalContext(input) {
228
287
  const inputKeys = Object.keys(input);
229
288
  if (inputKeys.length !== SCHEDULED_TASK_CONTEXT_KEYS.length ||
@@ -325,5 +384,6 @@ function invalidSchedulerContext() {
325
384
  return new ConfigError(`Invalid scheduler context; expected exactly ${SCHEDULED_TASK_CONTEXT_KEYS.join(", ")} as absolute paths.`, "INVALID_CONFIG_FILE");
326
385
  }
327
386
  function invalidSchedulerInvocation() {
328
- return new ConfigError("Invalid scheduler invocation; expected public `task run <id> [--bundle <bundle>] --scheduled` or `workflow run <qualified-ref>` argv.", "INVALID_CONFIG_FILE");
387
+ return new ConfigError("Invalid scheduler invocation; expected public " +
388
+ "`task run <id> [--bundle <bundle>] --scheduled [--<input> <value>…]` or `workflow run <qualified-ref>` argv.", "INVALID_CONFIG_FILE");
329
389
  }