akm-cli 0.9.0-rc.1 → 0.9.0-rc.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 (228) hide show
  1. package/CHANGELOG.md +223 -9
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/stash-skeleton/README.md +28 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  23. package/dist/assets/tasks/core/backup.yml +1 -0
  24. package/dist/assets/tasks/core/extract.yml +1 -0
  25. package/dist/assets/tasks/core/improve.yml +1 -0
  26. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  27. package/dist/assets/tasks/core/sync.yml +1 -0
  28. package/dist/assets/tasks/core/version-check.yml +1 -0
  29. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  30. package/dist/assets/templates/html/health.html +5 -1
  31. package/dist/cli/config-migrate.js +31 -138
  32. package/dist/cli/config-validate.js +10 -8
  33. package/dist/cli.js +48 -14
  34. package/dist/commands/agent/agent-dispatch.js +17 -16
  35. package/dist/commands/agent/agent-support.js +0 -24
  36. package/dist/commands/agent/contribute-cli.js +5 -15
  37. package/dist/commands/backup-cli.js +54 -0
  38. package/dist/commands/config-cli.js +45 -159
  39. package/dist/commands/env/secret.js +8 -5
  40. package/dist/commands/health/checks.js +130 -83
  41. package/dist/commands/health/html-report.js +4 -0
  42. package/dist/commands/health/improve-metrics.js +30 -32
  43. package/dist/commands/health/llm-usage.js +19 -19
  44. package/dist/commands/health/md-report.js +4 -0
  45. package/dist/commands/health/metrics.js +2 -1
  46. package/dist/commands/health/surfaces.js +5 -4
  47. package/dist/commands/health.js +1 -1
  48. package/dist/commands/improve/consolidate/chunking.js +2 -2
  49. package/dist/commands/improve/consolidate.js +28 -25
  50. package/dist/commands/improve/distill/promote-memory.js +5 -12
  51. package/dist/commands/improve/distill/quality-gate.js +5 -7
  52. package/dist/commands/improve/distill.js +16 -5
  53. package/dist/commands/improve/eligibility.js +22 -12
  54. package/dist/commands/improve/extract-cli.js +47 -19
  55. package/dist/commands/improve/extract.js +110 -62
  56. package/dist/commands/improve/improve-cli.js +38 -16
  57. package/dist/commands/improve/improve-result-file.js +30 -24
  58. package/dist/commands/improve/improve-strategies.js +137 -0
  59. package/dist/commands/improve/improve.js +60 -30
  60. package/dist/commands/improve/locks.js +66 -45
  61. package/dist/commands/improve/loop-stages.js +75 -33
  62. package/dist/commands/improve/memory/memory-belief.js +79 -7
  63. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  64. package/dist/commands/improve/preparation.js +71 -73
  65. package/dist/commands/improve/procedural.js +3 -2
  66. package/dist/commands/improve/recombine.js +2 -1
  67. package/dist/commands/improve/reflect.js +119 -214
  68. package/dist/commands/improve/shared.js +11 -5
  69. package/dist/commands/lint/base-linter.js +152 -42
  70. package/dist/commands/mv-cli.js +809 -0
  71. package/dist/commands/proposal/proposal-cli.js +18 -8
  72. package/dist/commands/proposal/propose.js +64 -69
  73. package/dist/commands/read/knowledge.js +436 -4
  74. package/dist/commands/read/remember-cli.js +39 -2
  75. package/dist/commands/read/search-cli.js +6 -1
  76. package/dist/commands/registry-cli.js +29 -14
  77. package/dist/commands/remember.js +2 -0
  78. package/dist/commands/sources/init.js +13 -14
  79. package/dist/commands/sources/schema-repair.js +2 -4
  80. package/dist/commands/sources/source-add.js +62 -73
  81. package/dist/commands/sources/source-manage.js +50 -46
  82. package/dist/commands/sources/stash-cli.js +41 -4
  83. package/dist/commands/tasks/default-tasks.js +12 -12
  84. package/dist/commands/tasks/tasks-cli.js +7 -3
  85. package/dist/commands/tasks/tasks.js +113 -18
  86. package/dist/commands/wiki-cli.js +9 -10
  87. package/dist/core/asset/frontmatter.js +12 -2
  88. package/dist/core/common.js +5 -3
  89. package/dist/core/config/config-io.js +28 -17
  90. package/dist/core/config/config-schema.js +358 -66
  91. package/dist/core/config/config-types.js +3 -3
  92. package/dist/core/config/config-version.js +29 -0
  93. package/dist/core/config/config-walker.js +98 -27
  94. package/dist/core/config/config.js +132 -266
  95. package/dist/core/config/deep-merge.js +41 -0
  96. package/dist/core/config/engine-semantics.js +32 -0
  97. package/dist/core/errors.js +2 -2
  98. package/dist/core/extra-params.js +61 -0
  99. package/dist/core/file-lock.js +201 -56
  100. package/dist/core/improve-result.js +178 -0
  101. package/dist/core/maintenance-barrier.js +119 -0
  102. package/dist/core/migration-backup.js +416 -0
  103. package/dist/core/paths.js +3 -0
  104. package/dist/core/redaction.js +358 -0
  105. package/dist/core/state/migrations.js +17 -2
  106. package/dist/core/state-db.js +44 -1
  107. package/dist/indexer/db/db.js +116 -1
  108. package/dist/indexer/graph/graph-extraction.js +28 -16
  109. package/dist/indexer/index-writer-lock.js +31 -24
  110. package/dist/indexer/index-written-assets.js +15 -6
  111. package/dist/indexer/indexer.js +47 -2
  112. package/dist/indexer/passes/memory-inference.js +10 -6
  113. package/dist/indexer/passes/metadata.js +250 -0
  114. package/dist/indexer/search/db-search.js +111 -44
  115. package/dist/indexer/search/fts-query.js +41 -0
  116. package/dist/indexer/search/ranking-contributors.js +48 -0
  117. package/dist/indexer/search/ranking.js +36 -23
  118. package/dist/indexer/search/search-fields.js +11 -1
  119. package/dist/integrations/agent/builder-shared.js +7 -0
  120. package/dist/integrations/agent/builders.js +5 -52
  121. package/dist/integrations/agent/config.js +3 -143
  122. package/dist/integrations/agent/detect.js +17 -2
  123. package/dist/integrations/agent/engine-resolution.js +202 -0
  124. package/dist/integrations/agent/index.js +1 -2
  125. package/dist/integrations/agent/model-aliases.js +7 -2
  126. package/dist/integrations/agent/profiles.js +6 -99
  127. package/dist/integrations/agent/runner-dispatch.js +76 -13
  128. package/dist/integrations/agent/runner.js +76 -207
  129. package/dist/integrations/agent/spawn.js +4 -6
  130. package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
  131. package/dist/integrations/harnesses/aider/index.js +0 -1
  132. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
  133. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  134. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  135. package/dist/integrations/harnesses/claude/index.js +0 -2
  136. package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
  137. package/dist/integrations/harnesses/codex/index.js +0 -1
  138. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  139. package/dist/integrations/harnesses/copilot/index.js +0 -1
  140. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
  141. package/dist/integrations/harnesses/gemini/index.js +0 -1
  142. package/dist/integrations/harnesses/index.js +1 -24
  143. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  144. package/dist/integrations/harnesses/opencode/index.js +0 -6
  145. package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
  146. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  147. package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
  148. package/dist/integrations/harnesses/openhands/index.js +0 -1
  149. package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
  150. package/dist/integrations/harnesses/pi/index.js +0 -1
  151. package/dist/integrations/harnesses/types.js +1 -32
  152. package/dist/integrations/lockfile.js +32 -21
  153. package/dist/llm/client.js +48 -14
  154. package/dist/llm/feature-gate.js +15 -47
  155. package/dist/llm/graph-extract.js +1 -1
  156. package/dist/llm/index-passes.js +8 -42
  157. package/dist/llm/memory-infer-impl.js +1 -1
  158. package/dist/llm/usage-persist.js +4 -0
  159. package/dist/llm/usage-telemetry.js +35 -5
  160. package/dist/output/shapes/helpers.js +2 -1
  161. package/dist/output/shapes/passthrough.js +2 -0
  162. package/dist/output/text/helpers.js +3 -1
  163. package/dist/schemas/akm-config.json +11013 -8600
  164. package/dist/schemas/akm-task.json +87 -0
  165. package/dist/schemas/akm-workflow.json +57 -13
  166. package/dist/scripts/migrate-storage.js +8591 -509
  167. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
  168. package/dist/setup/detected-engines.js +142 -0
  169. package/dist/setup/engine-config.js +89 -0
  170. package/dist/setup/setup.js +236 -132
  171. package/dist/setup/steps/connection.js +61 -32
  172. package/dist/setup/steps/platforms.js +4 -4
  173. package/dist/setup/steps.js +3 -2
  174. package/dist/storage/database.js +13 -1
  175. package/dist/storage/engines/sqlite-migrations.js +1 -0
  176. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  177. package/dist/storage/repositories/task-history-repository.js +78 -0
  178. package/dist/storage/repositories/workflow-runs-repository.js +9 -8
  179. package/dist/tasks/parser.js +138 -52
  180. package/dist/tasks/runner.js +71 -75
  181. package/dist/tasks/schema.js +1 -1
  182. package/dist/tasks/validator.js +11 -6
  183. package/dist/wiki/wiki.js +9 -8
  184. package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
  185. package/dist/workflows/concurrency-policy.js +15 -0
  186. package/dist/workflows/db.js +65 -13
  187. package/dist/workflows/exec/brief.js +18 -24
  188. package/dist/workflows/exec/frozen-judge.js +47 -0
  189. package/dist/workflows/exec/native-executor.js +124 -65
  190. package/dist/workflows/exec/report.js +93 -33
  191. package/dist/workflows/exec/run-workflow.js +38 -25
  192. package/dist/workflows/exec/scheduler.js +12 -41
  193. package/dist/workflows/exec/step-work.js +91 -35
  194. package/dist/workflows/ir/compile.js +13 -26
  195. package/dist/workflows/ir/freeze.js +243 -0
  196. package/dist/workflows/ir/plan-hash.js +40 -5
  197. package/dist/workflows/ir/schema.js +542 -1
  198. package/dist/workflows/parser.js +7 -0
  199. package/dist/workflows/program/parser.js +132 -23
  200. package/dist/workflows/program/project.js +3 -4
  201. package/dist/workflows/program/schema.js +2 -2
  202. package/dist/workflows/resource-limits.js +20 -0
  203. package/dist/workflows/runtime/plan-classifier.js +187 -0
  204. package/dist/workflows/runtime/runs.js +54 -47
  205. package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
  206. package/dist/workflows/validator.js +25 -0
  207. package/docs/data-and-telemetry.md +2 -2
  208. package/docs/migration/release-notes/0.6.0.md +1 -1
  209. package/docs/migration/release-notes/0.7.0.md +5 -4
  210. package/docs/migration/v0.8-to-v0.9.md +401 -0
  211. package/package.json +4 -2
  212. package/schemas/akm-config.json +16638 -0
  213. package/schemas/akm-task.json +87 -0
  214. package/schemas/akm-workflow.json +372 -0
  215. package/dist/commands/improve/improve-profiles.js +0 -168
  216. package/dist/core/config/config-migration.js +0 -602
  217. package/dist/core/deep-merge.js +0 -38
  218. package/dist/llm/call-ai.js +0 -62
  219. package/dist/setup/legacy-config.js +0 -106
  220. package/docs/README.md +0 -104
  221. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  222. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  223. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  224. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  225. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  226. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  227. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  228. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -24,15 +24,17 @@
24
24
  */
25
25
  import { createRequire } from "node:module";
26
26
  import { isMap, isScalar, LineCounter, parseDocument } from "yaml";
27
- import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_RUNNER_KINDS, PROGRAM_STEP_ID_PATTERN, } from "./schema.js";
27
+ import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
28
+ import { jsonBytes, utf8Bytes, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_INSTRUCTION_BYTES, WORKFLOW_MAX_MAP_EXPANSION, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_ROUTE_BRANCHES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_SOURCE_BYTES, WORKFLOW_MAX_STEPS, } from "../resource-limits.js";
29
+ import { PROGRAM_ISOLATION_KINDS, PROGRAM_ON_ERROR, PROGRAM_PARAM_NAME_PATTERN, PROGRAM_REDUCERS, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN, WORKFLOW_PROGRAM_VERSION, } from "./schema.js";
28
30
  const TOP_LEVEL_KEYS = ["version", "name", "description", "params", "defaults", "budget", "steps"];
29
- const DEFAULTS_KEYS = ["runner", "model", "timeout", "on_error"];
31
+ const DEFAULTS_KEYS = ["engine", "model", "timeout", "on_error", "llm"];
30
32
  const BUDGET_KEYS = ["max_tokens", "max_units"];
31
33
  const STEP_KEYS = ["id", "title", "unit", "map", "route", "output", "gate"];
32
34
  const UNIT_KEYS = [
33
- "runner",
34
- "profile",
35
+ "engine",
35
36
  "model",
37
+ "llm",
36
38
  "timeout",
37
39
  "retry",
38
40
  "on_error",
@@ -51,13 +53,19 @@ const TIMEOUT_HINT = `Use "<n>ms", "<n>s", "<n>m" (e.g. "10m"), or "none"`;
51
53
  /**
52
54
  * Cheap structural probe for the indexer matcher (mirrors `looksLikeWorkflow`
53
55
  * in ../parser.ts). Returns true if the text has the unmistakable top-level
54
- * shape of a YAML workflow program: `version: 1` and a `steps:` key, both at
56
+ * shape of a YAML workflow program: `version: 2` and a `steps:` key, both at
55
57
  * column 0. Used so the matcher and parser cannot drift.
56
58
  */
57
59
  export function looksLikeWorkflowProgram(yamlText) {
58
- return /^version[ \t]*:[ \t]*['"]?1['"]?[ \t]*(#.*)?$/m.test(yamlText) && /^steps[ \t]*:/m.test(yamlText);
60
+ return /^version[ \t]*:[ \t]*['"]?(?:1|2)['"]?[ \t]*(#.*)?$/m.test(yamlText) && /^steps[ \t]*:/m.test(yamlText);
59
61
  }
60
62
  export function parseWorkflowProgram(yamlText, source) {
63
+ if (utf8Bytes(yamlText) > WORKFLOW_MAX_SOURCE_BYTES) {
64
+ return {
65
+ ok: false,
66
+ errors: [{ line: 1, message: "Workflow source exceeds the 1 MiB resource limit." }],
67
+ };
68
+ }
61
69
  const errors = [];
62
70
  const lineCounter = new LineCounter();
63
71
  let doc;
@@ -116,14 +124,19 @@ export function parseWorkflowProgram(yamlText, source) {
116
124
  return {
117
125
  ok: false,
118
126
  errors: [
119
- { line: 1, message: `A workflow program must be a YAML mapping with "version: 1", "name", and "steps".` },
127
+ { line: 1, message: `A workflow program must be a YAML mapping with "version: 2", "name", and "steps".` },
120
128
  ],
121
129
  };
122
130
  }
123
131
  checkUnknownKeys(ctx, root, [], TOP_LEVEL_KEYS, "top-level");
124
- if (root.version !== 1) {
125
- const got = root.version === undefined ? "it is missing" : `got ${JSON.stringify(root.version)}`;
126
- ctx.err(["version"], `"version: 1" is required at the top level (${got}). Only the number 1 is a valid version.`);
132
+ if (root.version !== WORKFLOW_PROGRAM_VERSION) {
133
+ if (root.version === 1) {
134
+ ctx.err(["version"], `Workflow version 1 retired; version 2 is required. Replace runner/profile selectors with engine.`);
135
+ }
136
+ else {
137
+ const got = root.version === undefined ? "it is missing" : `got ${JSON.stringify(root.version)}`;
138
+ ctx.err(["version"], `"version: 2" is required at the top level (${got}). Only the number 2 is a valid version.`);
139
+ }
127
140
  }
128
141
  let name = "";
129
142
  if (typeof root.name === "string" && root.name.trim() !== "") {
@@ -150,7 +163,7 @@ export function parseWorkflowProgram(yamlText, source) {
150
163
  if (errors.length > 0)
151
164
  return { ok: false, errors };
152
165
  const program = {
153
- version: 1,
166
+ version: WORKFLOW_PROGRAM_VERSION,
154
167
  name,
155
168
  ...(description !== undefined ? { description } : {}),
156
169
  ...(params !== undefined ? { params } : {}),
@@ -171,6 +184,9 @@ function parseParams(ctx, raw) {
171
184
  ctx.err(["params"], `"params" must be a mapping of param name to a JSON Schema object (e.g. changed_files: { type: array }).`);
172
185
  return undefined;
173
186
  }
187
+ if (Object.keys(raw).length > WORKFLOW_MAX_PARAMS) {
188
+ ctx.err(["params"], `"params" must contain at most ${WORKFLOW_MAX_PARAMS} entries.`);
189
+ }
174
190
  const params = {};
175
191
  for (const [paramName, value] of Object.entries(raw)) {
176
192
  if (!PROGRAM_PARAM_NAME_PATTERN.test(paramName)) {
@@ -195,9 +211,12 @@ function parseDefaults(ctx, raw) {
195
211
  }
196
212
  checkUnknownKeys(ctx, raw, path, DEFAULTS_KEYS, `"defaults"`);
197
213
  const defaults = {};
198
- const runner = parseEnumField(ctx, raw.runner, [...path, "runner"], `"defaults.runner"`, PROGRAM_RUNNER_KINDS);
199
- if (runner !== undefined)
200
- defaults.runner = runner;
214
+ if (raw.engine !== undefined) {
215
+ if (typeof raw.engine === "string" && raw.engine.trim() !== "")
216
+ defaults.engine = raw.engine.trim();
217
+ else
218
+ ctx.err([...path, "engine"], `"defaults.engine" must be a non-empty engine name.`);
219
+ }
201
220
  if (raw.model !== undefined) {
202
221
  if (typeof raw.model === "string" && raw.model.trim() !== "")
203
222
  defaults.model = raw.model.trim();
@@ -210,6 +229,9 @@ function parseDefaults(ctx, raw) {
210
229
  const onError = parseEnumField(ctx, raw.on_error, [...path, "on_error"], `"defaults.on_error"`, PROGRAM_ON_ERROR);
211
230
  if (onError !== undefined)
212
231
  defaults.onError = onError;
232
+ const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `"defaults.llm"`);
233
+ if (llm !== undefined)
234
+ defaults.llm = llm;
213
235
  return Object.keys(defaults).length > 0 ? defaults : undefined;
214
236
  }
215
237
  function parseBudget(ctx, raw) {
@@ -231,11 +253,14 @@ function parseBudget(ctx, raw) {
231
253
  }
232
254
  }
233
255
  if (raw.max_units !== undefined) {
234
- if (typeof raw.max_units === "number" && Number.isInteger(raw.max_units) && raw.max_units >= 1) {
256
+ if (typeof raw.max_units === "number" &&
257
+ Number.isInteger(raw.max_units) &&
258
+ raw.max_units >= 1 &&
259
+ raw.max_units <= WORKFLOW_MAX_MAP_EXPANSION) {
235
260
  budget.maxUnits = raw.max_units;
236
261
  }
237
262
  else {
238
- ctx.err([...path, "max_units"], `"budget.max_units" must be an integer >= 1.`);
263
+ ctx.err([...path, "max_units"], `"budget.max_units" must be an integer from 1 through ${WORKFLOW_MAX_MAP_EXPANSION}.`);
239
264
  }
240
265
  }
241
266
  return Object.keys(budget).length > 0 ? budget : undefined;
@@ -245,6 +270,9 @@ function parseSteps(ctx, raw) {
245
270
  ctx.err(["steps"], `"steps" is required and must be a list with at least one step.`);
246
271
  return [];
247
272
  }
273
+ if (raw.length > WORKFLOW_MAX_STEPS) {
274
+ ctx.err(["steps"], `"steps" must contain at most ${WORKFLOW_MAX_STEPS} entries.`);
275
+ }
248
276
  // First pass: collect ids so route targets can be checked against ALL steps
249
277
  // (including ones that fail their own validation).
250
278
  const idIndex = new Map();
@@ -359,18 +387,18 @@ function parseUnit(ctx, raw, path, stepLabel) {
359
387
  if (typeof raw.instructions === "string" && raw.instructions.trim() !== "") {
360
388
  unit.instructions = raw.instructions;
361
389
  ctx.checkTemplates(raw.instructions, [...path, "instructions"], `${stepLabel} "instructions"`);
390
+ if (utf8Bytes(raw.instructions) > WORKFLOW_MAX_INSTRUCTION_BYTES) {
391
+ ctx.err([...path, "instructions"], `${stepLabel} "instructions" exceeds the 256 KiB resource limit.`);
392
+ }
362
393
  }
363
394
  else {
364
395
  ctx.err([...path, "instructions"], `${stepLabel} "unit" requires non-empty string "instructions".`);
365
396
  }
366
- const runner = parseEnumField(ctx, raw.runner, [...path, "runner"], `${stepLabel} "runner"`, PROGRAM_RUNNER_KINDS);
367
- if (runner !== undefined)
368
- unit.runner = runner;
369
- if (raw.profile !== undefined) {
370
- if (typeof raw.profile === "string" && raw.profile.trim() !== "")
371
- unit.profile = raw.profile.trim();
397
+ if (raw.engine !== undefined) {
398
+ if (typeof raw.engine === "string" && raw.engine.trim() !== "")
399
+ unit.engine = raw.engine.trim();
372
400
  else
373
- ctx.err([...path, "profile"], `${stepLabel} "profile" must be a non-empty string.`);
401
+ ctx.err([...path, "engine"], `${stepLabel} "engine" must be a non-empty engine name.`);
374
402
  }
375
403
  if (raw.model !== undefined) {
376
404
  if (typeof raw.model === "string" && raw.model.trim() !== "")
@@ -378,6 +406,9 @@ function parseUnit(ctx, raw, path, stepLabel) {
378
406
  else
379
407
  ctx.err([...path, "model"], `${stepLabel} "model" must be a non-empty string (a model alias or exact id).`);
380
408
  }
409
+ const llm = parseLlmOverrides(ctx, raw.llm, [...path, "llm"], `${stepLabel} "llm"`);
410
+ if (llm !== undefined)
411
+ unit.llm = llm;
381
412
  const timeoutMs = parseTimeoutField(ctx, raw.timeout, [...path, "timeout"], `${stepLabel} "timeout"`);
382
413
  if (timeoutMs !== undefined)
383
414
  unit.timeoutMs = timeoutMs;
@@ -461,6 +492,9 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
461
492
  ctx.err(whenPath, `${stepLabel} "route" requires "when": a mapping of match value to target step id (e.g. when: { pass: ship }).`);
462
493
  }
463
494
  else if (isMap(whenNode)) {
495
+ if (whenNode.items.length > WORKFLOW_MAX_ROUTE_BRANCHES) {
496
+ ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
497
+ }
464
498
  // Walk the AST pairs (not the JS object) so duplicate matches that only
465
499
  // collide after stringification ("true" vs true) are still caught.
466
500
  const seenMatches = new Map();
@@ -501,6 +535,9 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
501
535
  ctx.err(whenPath, `${stepLabel} "when: ${match}" must map to a step id string.`);
502
536
  }
503
537
  }
538
+ if (Object.keys(raw.when).length > WORKFLOW_MAX_ROUTE_BRANCHES) {
539
+ ctx.err(whenPath, `${stepLabel} "when" must contain at most ${WORKFLOW_MAX_ROUTE_BRANCHES} branches.`);
540
+ }
504
541
  if (Object.keys(raw.when).length === 0) {
505
542
  ctx.err(whenPath, `${stepLabel} "when" must contain at least one match → step-id entry.`);
506
543
  }
@@ -634,6 +671,75 @@ function parseEnumField(ctx, raw, path, label, allowed) {
634
671
  ctx.err(path, `${label} must be one of: ${allowed.join(" | ")} (got ${JSON.stringify(raw)}).`);
635
672
  return undefined;
636
673
  }
674
+ /** Parse only invocation tuning. Connection identity belongs to a named engine. */
675
+ function parseLlmOverrides(ctx, raw, path, label) {
676
+ if (raw === undefined)
677
+ return undefined;
678
+ if (!isPlainRecord(raw)) {
679
+ ctx.err(path, `${label} must be a mapping of LLM invocation overrides.`);
680
+ return undefined;
681
+ }
682
+ const keys = [
683
+ "temperature",
684
+ "max_tokens",
685
+ "supports_json_schema",
686
+ "extra_params",
687
+ "context_length",
688
+ "enable_thinking",
689
+ ];
690
+ checkUnknownKeys(ctx, raw, path, keys, label);
691
+ const result = {};
692
+ if (raw.temperature !== undefined) {
693
+ if (typeof raw.temperature === "number" && Number.isFinite(raw.temperature))
694
+ result.temperature = raw.temperature;
695
+ else
696
+ ctx.err([...path, "temperature"], `${label}.temperature must be a finite number.`);
697
+ }
698
+ if (raw.max_tokens !== undefined) {
699
+ if (typeof raw.max_tokens === "number" && Number.isInteger(raw.max_tokens) && raw.max_tokens > 0) {
700
+ result.maxTokens = raw.max_tokens;
701
+ }
702
+ else
703
+ ctx.err([...path, "max_tokens"], `${label}.max_tokens must be a positive integer.`);
704
+ }
705
+ if (raw.supports_json_schema !== undefined) {
706
+ if (typeof raw.supports_json_schema === "boolean")
707
+ result.supportsJsonSchema = raw.supports_json_schema;
708
+ else
709
+ ctx.err([...path, "supports_json_schema"], `${label}.supports_json_schema must be a boolean.`);
710
+ }
711
+ if (raw.extra_params !== undefined) {
712
+ if (!isPlainRecord(raw.extra_params)) {
713
+ ctx.err([...path, "extra_params"], `${label}.extra_params must be a JSON object.`);
714
+ }
715
+ else {
716
+ const issues = validateExtraParams(raw.extra_params);
717
+ for (const issue of issues) {
718
+ ctx.err([...path, "extra_params", ...issue.path], `${formatExtraParamsIssue(`${label}.extra_params`, issue)}.`);
719
+ }
720
+ if (jsonBytes(raw.extra_params) > WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
721
+ ctx.err([...path, "extra_params"], `${label}.extra_params exceeds the 64 KiB resource limit.`);
722
+ }
723
+ if (issues.length === 0 && jsonBytes(raw.extra_params) <= WORKFLOW_MAX_EXTRA_PARAMS_BYTES) {
724
+ result.extraParams = raw.extra_params;
725
+ }
726
+ }
727
+ }
728
+ if (raw.context_length !== undefined) {
729
+ if (typeof raw.context_length === "number" && Number.isInteger(raw.context_length) && raw.context_length > 0) {
730
+ result.contextLength = raw.context_length;
731
+ }
732
+ else
733
+ ctx.err([...path, "context_length"], `${label}.context_length must be a positive integer.`);
734
+ }
735
+ if (raw.enable_thinking !== undefined) {
736
+ if (typeof raw.enable_thinking === "boolean")
737
+ result.enableThinking = raw.enable_thinking;
738
+ else
739
+ ctx.err([...path, "enable_thinking"], `${label}.enable_thinking must be a boolean.`);
740
+ }
741
+ return Object.keys(result).length > 0 ? result : undefined;
742
+ }
637
743
  function parseSchemaObject(ctx, raw, path, label) {
638
744
  if (raw === undefined)
639
745
  return undefined;
@@ -641,6 +747,9 @@ function parseSchemaObject(ctx, raw, path, label) {
641
747
  ctx.err(path, `${label} must be a JSON Schema object (e.g. { type: object, properties: { … } }).`);
642
748
  return undefined;
643
749
  }
750
+ if (jsonBytes(raw) > WORKFLOW_MAX_SCHEMA_BYTES) {
751
+ ctx.err(path, `${label} exceeds the 256 KiB resource limit.`);
752
+ }
644
753
  return raw;
645
754
  }
646
755
  function checkUnknownKeys(ctx, obj, path, allowed, label) {
@@ -65,19 +65,18 @@ export function projectProgramParameters(program) {
65
65
  /**
66
66
  * Compact, show-facing orchestration summary for one program step, reusing
67
67
  * the existing `WorkflowStepOrchestrationSummary` shape. Field mapping:
68
- * `runner`/`model`/`timeoutMs` merge the run-level `defaults` exactly like
68
+ * `engine`/`model`/`timeoutMs` merge the run-level `defaults` exactly like
69
69
  * the compiler does (per-unit wins), `fanOut.over` carries the raw `${{ … }}`
70
70
  * expression, and `route` carries the explicit input + branch table.
71
71
  * Returns undefined when the step declares nothing worth summarizing.
72
72
  */
73
73
  export function summarizeProgramStepOrchestration(step, defaults) {
74
74
  const unit = step.unit ?? step.map?.unit;
75
- const runner = unit?.runner ?? defaults?.runner;
75
+ const engine = unit?.engine ?? defaults?.engine;
76
76
  const model = unit?.model ?? defaults?.model;
77
77
  const timeoutMs = unit?.timeoutMs !== undefined ? unit.timeoutMs : defaults?.timeoutMs;
78
78
  const summary = {
79
- ...(runner !== undefined ? { runner } : {}),
80
- ...(unit?.profile !== undefined ? { profile: unit.profile } : {}),
79
+ ...(engine !== undefined ? { engine } : {}),
81
80
  ...(model !== undefined ? { model } : {}),
82
81
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
83
82
  ...(step.map
@@ -1,8 +1,8 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
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
- export const WORKFLOW_PROGRAM_VERSION = 1;
5
- /** Execution backend for a unit. `inherit` defers to the run-level default. */
4
+ export const WORKFLOW_PROGRAM_VERSION = 2;
5
+ /** @deprecated source v2 rejects runner; retained for TypeScript migration only. */
6
6
  export const PROGRAM_RUNNER_KINDS = ["llm", "agent", "sdk", "inherit"];
7
7
  /** How a map step folds its per-item unit results into the step artifact. */
8
8
  export const PROGRAM_REDUCERS = ["collect", "vote"];
@@ -0,0 +1,20 @@
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
+ export const WORKFLOW_MAX_PLAN_BYTES = 2 * 1024 * 1024;
5
+ export const WORKFLOW_MAX_SOURCE_BYTES = 1024 * 1024;
6
+ export const WORKFLOW_MAX_STEPS = 256;
7
+ export const WORKFLOW_MAX_ENGINES = 64;
8
+ export const WORKFLOW_MAX_PARAMS = 128;
9
+ export const WORKFLOW_MAX_ROUTE_BRANCHES = 256;
10
+ export const WORKFLOW_MAX_INSTRUCTION_BYTES = 256 * 1024;
11
+ export const WORKFLOW_MAX_SCHEMA_BYTES = 256 * 1024;
12
+ export const WORKFLOW_MAX_EXTRA_PARAMS_BYTES = 64 * 1024;
13
+ export const WORKFLOW_MAX_JSON_DEPTH = 64;
14
+ export const WORKFLOW_MAX_MAP_EXPANSION = 10_000;
15
+ export function utf8Bytes(value) {
16
+ return Buffer.byteLength(value, "utf8");
17
+ }
18
+ export function jsonBytes(value) {
19
+ return utf8Bytes(JSON.stringify(value));
20
+ }
@@ -0,0 +1,187 @@
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 { UsageError } from "../../core/errors.js";
5
+ import { decodeCanonicalPlan } from "../ir/plan-hash.js";
6
+ import { WORKFLOW_IR_VERSION } from "../ir/schema.js";
7
+ /** One policy authority for executable versus inspection-only historical runs. */
8
+ export function classifyWorkflowRunPlan(row) {
9
+ const runId = row.id ?? "(unknown)";
10
+ if (!row.plan_json) {
11
+ if (row.plan_ir_version === WORKFLOW_IR_VERSION) {
12
+ return {
13
+ support: "corrupt-plan",
14
+ irVersion: row.plan_ir_version,
15
+ error: `Workflow run ${runId} declares workflow IR version ${row.plan_ir_version} but has no frozen plan.`,
16
+ };
17
+ }
18
+ return {
19
+ support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
20
+ irVersion: row.plan_ir_version ?? null,
21
+ error: row.plan_ir_version === null || row.plan_ir_version === undefined
22
+ ? `Workflow run ${runId} has no executable workflow IR plan.`
23
+ : `Workflow run ${runId} uses unsupported workflow IR version ${String(row.plan_ir_version)} and has no frozen plan.`,
24
+ };
25
+ }
26
+ let raw;
27
+ try {
28
+ raw = JSON.parse(row.plan_json);
29
+ }
30
+ catch {
31
+ if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
32
+ return {
33
+ support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
34
+ irVersion: row.plan_ir_version ?? null,
35
+ error: `Workflow run ${runId} has malformed historical frozen plan JSON that cannot be executed.`,
36
+ };
37
+ }
38
+ return {
39
+ support: "corrupt-plan",
40
+ irVersion: row.plan_ir_version ?? null,
41
+ error: `Workflow run ${runId} has corrupt frozen plan JSON.`,
42
+ };
43
+ }
44
+ const decodedVersion = typeof raw === "object" && raw !== null ? raw.irVersion : null;
45
+ if (!Number.isSafeInteger(decodedVersion) || decodedVersion < 1) {
46
+ if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
47
+ return {
48
+ support: row.plan_ir_version === null || row.plan_ir_version === undefined ? "missing-plan" : "unsupported-version",
49
+ irVersion: row.plan_ir_version ?? null,
50
+ error: `Workflow run ${runId} has historical frozen plan data with no supported IR version.`,
51
+ };
52
+ }
53
+ return {
54
+ support: "corrupt-plan",
55
+ irVersion: row.plan_ir_version ?? null,
56
+ error: `Workflow run ${runId} has a missing or invalid frozen plan IR version.`,
57
+ };
58
+ }
59
+ if (row.plan_ir_version !== null && row.plan_ir_version !== undefined && row.plan_ir_version !== decodedVersion) {
60
+ if (row.plan_ir_version !== WORKFLOW_IR_VERSION && decodedVersion !== WORKFLOW_IR_VERSION) {
61
+ return {
62
+ support: "unsupported-version",
63
+ irVersion: decodedVersion,
64
+ error: `Workflow run ${runId} uses unsupported workflow IR version ${String(decodedVersion)} with mismatched historical metadata.`,
65
+ };
66
+ }
67
+ return {
68
+ support: "corrupt-plan",
69
+ irVersion: decodedVersion,
70
+ error: `Workflow run ${runId} has mismatched stored plan IR version (${String(row.plan_ir_version)} != ${String(decodedVersion)}).`,
71
+ };
72
+ }
73
+ if (decodedVersion !== WORKFLOW_IR_VERSION) {
74
+ return {
75
+ support: "unsupported-version",
76
+ irVersion: decodedVersion,
77
+ error: `Workflow run ${runId} uses unsupported workflow IR version ${String(decodedVersion)}.`,
78
+ };
79
+ }
80
+ if (row.plan_ir_version !== WORKFLOW_IR_VERSION) {
81
+ return {
82
+ support: "corrupt-plan",
83
+ irVersion: row.plan_ir_version ?? null,
84
+ error: `Workflow run ${runId} has no stored plan IR version.`,
85
+ };
86
+ }
87
+ try {
88
+ return {
89
+ support: "supported",
90
+ irVersion: WORKFLOW_IR_VERSION,
91
+ plan: decodeCanonicalPlan(runId, row.plan_json, row.plan_hash),
92
+ };
93
+ }
94
+ catch (cause) {
95
+ return { support: "corrupt-plan", irVersion: 3, error: cause instanceof Error ? cause.message : String(cause) };
96
+ }
97
+ }
98
+ /** Reject an execution mutation while preserving inspection and abandon access. */
99
+ export function requireExecutableWorkflowPlan(row) {
100
+ const classified = classifyWorkflowRunPlan(row);
101
+ if (classified.support === "supported")
102
+ return classified.plan;
103
+ if (classified.support === "missing-plan" || classified.support === "unsupported-version") {
104
+ throw new UsageError(`${classified.error} This historical run is inspection-only; abandon it with \`akm workflow abandon ${row.id ?? "<run>"}\` and start a new run.`, "WORKFLOW_IR_VERSION_UNSUPPORTED");
105
+ }
106
+ throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
107
+ }
108
+ /** Abandon is the sole mutation allowed for a historical run; corrupt v3 data is never mutated. */
109
+ export function requireAbandonableWorkflowPlan(row) {
110
+ const classified = classifyWorkflowRunPlan(row);
111
+ if (classified.support === "corrupt-plan")
112
+ throw new UsageError(classified.error, "INVALID_JSON_ARGUMENT");
113
+ }
114
+ /** Project persisted spine rows from the decoded plan, never from the mutable source asset. */
115
+ export function frozenStepRows(plan) {
116
+ return plan.steps.map((step) => ({
117
+ stepId: step.stepId,
118
+ stepTitle: step.title,
119
+ instructions: step.root
120
+ ? step.root.kind === "map"
121
+ ? step.root.template.instructions
122
+ : step.root.instructions
123
+ : routeInstructions(step.route),
124
+ completionJson: step.gate.criteria.length > 0 ? JSON.stringify(step.gate.criteria) : null,
125
+ sequenceIndex: step.sequenceIndex,
126
+ }));
127
+ }
128
+ /** Verify the durable spine still agrees with the decoded/hash-verified plan before any mutation. */
129
+ export function assertWorkflowSpineMatchesPlan(plan, run, rows) {
130
+ const expected = frozenStepRows(plan);
131
+ if (rows.length !== expected.length)
132
+ corruptSpine(run.id, "step count differs from the frozen plan");
133
+ for (let index = 0; index < expected.length; index++) {
134
+ const actual = rows[index];
135
+ const planned = expected[index];
136
+ if (!actual ||
137
+ !planned ||
138
+ actual.step_id !== planned.stepId ||
139
+ actual.step_title !== planned.stepTitle ||
140
+ actual.instructions !== planned.instructions ||
141
+ actual.completion_json !== planned.completionJson ||
142
+ actual.sequence_index !== planned.sequenceIndex) {
143
+ const fields = [
144
+ actual.step_id !== planned.stepId ? "step_id" : "",
145
+ actual.step_title !== planned.stepTitle ? "step_title" : "",
146
+ actual.instructions !== planned.instructions ? "instructions" : "",
147
+ actual.completion_json !== planned.completionJson ? "completion_json" : "",
148
+ actual.sequence_index !== planned.sequenceIndex ? "sequence_index" : "",
149
+ ].filter(Boolean);
150
+ corruptSpine(run.id, `step row ${index} differs from the frozen plan (${fields.join(", ") || "missing row"})`);
151
+ }
152
+ }
153
+ if (run.current_step_id !== null && !expected.some((step) => step.stepId === run.current_step_id))
154
+ corruptSpine(run.id, `current step ${run.current_step_id} is not in the frozen plan`);
155
+ const current = run.current_step_id ? rows.find((row) => row.step_id === run.current_step_id) : undefined;
156
+ if (run.status === "active") {
157
+ const firstPending = rows.find((row) => row.status === "pending");
158
+ if (!current || current.status !== "pending" || firstPending?.step_id !== current.step_id)
159
+ corruptSpine(run.id, "active status/current step does not match the first pending plan step");
160
+ }
161
+ else if (run.status === "blocked") {
162
+ if (!current || current.status !== "blocked")
163
+ corruptSpine(run.id, `${run.status} status does not match the current plan step`);
164
+ }
165
+ else if (run.status === "failed") {
166
+ // `workflow abandon` marks the run failed while intentionally leaving its
167
+ // current step pending so `resume` can reopen the same work.
168
+ if (!current || (current.status !== "failed" && current.status !== "pending"))
169
+ corruptSpine(run.id, `${run.status} status does not match the current plan step`);
170
+ }
171
+ else if (run.status === "completed") {
172
+ if (run.current_step_id !== null ||
173
+ rows.some((row) => row.status === "pending" || row.status === "blocked" || row.status === "failed"))
174
+ corruptSpine(run.id, "completed status disagrees with the plan spine");
175
+ }
176
+ }
177
+ function routeInstructions(route) {
178
+ const branches = Object.entries(route.when)
179
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
180
+ .map(([match, stepId]) => `"${match}" -> ${stepId}`);
181
+ if (route.defaultStepId !== undefined)
182
+ branches.push(`default -> ${route.defaultStepId}`);
183
+ return `Route on ${route.input}: ${branches.join(", ")}.`;
184
+ }
185
+ function corruptSpine(runId, detail) {
186
+ throw new UsageError(`Workflow run ${runId} has a corrupt durable step spine: ${detail}. Refusing to mutate state that disagrees with its frozen plan.`, "INVALID_JSON_ARGUMENT");
187
+ }