akm-cli 0.9.11 → 0.9.12

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 (127) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/STABILITY.md +6 -1
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/cli/shared.js +16 -4
  8. package/dist/cli.js +15 -13
  9. package/dist/commands/agent/agent-dispatch.js +8 -0
  10. package/dist/commands/command/execution-source-loader.js +25 -22
  11. package/dist/commands/command/portable-template.js +4 -26
  12. package/dist/commands/config-cli.js +10 -4
  13. package/dist/commands/env/env-binding.js +10 -3
  14. package/dist/commands/env/env-cli.js +7 -0
  15. package/dist/commands/env/secret-cli.js +15 -4
  16. package/dist/commands/health/checks.js +186 -71
  17. package/dist/commands/health.js +16 -4
  18. package/dist/commands/improve/distill/quality-gate.js +2 -2
  19. package/dist/commands/improve/distill.js +28 -12
  20. package/dist/commands/improve/execution.js +1 -2
  21. package/dist/commands/improve/extract.js +82 -56
  22. package/dist/commands/improve/improve-strategies.js +26 -8
  23. package/dist/commands/improve/improve.js +13 -0
  24. package/dist/commands/improve/preparation.js +9 -6
  25. package/dist/commands/improve/reflect.js +61 -77
  26. package/dist/commands/lint/index.js +3 -1
  27. package/dist/commands/migrate-cli.js +6 -4
  28. package/dist/commands/proposal/drain-policies.js +22 -2
  29. package/dist/commands/proposal/repository.js +4 -4
  30. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  31. package/dist/commands/proposal/validators/proposals.js +10 -19
  32. package/dist/commands/read/show.js +42 -31
  33. package/dist/commands/registry-cli.js +4 -2
  34. package/dist/commands/sources/init.js +4 -8
  35. package/dist/commands/sources/self-update.js +2 -2
  36. package/dist/commands/sources/source-clone.js +5 -7
  37. package/dist/commands/sources/sources-cli.js +3 -5
  38. package/dist/commands/tasks/tasks-cli.js +4 -12
  39. package/dist/commands/tasks/tasks.js +38 -35
  40. package/dist/commands/workflow-cli.js +17 -15
  41. package/dist/core/activation-policy.js +31 -3
  42. package/dist/core/adapter/execution-source.js +39 -11
  43. package/dist/core/asset/stash-meta.js +7 -41
  44. package/dist/core/common.js +8 -17
  45. package/dist/core/config/config-schema.js +3 -23
  46. package/dist/core/config/config-walker.js +50 -4
  47. package/dist/core/config/config.js +21 -5
  48. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  49. package/dist/core/config/schema/embedding.js +2 -2
  50. package/dist/core/config/schema/engines.js +2 -2
  51. package/dist/core/config/schema/index-config.js +19 -21
  52. package/dist/core/config/schema/primitives.js +21 -10
  53. package/dist/core/config/schema/sources-bundles.js +1 -6
  54. package/dist/core/errors.js +2 -3
  55. package/dist/core/improve-types.js +17 -0
  56. package/dist/core/json-schema.js +1 -11
  57. package/dist/core/maintenance-barrier.js +17 -2
  58. package/dist/core/paths.js +12 -15
  59. package/dist/core/state/migrations.js +28 -0
  60. package/dist/core/state-db.js +28 -1
  61. package/dist/core/write-source.js +6 -6
  62. package/dist/indexer/bundle-identity-guard.js +3 -0
  63. package/dist/indexer/ensure-index.js +5 -0
  64. package/dist/indexer/indexer.js +11 -3
  65. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  66. package/dist/indexer/passes/metadata.js +16 -5
  67. package/dist/indexer/search/search-fields.js +1 -30
  68. package/dist/integrations/agent/engine-resolution.js +15 -1
  69. package/dist/integrations/agent/model-map.js +16 -10
  70. package/dist/integrations/agent/prompts.js +13 -6
  71. package/dist/integrations/lockfile.js +22 -7
  72. package/dist/llm/client.js +16 -0
  73. package/dist/llm/index-passes.js +3 -2
  74. package/dist/output/shapes/passthrough.js +9 -3
  75. package/dist/output/shapes.js +5 -3
  76. package/dist/output/text/workflow-format.js +8 -1
  77. package/dist/scripts/akm-migrate-node.js +1461 -1267
  78. package/dist/scripts/akm-migrate.js +1460 -1266
  79. package/dist/setup/setup.js +14 -21
  80. package/dist/sources/include.js +150 -20
  81. package/dist/sources/providers/git-install.js +14 -12
  82. package/dist/sources/providers/git-provider.js +3 -3
  83. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  84. package/dist/sources/website-url.js +12 -4
  85. package/dist/storage/engines/sqlite-migrations.js +40 -10
  86. package/dist/storage/like-pattern.js +7 -0
  87. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  88. package/dist/storage/repositories/index-connection.js +27 -10
  89. package/dist/storage/repositories/index-entry-schema.js +19 -2
  90. package/dist/storage/repositories/index-schema.js +30 -9
  91. package/dist/storage/repositories/proposals-repository.js +2 -1
  92. package/dist/storage/repositories/task-history-repository.js +14 -7
  93. package/dist/storage/repositories/workflow-runs-repository.js +15 -1
  94. package/dist/storage/sqlite-read-snapshot.js +11 -9
  95. package/dist/tasks/backends/cron.js +34 -5
  96. package/dist/tasks/backends/launchd.js +23 -26
  97. package/dist/tasks/backends/schtasks.js +50 -3
  98. package/dist/tasks/frozen-script.js +2 -0
  99. package/dist/tasks/prepare/prepare.js +2 -7
  100. package/dist/tasks/prepare/script-capture.js +38 -6
  101. package/dist/tasks/schedule.js +154 -13
  102. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  103. package/dist/tasks/source/task-source-v4.js +0 -1
  104. package/dist/workflows/exec/child-workflow.js +2 -3
  105. package/dist/workflows/exec/exec-unit.js +3 -4
  106. package/dist/workflows/exec/run-workflow.js +19 -10
  107. package/dist/workflows/exec/step-work.js +35 -56
  108. package/dist/workflows/freeze/resolve-steps.js +19 -11
  109. package/dist/workflows/freeze/source-freeze.js +7 -0
  110. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  111. package/dist/workflows/freeze/targets/command.js +14 -2
  112. package/dist/workflows/ir/environment-v4.js +4 -2
  113. package/dist/workflows/ir/freeze-v4.js +2 -5
  114. package/dist/workflows/ir/plan-hash.js +0 -3
  115. package/dist/workflows/ir/schema-v4.js +14 -9
  116. package/dist/workflows/ir/schema.js +1 -3
  117. package/dist/workflows/resource-limits.js +35 -48
  118. package/dist/workflows/runtime/plan-classifier.js +89 -41
  119. package/dist/workflows/runtime/run-outputs.js +1 -21
  120. package/dist/workflows/runtime/runs.js +76 -150
  121. package/dist/workflows/source-files.js +28 -54
  122. package/dist/workflows/source-ir/program.js +2 -2
  123. package/dist/workflows/source-ir/semantics.js +5 -23
  124. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  125. package/docs/reference/cli.md +74 -17
  126. package/package.json +1 -1
  127. package/schemas/akm-config.json +5 -10
@@ -33,17 +33,49 @@ const SCRIPT_INTERPRETERS = Object.freeze({
33
33
  ".kt": "kotlin",
34
34
  ".kts": "kotlin",
35
35
  });
36
- export function scriptInterpreter(extension, ref) {
37
- const interpreter = SCRIPT_INTERPRETERS[extension];
36
+ const SHEBANG_INTERPRETERS = [
37
+ [/^(bash|sh|zsh|dash|ksh)$/, "sh"],
38
+ [/^python[0-9.]*$/, "python"],
39
+ [/^ruby$/, "ruby"],
40
+ [/^perl$/, "perl"],
41
+ [/^php$/, "php"],
42
+ [/^lua$/, "lua"],
43
+ [/^node$/, "node"],
44
+ ];
45
+ function interpreterFromShebang(bytes) {
46
+ if (!bytes || bytes.length < 2 || bytes[0] !== 0x23 || bytes[1] !== 0x21)
47
+ return undefined;
48
+ const newline = bytes.indexOf(0x0a);
49
+ const lineBytes = newline === -1 ? bytes.subarray(2) : bytes.subarray(2, newline);
50
+ const tokens = Buffer.from(lineBytes).toString("utf8").trim().split(/\s+/).filter(Boolean);
51
+ const [first, second] = tokens;
52
+ const named = first?.split("/").pop() === "env" ? second : first?.split("/").pop();
53
+ if (!named)
54
+ return undefined;
55
+ for (const [pattern, interpreter] of SHEBANG_INTERPRETERS) {
56
+ if (pattern.test(named))
57
+ return interpreter;
58
+ }
59
+ return undefined;
60
+ }
61
+ function nodeStripsTypeScript() {
62
+ const features = process.features;
63
+ return Boolean(features?.typescript);
64
+ }
65
+ export function scriptInterpreter(extension, ref, bytes) {
66
+ const interpreter = SCRIPT_INTERPRETERS[extension] ?? (extension === "" ? interpreterFromShebang(bytes) : undefined);
38
67
  if (!interpreter) {
39
68
  throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} has no closed runtime interpreter for extension ${JSON.stringify(extension)}.`, "TASK_TARGET_UNSUPPORTED");
40
69
  }
41
70
  if (interpreter !== "bun")
42
71
  return interpreter;
43
- if (!process.versions.bun) {
44
- throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} requires Bun for ${extension} execution, but this runtime cannot provide it.`, "TASK_TARGET_UNSUPPORTED");
72
+ if (process.versions.bun) {
73
+ return isBunStandaloneMain() ? "bun-standalone" : "bun";
74
+ }
75
+ if (extension === ".js" || (extension === ".ts" && nodeStripsTypeScript())) {
76
+ return "node";
45
77
  }
46
- return isBunStandaloneMain() ? "bun-standalone" : "bun";
78
+ throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} requires Bun for ${extension} execution, but this runtime cannot provide it.`, "TASK_TARGET_UNSUPPORTED");
47
79
  }
48
80
  export function captureDirectoryIdentity(bundleRoot, workingDirectory) {
49
81
  try {
@@ -69,7 +101,7 @@ export function captureScriptTarget(ref, file, bundleRoot, readFile) {
69
101
  const bytes = Uint8Array.from(raw);
70
102
  const cwdIdentity = captureDirectoryIdentity(bundleRoot);
71
103
  return Object.freeze({
72
- interpreter: scriptInterpreter(extension, ref),
104
+ interpreter: scriptInterpreter(extension, ref, bytes),
73
105
  extension,
74
106
  bytesBase64: Buffer.from(bytes).toString("base64"),
75
107
  byteLength: bytes.byteLength,
@@ -15,10 +15,13 @@
15
15
  * • a launchd plist `<StartCalendarInterval>` / `<StartInterval>` (macOS),
16
16
  * • Task Scheduler XML triggers (Windows).
17
17
  *
18
- * The shared subset is `*`, single integers, `*\/N`, `A-B/N`, plus the `@hourly /
19
- * @daily / @weekly / @monthly` aliases. Patterns outside that — multi-value
20
- * lists, plain ranges, day-of-month AND
21
- * day-of-week combinations are rejected with a {@link UsageError}.
18
+ * The shared subset is `*`, single integers, `*\/N`, `A-B`, `A-B/N`, comma
19
+ * lists, three-letter day/month names (`MON`, `JAN`) and name ranges
20
+ * (`MON-FRI`), plus the `@hourly / @daily / @weekly / @monthly / @yearly /
21
+ * @annually / @reboot` aliases. Day-of-month AND day-of-week combined in the
22
+ * same expression are rejected with a {@link UsageError} — cron gives that
23
+ * combination OR semantics no other backend can express portably, and it is
24
+ * genuinely ambiguous to a person reading the schedule back.
22
25
  *
23
26
  * Cron is the most permissive of the three backends; some patterns it
24
27
  * accepts (e.g. `@hourly` = `0 * * * *`) have no clean schtasks primitive.
@@ -33,7 +36,58 @@ const ALIAS_TO_CRON = {
33
36
  "@midnight": "0 0 * * *",
34
37
  "@weekly": "0 0 * * 0",
35
38
  "@monthly": "0 0 1 * *",
39
+ "@yearly": "0 0 1 1 *",
40
+ "@annually": "0 0 1 1 *",
36
41
  };
42
+ /**
43
+ * vixie-cron's one true nickname with no 5-field equivalent — it fires once at
44
+ * daemon startup, not on a recurring calendar boundary. Passed straight
45
+ * through, unexpanded, on the cron backend (a real crontab line is just
46
+ * `@reboot <command>`); launchd and schtasks have no "run at boot" trigger in
47
+ * the vocabulary this module targets, so those backends reject it.
48
+ */
49
+ const REBOOT_ALIAS = "@reboot";
50
+ /** Placeholder fields for the `@reboot` `ScheduleSpec` — never read: cron's translator emits `spec.cron` verbatim, and launchd/schtasks refuse `@reboot` before touching `fields`. */
51
+ const REBOOT_FIELDS = {
52
+ minute: { kind: "star" },
53
+ hour: { kind: "star" },
54
+ dom: { kind: "star" },
55
+ month: { kind: "star" },
56
+ dow: { kind: "star" },
57
+ };
58
+ const DOW_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
59
+ const MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
60
+ /**
61
+ * Substitute three-letter day-of-week / month names (and name ranges, e.g.
62
+ * `MON-FRI`) with their numeric cron equivalents, case-insensitively, so the
63
+ * existing numeric grammar below (value / range / list) parses the result
64
+ * unchanged. Tokens that are not recognized names pass through untouched —
65
+ * they are numbers already, or genuinely invalid and left for the numeric
66
+ * parsers to reject with their own message.
67
+ */
68
+ function substituteNamedTokens(raw, fieldName) {
69
+ const names = fieldName === "month" ? MONTH_NAMES : DOW_NAMES;
70
+ const offset = fieldName === "month" ? 1 : 0;
71
+ const indexOf = (token) => {
72
+ const idx = names.indexOf(token.toLowerCase());
73
+ return idx === -1 ? undefined : idx + offset;
74
+ };
75
+ return raw
76
+ .split(",")
77
+ .map((part) => {
78
+ const rangeMatch = part.match(/^([A-Za-z]{3})-([A-Za-z]{3})$/);
79
+ if (rangeMatch) {
80
+ const start = indexOf(rangeMatch[1]);
81
+ const end = indexOf(rangeMatch[2]);
82
+ if (start !== undefined && end !== undefined)
83
+ return `${start}-${end}`;
84
+ return part;
85
+ }
86
+ const single = /^[A-Za-z]{3}$/.test(part) ? indexOf(part) : undefined;
87
+ return single !== undefined ? String(single) : part;
88
+ })
89
+ .join(",");
90
+ }
37
91
  const FIELD_LIMITS = {
38
92
  minute: { min: 0, max: 59 },
39
93
  hour: { min: 0, max: 23 },
@@ -41,10 +95,17 @@ const FIELD_LIMITS = {
41
95
  month: { min: 1, max: 12 },
42
96
  dow: { min: 0, max: 6 },
43
97
  };
44
- const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), steps (`*/N`, `A-B/N`), and comma lists (`7,37`). " +
45
- "Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`. " +
46
- "Plain ranges and named days/months are not supported.";
98
+ const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), ranges (`A-B`), steps (`*/N`, `A-B/N`), and comma lists " +
99
+ "(`7,37`). Day-of-week and month fields also accept three-letter names and name ranges (`MON`, `MON-FRI`, " +
100
+ "`JAN`). Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`/`@annually`, `@reboot` (cron only).";
47
101
  export function parseSchedule(input, backend) {
102
+ const trimmed = input.trim();
103
+ if (trimmed.toLowerCase() === REBOOT_ALIAS) {
104
+ if (backend !== "cron") {
105
+ throw new UsageError(`Schedule "${input}" (@reboot, run once at startup) has no ${backend === "launchd" ? "macOS launchd" : "Windows Task Scheduler"} equivalent this release expresses. Use a cron-backed install, or rewrite the task with a recurring schedule.`, "INVALID_FLAG_VALUE");
106
+ }
107
+ return { raw: input, cron: REBOOT_ALIAS, fields: REBOOT_FIELDS };
108
+ }
48
109
  const cron = expandAlias(input);
49
110
  const fields = parseCronFields(cron, input);
50
111
  const spec = { raw: input, cron, fields };
@@ -84,7 +145,8 @@ function parseCronFields(cron, original) {
84
145
  dow: parseField(dow, "day-of-week", FIELD_LIMITS.dow, original),
85
146
  };
86
147
  }
87
- function parseField(raw, name, limit, original) {
148
+ function parseField(rawInput, name, limit, original) {
149
+ const raw = name === "month" || name === "day-of-week" ? substituteNamedTokens(rawInput, name) : rawInput;
88
150
  if (raw === "*")
89
151
  return { kind: "star" };
90
152
  const stepMatch = raw.match(/^\*\/(\d+)$/);
@@ -109,6 +171,19 @@ function parseField(raw, name, limit, original) {
109
171
  }
110
172
  return { kind: "rangeStep", start, end, step };
111
173
  }
174
+ // Plain range, no step: `A-B` (e.g. `1-5` for Mon-Fri, `1-5` for the first
175
+ // five days of the month). Represented as a `rangeStep` with `step: 1` so
176
+ // every downstream consumer (verbatim cron passthrough, launchd/schtasks
177
+ // expansion) reuses the exact same handling a stepped range already gets.
178
+ const rangeMatch = raw.match(/^(\d+)-(\d+)$/);
179
+ if (rangeMatch) {
180
+ const start = Number(rangeMatch[1]);
181
+ const end = Number(rangeMatch[2]);
182
+ if (start < limit.min || end > limit.max || start > end) {
183
+ throw new UsageError(`Invalid ${name} range "${raw}" in schedule "${original}" (allowed ${limit.min}-${limit.max}).`, "INVALID_FLAG_VALUE");
184
+ }
185
+ return { kind: "rangeStep", start, end, step: 1 };
186
+ }
112
187
  if (/^\d+$/.test(raw)) {
113
188
  const value = Number(raw);
114
189
  if (value < limit.min || value > limit.max) {
@@ -161,6 +236,42 @@ export function translateToLaunchd(spec) {
161
236
  })),
162
237
  };
163
238
  }
239
+ // A day-of-week or month range/list (`1-5` for Mon-Fri, `1,3,5`) has no
240
+ // single-dict launchd primitive either, but — unlike a step — it names a
241
+ // small, closed set of concrete values, so it genuinely IS expressible: one
242
+ // calendar dict per (month, weekday) combination, same trick as the
243
+ // minute/hour step expansion above. Only attempted when the remaining
244
+ // fields are already single values or `*`; anything else (e.g. a minute
245
+ // step combined with a weekday range) falls through to the generic path
246
+ // below, where `rejectStepInsideCalendar` reports the field it actually
247
+ // cannot express.
248
+ const dowValues = expandListLikeField(f.dow, FIELD_LIMITS.dow);
249
+ const monthValues = expandListLikeField(f.month, FIELD_LIMITS.month);
250
+ const remainingFieldsAreSimple = (f.minute.kind === "value" || f.minute.kind === "star") &&
251
+ (f.hour.kind === "value" || f.hour.kind === "star") &&
252
+ (f.dom.kind === "value" || f.dom.kind === "star");
253
+ if ((dowValues || monthValues) && remainingFieldsAreSimple) {
254
+ const base = {};
255
+ if (f.minute.kind === "value")
256
+ base.Minute = f.minute.value;
257
+ if (f.hour.kind === "value")
258
+ base.Hour = f.hour.value;
259
+ if (f.dom.kind === "value")
260
+ base.Day = f.dom.value;
261
+ const months = monthValues ?? (f.month.kind === "value" ? [f.month.value] : [undefined]);
262
+ const weekdays = dowValues ?? (f.dow.kind === "value" ? [f.dow.value] : [undefined]);
263
+ const calendars = [];
264
+ for (const month of months) {
265
+ for (const weekday of weekdays) {
266
+ calendars.push({
267
+ ...base,
268
+ ...(month !== undefined ? { Month: month } : {}),
269
+ ...(weekday !== undefined ? { Weekday: weekday } : {}),
270
+ });
271
+ }
272
+ }
273
+ return { calendars };
274
+ }
164
275
  // Otherwise build a calendar dict from concrete values. launchd treats any
165
276
  // omitted key as "every value", so a `*` field translates to "no key".
166
277
  // Exception: launchd does not support arbitrary step values inside a
@@ -199,12 +310,21 @@ function expandFieldValues(field, limit) {
199
310
  values.push(value);
200
311
  return values;
201
312
  }
313
+ /** Discrete values named by a `list` or (stepless) `rangeStep` field; `null` for anything else (`*`, `value`, `step`). */
314
+ function expandListLikeField(field, limit) {
315
+ if (field.kind === "list")
316
+ return field.values;
317
+ if (field.kind === "rangeStep")
318
+ return expandFieldValues(field, limit);
319
+ return null;
320
+ }
202
321
  function rejectStepInsideCalendar(field, name, spec) {
203
322
  if (field.kind === "step") {
204
323
  throw new UsageError(`Schedule "${spec.raw}" uses step (${name} = */N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either restrict the step to the minute or hour field only, or rewrite the schedule with concrete values.");
205
324
  }
206
325
  if (field.kind === "rangeStep") {
207
- throw new UsageError(`Schedule "${spec.raw}" uses range-step (${name} = A-B/N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Restrict the range-step to the minute or hour field, or rewrite the schedule with a concrete value.");
326
+ const shape = field.step === 1 ? "A-B" : "A-B/N";
327
+ throw new UsageError(`Schedule "${spec.raw}" uses a range (${name} = ${shape}) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Restrict the range to the minute, hour, day-of-week, or month field, or rewrite the schedule with a concrete value.");
208
328
  }
209
329
  if (field.kind === "list") {
210
330
  throw new UsageError(`Schedule "${spec.raw}" uses comma list (${name} = a,b,...) which macOS launchd cannot express as a single trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either install one task per list element, or rewrite the schedule with a step (`*/N`) or single value.");
@@ -286,20 +406,41 @@ export function translateToSchtasks(spec) {
286
406
  f.dow.kind === "star") {
287
407
  return { kind: "daily", atHour: f.hour.value, atMinute: f.minute.value };
288
408
  }
289
- // `M H * * D` → WEEKLY at H:M on day D.
409
+ // `M H * * D` → WEEKLY at H:M on day(s) D — a single day, a plain range
410
+ // (`1-5`, e.g. from `MON-FRI`), or a comma list all name a small closed set
411
+ // of weekdays that Task Scheduler's native `<DaysOfWeek>` already takes as
412
+ // a set, so no expansion into multiple triggers is needed.
290
413
  if (f.minute.kind === "value" &&
291
414
  f.hour.kind === "value" &&
292
415
  f.dom.kind === "star" &&
293
416
  f.month.kind === "star" &&
294
- f.dow.kind === "value") {
417
+ (f.dow.kind === "value" || f.dow.kind === "list" || f.dow.kind === "rangeStep")) {
418
+ const daysOfWeek = f.dow.kind === "value" ? [f.dow.value] : expandListLikeField(f.dow, FIELD_LIMITS.dow);
295
419
  return {
296
420
  kind: "weekly",
297
421
  atHour: f.hour.value,
298
422
  atMinute: f.minute.value,
299
- daysOfWeek: [f.dow.value],
423
+ daysOfWeek,
300
424
  };
301
425
  }
302
- throw new UsageError(`Schedule "${spec.raw}" cannot be expressed as a Windows Task Scheduler trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Use one of: minute steps/range-steps, fixed-minute hour steps/range-steps, hourly, daily, or weekly on a single weekday.");
426
+ // `M H D * *` / `M H D m *` MONTHLY at H:M on day(s)-of-month D, in the
427
+ // given month(s) m (or every month when `m` is `*`). Task Scheduler's
428
+ // `ScheduleByMonth` trigger takes both as native sets, same as `ScheduleByWeek`
429
+ // above.
430
+ if (f.minute.kind === "value" &&
431
+ f.hour.kind === "value" &&
432
+ (f.dom.kind === "value" || f.dom.kind === "list") &&
433
+ (f.month.kind === "star" || f.month.kind === "value" || f.month.kind === "list") &&
434
+ f.dow.kind === "star") {
435
+ const daysOfMonth = f.dom.kind === "value" ? [f.dom.value] : f.dom.values;
436
+ const months = f.month.kind === "star"
437
+ ? Array.from({ length: 12 }, (_, i) => i + 1)
438
+ : f.month.kind === "value"
439
+ ? [f.month.value]
440
+ : f.month.values;
441
+ return { kind: "monthly", atHour: f.hour.value, atMinute: f.minute.value, daysOfMonth, months };
442
+ }
443
+ throw new UsageError(`Schedule "${spec.raw}" cannot be expressed as a Windows Task Scheduler trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Use one of: minute steps/range-steps, fixed-minute hour steps/range-steps, hourly, daily, weekly on one or more weekdays, or monthly on one or more days-of-month.");
303
444
  }
304
445
  function minuteValuesTrigger(minutes, spec) {
305
446
  if (minutes.length > MAX_SCHTASKS_TRIGGERS) {
@@ -377,7 +377,6 @@ export function parseTaskV3Document(value, options) {
377
377
  if (own(input, "with"))
378
378
  sourceError(ctx, ["with"], "is legal only with uses.");
379
379
  const run = stringField(input.run, ctx, ["run"], { nonempty: true });
380
- noGithubExpression(run, ctx, ["run"]);
381
380
  let shell;
382
381
  if (own(input, "shell")) {
383
382
  const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
@@ -259,7 +259,6 @@ function parseTarget(input, ctx) {
259
259
  if (own(input, "with"))
260
260
  sourceError(ctx, ["with"], "is legal only with uses: akm/command; declare typed inputs: instead.");
261
261
  const run = stringField(input.run, ctx, ["run"], { nonempty: true });
262
- noGithubExpression(run, ctx, ["run"]);
263
262
  let shell;
264
263
  if (own(input, "shell")) {
265
264
  const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
@@ -250,9 +250,8 @@ async function driveChildRun(input, childRow) {
250
250
  // (dispatchJournaledAttempt awaits this call with no try of its own),
251
251
  // so an uncaught throw here escaped all the way into the scheduler and
252
252
  // was silently swallowed (R1, above). Reachable causes include the
253
- // child's own LeaseHeartbeat.assertAlive() firing mid-drive, a Lane B
254
- // UsageError out of the child's own completeWorkflowStep (e.g.
255
- // WORKFLOW_OUTPUT_INVALID), requireExecutableWorkflowPlan rejecting a
253
+ // child's own LeaseHeartbeat.assertAlive() firing mid-drive,
254
+ // requireExecutableWorkflowPlan rejecting a
256
255
  // tampered child plan_json, and the child's status changing between
257
256
  // this function's own step 5 read and the drive's internal
258
257
  // getNextWorkflowStep re-read — none of which match
@@ -220,10 +220,9 @@ function truncationNote(read) {
220
220
  * The captured stdout, with an unmistakable truncation block appended when the
221
221
  * retention cap discarded part of it.
222
222
  *
223
- * Same idiom, same reason as `WORKFLOW_EVIDENCE_TRUNCATED_MARKER`
224
- * (`runtime/runs.ts`): truncated data must never be mistakable for complete
225
- * data. The block names both byte counts, so a reader can see exactly how much
226
- * is missing rather than inferring it from a suspiciously round length.
223
+ * Truncated data must never be mistakable for complete data. The block names
224
+ * both byte counts, so a reader can see exactly how much is missing rather
225
+ * than inferring it from a suspiciously round length.
227
226
  */
228
227
  function markTruncatedStdout(result) {
229
228
  const read = result.stdoutRead;
@@ -36,6 +36,10 @@ export async function runWorkflowSteps(options) {
36
36
  let target = options.target;
37
37
  let params = options.params;
38
38
  let parameterFlags = options.parameterFlags;
39
+ // `--new` only applies to the FIRST resolution of `target` (a ref); every
40
+ // retry re-targets the run id `startWorkflowRun` already created, so it is
41
+ // cleared alongside `params`/`parameterFlags` below (#919).
42
+ let newRun = options.newRun;
39
43
  let remainingRetries = options.maxRetries ?? 0;
40
44
  let remainingSteps = options.maxSteps;
41
45
  const executed = [];
@@ -51,6 +55,7 @@ export async function runWorkflowSteps(options) {
51
55
  target,
52
56
  ...(params !== undefined ? { params } : { params: undefined }),
53
57
  ...(parameterFlags !== undefined ? { parameterFlags } : { parameterFlags: undefined }),
58
+ newRun,
54
59
  ...(remainingSteps !== undefined ? { maxSteps: remainingSteps } : { maxSteps: undefined }),
55
60
  }, liveEvidence);
56
61
  executed.push(...result.executed);
@@ -72,12 +77,14 @@ export async function runWorkflowSteps(options) {
72
77
  target = result.run.id;
73
78
  params = undefined;
74
79
  parameterFlags = undefined;
80
+ newRun = undefined;
75
81
  remainingRetries -= 1;
76
82
  }
77
83
  }
78
84
  async function runWorkflowAttempt(options, liveEvidence) {
79
85
  const next = await getNextWorkflowStep(options.target, options.params, {
80
86
  parameterFlags: options.parameterFlags,
87
+ newRun: options.newRun,
81
88
  });
82
89
  // Version/canonical/hash validation precedes every executable mutation,
83
90
  // including lease acquisition. Historical rows remain inspectable/abandonable.
@@ -137,8 +144,14 @@ async function runWorkflowAttempt(options, liveEvidence) {
137
144
  const result = await withWorkflowRunsConnection(() => driveRun(options, next, leaseHolder, heartbeat, liveEvidence));
138
145
  // Creation-time notices reach the caller only here: the run row has no
139
146
  // warnings column, and a later invocation of the same run must stay silent
140
- // about a decision it did not make. `driveRun` never sets `warnings`.
141
- return next.startWarnings?.length ? { ...result, warnings: next.startWarnings } : result;
147
+ // about a decision it did not make. `driveRun` never sets `warnings` or
148
+ // `resumed` both are properties of THIS resolution of `target`, not of
149
+ // the run row (#919).
150
+ return {
151
+ ...result,
152
+ ...(next.resumed ? { resumed: true } : {}),
153
+ ...(next.startWarnings?.length ? { warnings: next.startWarnings } : {}),
154
+ };
142
155
  }
143
156
  finally {
144
157
  heartbeat?.stop();
@@ -639,14 +652,10 @@ async function driveRun(options, initial, leaseHolder, heartbeat,
639
652
  /**
640
653
  * The COMPLETE in-memory evidence of every step THIS call has completed,
641
654
  * keyed by step id, preferred over the re-read row when the downstream scope
642
- * is rebuilt below. The spine rows are re-read between steps, and
643
- * `clipStepEvidenceForPersistence` (runtime/runs.ts) may have replaced an
644
- * over-cap artifact with a truncation envelope on the way in a bound on ONE
645
- * SQLite row, not on what a run may promote (the exec per-pipe cap alone
646
- * retains 8 MiB). Preferring the live value keeps the persistence bound
647
- * invisible to the run that produced it. A LATER `akm workflow run` starts
648
- * with an empty map and reads the rows, where a reference into a truncated
649
- * artifact fails loudly by name (`isTruncatedEvidence`).
655
+ * is rebuilt below avoiding a re-parse of a row this same invocation just
656
+ * wrote (step artifacts are persisted whole, so the two values agree; this
657
+ * is purely an avoided round trip, not a correctness dependency). A LATER
658
+ * `akm workflow run` starts with an empty map and reads the rows directly.
650
659
  *
651
660
  * Only steps some OTHER step's references NAME are stored (`referencedStepIds`
652
661
  * — the set-time filter): a step nothing downstream reads has no consumer to
@@ -23,7 +23,7 @@ import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-
23
23
  import { canonicalJson } from "../ir/plan-hash.js";
24
24
  import { parseReference, resolveReferenceString, } from "../program/expressions.js";
25
25
  import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
26
- import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
26
+ import { completeWorkflowStep } from "../runtime/runs.js";
27
27
  import { GATE_EVALUATION_PHASE } from "../runtime/unit-phases.js";
28
28
  import { parseJudgeVerdict } from "../validate-summary.js";
29
29
  import { gateNodeId } from "./frozen-judge.js";
@@ -65,30 +65,38 @@ function validateFanOutItems(stepId, items) {
65
65
  return (`Step "${stepId}" fan-out list contains a null item (index ${nullIndex}). ` +
66
66
  `Every item must be a concrete value — fix the producing step's output.`);
67
67
  }
68
- const firstIndexByCanonical = new Map();
69
- for (let i = 0; i < items.length; i++) {
70
- const canonical = canonicalJson(items[i]) ?? "null";
71
- const firstIndex = firstIndexByCanonical.get(canonical);
72
- if (firstIndex !== undefined) {
73
- return (`Step "${stepId}" fan-out list contains duplicate items (indices ${firstIndex} and ${i}: ` +
74
- `${clip(canonical, 200)}). Content-derived unit identity requires distinct items — ` +
75
- `deduplicate the list this workflow fans out over.`);
76
- }
77
- firstIndexByCanonical.set(canonical, i);
78
- }
79
68
  return undefined;
80
69
  }
81
70
  /**
82
71
  * Resolve one whole-value reference, refusing a value a persisted TRUNCATION
83
72
  * ENVELOPE stands in for (`clipStepEvidenceForPersistence`, runtime/runs.ts).
84
73
  *
85
- * The engine threads each step's complete in-memory evidence to the rest of its
86
- * own invocation, so only a RESUMED run can meet an envelope here. Left to the
87
- * raw resolver, a path reference into one reports a generic missing property
88
- * and a whole-value reference at one succeeds handing the envelope to a unit
89
- * as if it were the artifact. Both are silent corruption; name the cause
90
- * instead. Every whole-value position (`inputs[]`, `map.over`, `route.input`)
91
- * goes through here.
74
+ * The first occurrence of a given canonical value keeps the byte-identical id
75
+ * {@link unitIdFor} always produced for it so a plan with no duplicates (the
76
+ * overwhelming common case) is completely unaffected, and no prior journal
77
+ * entry is ever invalidated by this change. Only the SECOND and later
78
+ * occurrences gain a `#<n>` suffix (`#2`, `#3`, …), computed purely from each
79
+ * item's position in `items` — deterministic across a fresh run and a
80
+ * resumed one, since both call this from the same place in
81
+ * {@link computeStepWorkList} over the same resolved list.
82
+ */
83
+ function occurrenceSuffixedUnitIds(nodeId, items) {
84
+ const occurrenceByCanonical = new Map();
85
+ return items.map((item) => {
86
+ const base = unitIdFor(nodeId, item, true, true);
87
+ const canonical = canonicalJson(item) ?? "null";
88
+ const occurrence = (occurrenceByCanonical.get(canonical) ?? 0) + 1;
89
+ occurrenceByCanonical.set(canonical, occurrence);
90
+ return occurrence === 1 ? base : `${base}#${occurrence}`;
91
+ });
92
+ }
93
+ /**
94
+ * Resolve one whole-value reference (`inputs[]`, `map.over`, `route.input`).
95
+ *
96
+ * Every step artifact is now persisted whole (issue C), so this is a thin
97
+ * wrapper: source adapters may retain GitHub's whole-value `${{ ... }}`
98
+ * spelling, and this work-list seam unwraps only an exact whole-value
99
+ * wrapper — it never interpolates prose.
92
100
  */
93
101
  function resolveStepReference(reference, scope) {
94
102
  // Source adapters may retain GitHub's whole-value `${{ ... }}` spelling.
@@ -96,40 +104,7 @@ function resolveStepReference(reference, scope) {
96
104
  // unwraps only an exact whole-value wrapper and never interpolates prose.
97
105
  const exactWrapper = /^\$\{\{\s*([^{}]+?)\s*\}\}$/.exec(reference);
98
106
  const canonicalReference = exactWrapper?.[1] ?? reference;
99
- const resolved = resolveReferenceString(canonicalReference, scope);
100
- const truncated = truncatedReferenceTarget(canonicalReference, scope, resolved);
101
- if (!truncated)
102
- return resolved;
103
- return {
104
- ok: false,
105
- error: {
106
- reference,
107
- message: `${reference} reads a step artifact that was NOT persisted (${truncated.originalBytes} bytes exceeded the ` +
108
- `${truncated.limitBytes}-byte evidence_json cap, so the row stores a truncation marker). This run was ` +
109
- `resumed from rows that no longer hold the value — it cannot be recovered. Start a new run, or have the ` +
110
- `producing step emit a reference (path, id) instead of inline bulk data.`,
111
- },
112
- };
113
- }
114
- /** The envelope a reference lands on or walks through, if any. */
115
- function truncatedReferenceTarget(reference, scope, resolved) {
116
- if (resolved.ok)
117
- return isTruncatedEvidence(resolved.value) ? resolved.value : undefined;
118
- // A FAILED resolution is re-walked: the envelope is an object with none of
119
- // the original's keys, so the raw failure is whatever property went missing
120
- // along the way, several segments past the truncation.
121
- const parsed = parseReference(reference);
122
- if (!parsed.ok || parsed.expr.kind !== "stepOutput")
123
- return undefined;
124
- let current = scope.stepOutputs[parsed.expr.stepId];
125
- for (const segment of parsed.expr.path) {
126
- if (isTruncatedEvidence(current))
127
- return current;
128
- if (typeof current !== "object" || current === null)
129
- return undefined;
130
- current = current[segment];
131
- }
132
- return isTruncatedEvidence(current) ? current : undefined;
107
+ return resolveReferenceString(canonicalReference, scope);
133
108
  }
134
109
  /**
135
110
  * Pre-attempt resolution of a task-composing step's frozen `inputBindings`
@@ -248,9 +223,13 @@ export function computeStepWorkList(plan, input) {
248
223
  const fanOutProblem = isFanOut ? validateFanOutItems(plan.stepId, items) : undefined;
249
224
  if (fanOutProblem)
250
225
  return { ok: false, error: fanOutProblem };
251
- // Content-derived unit identity: compute every id up front (duplicate items
252
- // were rejected above identity requires distinct items).
253
- const unitIds = items.map((item) => unitIdFor(template.id, item, isFanOut, true));
226
+ // Content-derived unit identity: compute every id up front. A fan-out's
227
+ // canonical duplicates are disambiguated by occurrence ordinal rather than
228
+ // rejected (issue 6); a solo (non-fan-out) step has exactly one item, so
229
+ // there is nothing to disambiguate.
230
+ const unitIds = isFanOut
231
+ ? occurrenceSuffixedUnitIds(template.id, items)
232
+ : [unitIdFor(template.id, undefined, false, true)];
254
233
  const gateLoop = input.gateLoop ?? 1;
255
234
  const target = template.frozenTarget;
256
235
  const frozenExec = target.kind === "shell" || target.kind === "script" ? target.exec : undefined;
@@ -62,17 +62,25 @@ function rejectNonTaskBindingWith(source, ref, kind) {
62
62
  const family = kind === "command" ? "commands" : "scripts";
63
63
  throw new UsageError(`Workflow step ${source.id} cannot pass with: to ${family} target ${ref}; a ${kind} ref is not a binding surface.`, "COMPOSITION_INVALID");
64
64
  }
65
+ const NO_ENGINE_AVAILABLE_MESSAGE = "the fixed opencode-sdk fallback is unavailable";
66
+ function isNoEngineAvailable(err) {
67
+ return err instanceof ConfigError && err.message.includes(NO_ENGINE_AVAILABLE_MESSAGE);
68
+ }
65
69
  export function resolveJudge(source, context) {
66
- const engine = context.config.workflow?.judgeEngine;
67
- if (!engine) {
68
- throw new ConfigError("This workflow declares completion criteria but no verification engine is configured. Set workflow.judgeEngine to a named LLM or agent engine.", "INVALID_CONFIG_FILE");
69
- }
70
+ const configuredEngine = context.config.workflow?.judgeEngine;
70
71
  const content = source.gate?.rubric?.trim() ?? "Judge workflow completion.";
71
- const prepared = prepareInlineExecution({
72
- content,
73
- config: context.config,
74
- invocationKind: "workflow",
75
- current: { engine },
76
- });
77
- return commandResult(source, { onError: "fail", source: sourceStepRef(source) }, prepared, context);
72
+ try {
73
+ const prepared = prepareInlineExecution({
74
+ content,
75
+ config: context.config,
76
+ invocationKind: "workflow",
77
+ ...(configuredEngine ? { current: { engine: configuredEngine } } : {}),
78
+ });
79
+ return commandResult(source, { onError: "fail", source: sourceStepRef(source) }, prepared, context);
80
+ }
81
+ catch (err) {
82
+ if (configuredEngine || !isNoEngineAvailable(err))
83
+ throw err;
84
+ return undefined;
85
+ }
78
86
  }
@@ -2,6 +2,7 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { COMPOSITION_INVALID_MULTI_JOB_HINT, UsageError } from "../../core/errors.js";
5
+ import { warn } from "../../core/warn.js";
5
6
  import { compileWorkflowSource } from "../source-ir/compile.js";
6
7
  import { resolveJudge, resolveStep } from "./resolve-steps.js";
7
8
  /**
@@ -42,6 +43,12 @@ export async function resolveWorkflowSourceV4(asset, workflowSource, config, col
42
43
  }
43
44
  if (sourceStep.gate?.rubric?.trim()) {
44
45
  const judge = resolveJudge(sourceStep, context);
46
+ if (judge === undefined) {
47
+ warn(`Workflow step "${sourceStep.id}" has completion criteria but no verification engine is available ` +
48
+ "(set workflow.judgeEngine, defaults.engine, or install an opencode-sdk binary). The step will block " +
49
+ "for `akm workflow resume` once it is reached.");
50
+ continue;
51
+ }
45
52
  if (judge.target.kind !== "command")
46
53
  throw new Error(`workflow judge ${sourceStep.id} did not resolve to a command target`);
47
54
  judges.set(sourceStep.id, judge.target);