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
@@ -41,13 +41,17 @@ import { randomUUID } from "node:crypto";
41
41
  import { UsageError } from "../../core/errors.js";
42
42
  import { appendEvent } from "../../core/events.js";
43
43
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
44
+ import { acquireMaintenanceActivity } from "../../core/maintenance-barrier.js";
45
+ import { collectSensitiveValues, isEnvPassthroughValueSafeToExpose, redactSensitiveText, redactSensitiveValue, } from "../../core/redaction.js";
44
46
  import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
45
47
  import { assertRunParamsSatisfyPlan } from "../ir/params.js";
46
48
  import { PROGRAM_RETRY_REASONS } from "../program/schema.js";
49
+ import { requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
47
50
  import { completeWorkflowStep, getNextWorkflowStep, snapshotRunForDriver, } from "../runtime/runs.js";
48
51
  import { UNIT_STALE_MS } from "../runtime/unit-checkin.js";
49
52
  import { buildLease, resolveRunId } from "./brief.js";
50
- import { activeGateLoop, cascadeSkippedRouter, computeStepWorkList, finalizeExecutedStep, isRetryEligibleFailure, parseFrozenPlan, recoverGateFeedback, reduceEmptyStep, reduceStepOutcomes, seedJournaledRouteDecisions, selectUnitAttemptRow, stepOutputsFromEvidence, unitOutcomeFromRow, unitStillNeedsReport, } from "./step-work.js";
53
+ import { frozenSummaryJudge } from "./frozen-judge.js";
54
+ import { activeGateLoop, cascadeSkippedRouter, computeStepWorkList, finalizeExecutedStep, isRetryEligibleFailure, recoverGateFeedback, reduceEmptyStep, reduceStepOutcomes, seedJournaledRouteDecisions, selectUnitAttemptRow, stepOutputsFromEvidence, unitOutcomeFromRow, unitStillNeedsReport, } from "./step-work.js";
51
55
  /**
52
56
  * Claim time-to-live. Equal to the unit-checkin stale window
53
57
  * ({@link UNIT_STALE_MS}) so a claim expires at exactly the moment
@@ -66,14 +70,21 @@ const CLAIM_TTL_MS = UNIT_STALE_MS;
66
70
  const FINALIZE_LOCK_TTL_MS = 90_000;
67
71
  // ── Entry point ──────────────────────────────────────────────────────────────
68
72
  export async function reportWorkflowUnit(input) {
73
+ const release = await acquireMaintenanceActivity("workflow-report");
74
+ try {
75
+ return await reportWorkflowUnitWithBarrier(input);
76
+ }
77
+ finally {
78
+ release();
79
+ }
80
+ }
81
+ async function reportWorkflowUnitWithBarrier(input) {
69
82
  const nowFn = input.now ?? (() => new Date());
70
83
  const nowIso = nowFn().toISOString();
71
84
  const runId = await resolveRunId(input.target);
72
85
  // #14: read the spine, run row, and unit journal in ONE snapshot so the guards
73
86
  // below see a consistent point-in-time state (same fix as `brief`).
74
87
  const { next, run: runRow, units } = await snapshotRunForDriver(runId);
75
- const planJson = runRow.plan_json;
76
- const planHash = runRow.plan_hash;
77
88
  const leaseHolder = runRow.engine_lease_holder;
78
89
  const leaseUntil = runRow.engine_lease_until;
79
90
  // Refuse a non-active run: there is no work-list to report against.
@@ -100,7 +111,7 @@ export async function reportWorkflowUnit(input) {
100
111
  `moved since you briefed it — a concurrent report/run/manual completion advanced the run. Re-run ` +
101
112
  `\`akm workflow brief ${runId}\` and report against the current step.`, "INVALID_FLAG_VALUE");
102
113
  }
103
- const plan = loadFrozenPlan(runId, planJson, planHash);
114
+ const plan = requireExecutableWorkflowPlan(runRow);
104
115
  // Reviewer #12: the journaled params row must still satisfy the frozen param
105
116
  // schemas before report resolves any unit prompt from it — a violation is
106
117
  // post-start corruption, refused loudly (mirrors the frozen-plan hash check
@@ -140,14 +151,21 @@ export async function reportWorkflowUnit(input) {
140
151
  throw new UsageError(`Unit "${input.unitId}" cannot be resolved (${workUnit.resolved.error}) — it has no dispatchable input to ` +
141
152
  `report a result against. This is an authoring/data error in the workflow; fix it and start a new run.`);
142
153
  }
154
+ if (!workUnit.engine || !workUnit.invocation) {
155
+ throw new UsageError(`Unit "${workUnit.unitId}" has no complete frozen engine attribution.`);
156
+ }
157
+ const engineName = workUnit.engine.name;
158
+ const exactModel = workUnit.invocation.model;
143
159
  const inputHash = workUnit.resolved.inputHash;
144
160
  const journalId = workUnit.journalBaseId;
161
+ const sensitiveValues = await collectReportedUnitSensitiveValues(workUnit);
162
+ const sessionId = input.sessionId === undefined ? undefined : redactSensitiveText(input.sessionId, sensitiveValues);
145
163
  // ── running: claim / heartbeat, never advances the spine ───────────────────
146
164
  if (input.status === "running") {
147
165
  // The claim holder: the driver's --session-id, else a token we mint and
148
166
  // return so the driver can reuse it (as --session-id) to heartbeat and
149
167
  // finish the SAME claim. First unexpired claim wins.
150
- const holder = input.sessionId ?? `claim:${randomUUID()}`;
168
+ const holder = sessionId ?? `claim:${randomUUID()}`;
151
169
  const claimExpiresAt = new Date(nowFn().getTime() + CLAIM_TTL_MS).toISOString();
152
170
  const claimed = await withWorkflowRunsRepo((repo) => repo.transaction(() => {
153
171
  const existing = repo.getUnit(runId, journalId);
@@ -175,7 +193,8 @@ export async function reportWorkflowUnit(input) {
175
193
  parentUnitId: workUnit.isFanOut ? `${stepState.id}.map` : null,
176
194
  phase: null,
177
195
  runner: workUnit.runner,
178
- model: workUnit.model ?? null,
196
+ engine: engineName,
197
+ model: exactModel,
179
198
  inputHash,
180
199
  startedAt: nowIso,
181
200
  claimHolder: holder,
@@ -208,7 +227,7 @@ export async function reportWorkflowUnit(input) {
208
227
  };
209
228
  }
210
229
  // ── completed / failed: validate, guard, write, maybe finalize ─────────────
211
- const { resultJson, failureReason } = prepareResult(input, workUnit);
230
+ const { resultJson, failureReason } = prepareResult(input, workUnit, sensitiveValues);
212
231
  const thisTokens = input.tokens ?? 0;
213
232
  // Guarded write: the idempotent re-report / replay-divergence check and the
214
233
  // budget ceiling are evaluated INSIDE the same SQLite transaction as the
@@ -216,7 +235,7 @@ export async function reportWorkflowUnit(input) {
216
235
  // one budgeted step) serialize on the write lock — each sees the other's row
217
236
  // and the row is always internally consistent.
218
237
  const status = input.status;
219
- const holder = input.sessionId ?? null;
238
+ const holder = sessionId ?? null;
220
239
  const writeResult = await withWorkflowRunsRepo((repo) => repo.transaction(() => {
221
240
  const existing = repo.getUnit(runId, journalId);
222
241
  if (existing?.status === "completed") {
@@ -279,7 +298,8 @@ export async function reportWorkflowUnit(input) {
279
298
  parentUnitId: workUnit.isFanOut ? `${stepState.id}.map` : null,
280
299
  phase: null,
281
300
  runner: workUnit.runner,
282
- model: workUnit.model ?? null,
301
+ engine: engineName,
302
+ model: exactModel,
283
303
  inputHash,
284
304
  startedAt: existing?.started_at ?? nowIso,
285
305
  });
@@ -306,7 +326,7 @@ export async function reportWorkflowUnit(input) {
306
326
  resultJson,
307
327
  tokens: carriedTokens,
308
328
  failureReason,
309
- sessionId: input.sessionId ?? null,
329
+ sessionId: sessionId ?? null,
310
330
  finishedAt: nowIso,
311
331
  });
312
332
  // A `tokens-cross` verdict: this unit's OWN tokens push the run total over
@@ -381,7 +401,7 @@ export async function reportWorkflowUnit(input) {
381
401
  byUnit,
382
402
  gateLoop,
383
403
  priorEvidence,
384
- summaryJudge: input.summaryJudge,
404
+ summaryJudge: input.summaryJudge === undefined ? frozenSummaryJudge(plan, stepPlan.gate.judge) : input.summaryJudge,
385
405
  now: nowFn,
386
406
  written: { unitId: journalId, status },
387
407
  recorded: "written",
@@ -444,7 +464,7 @@ export async function reportWorkflowUnit(input) {
444
464
  byUnit,
445
465
  gateLoop,
446
466
  priorEvidence,
447
- summaryJudge: input.summaryJudge,
467
+ summaryJudge: input.summaryJudge === undefined ? frozenSummaryJudge(plan, stepPlan.gate.judge) : input.summaryJudge,
448
468
  now: nowFn,
449
469
  written: { unitId: journalId, status },
450
470
  recorded: idempotent ? "idempotent" : "written",
@@ -471,6 +491,15 @@ export async function reportWorkflowUnit(input) {
471
491
  * `finalizeStep` uses) so two concurrent settles/reports cannot double-journal.
472
492
  */
473
493
  export async function settleWorkflowSpine(input) {
494
+ const release = await acquireMaintenanceActivity("workflow-report-settle");
495
+ try {
496
+ return await settleWorkflowSpineWithBarrier(input);
497
+ }
498
+ finally {
499
+ release();
500
+ }
501
+ }
502
+ async function settleWorkflowSpineWithBarrier(input) {
474
503
  const nowFn = input.now ?? (() => new Date());
475
504
  const runId = await resolveRunId(input.target);
476
505
  const { next, run: runRow, units } = await snapshotRunForDriver(runId);
@@ -491,7 +520,7 @@ export async function settleWorkflowSpine(input) {
491
520
  throw new UsageError(`Workflow run ${runId} is now on step "${next.step.id}", not "${input.expectStep}" (--expect-step). The spine ` +
492
521
  `moved since you briefed it — re-run \`akm workflow brief ${runId}\` and settle against the current step.`, "INVALID_FLAG_VALUE");
493
522
  }
494
- const plan = loadFrozenPlan(runId, runRow.plan_json, runRow.plan_hash);
523
+ const plan = requireExecutableWorkflowPlan(runRow);
495
524
  assertRunParamsSatisfyPlan(runId, plan, next.run.params ?? {});
496
525
  // A step with resolvable units is settled ONLY when its work-list is FULLY
497
526
  // TERMINAL — every resolvable unit run to a terminal state, nothing left to
@@ -611,6 +640,7 @@ function buildStepContext(runId, plan, next, units) {
611
640
  runId,
612
641
  params: next.run.params ?? {},
613
642
  stepOutputs,
643
+ engines: plan.execution.engines,
614
644
  gateLoop,
615
645
  ...(gateFeedback ? { gateFeedback } : {}),
616
646
  });
@@ -778,7 +808,7 @@ async function finalizeStep(args) {
778
808
  workflowRef: next.run.workflowRef,
779
809
  stepPlan,
780
810
  stepId: stepState.id,
781
- completionCriteria: stepState.completionCriteria ?? [],
811
+ completionCriteria: stepPlan.gate.criteria,
782
812
  gateLoop,
783
813
  reduced,
784
814
  priorEvidence: args.priorEvidence,
@@ -860,6 +890,7 @@ async function settleSpine(args) {
860
890
  const sp = plan.steps.find((s) => s.stepId === step.id);
861
891
  if (!sp)
862
892
  break;
893
+ const stepJudge = summaryJudge === undefined ? frozenSummaryJudge(plan, sp.gate.judge) : summaryJudge;
863
894
  // A route-skipped target: complete it as skipped, cascading if it is itself
864
895
  // a router (identical to the engine loop's skip handling).
865
896
  const skipInfo = routeUnselected.get(step.id);
@@ -883,7 +914,7 @@ async function settleSpine(args) {
883
914
  workflowRef: state.run.workflowRef,
884
915
  stepId: step.id,
885
916
  stepPlan: sp,
886
- completionCriteria: step.completionCriteria ?? [],
917
+ completionCriteria: sp.gate.criteria,
887
918
  gateLoop: 1,
888
919
  loopsRemaining: false,
889
920
  result: {
@@ -896,7 +927,7 @@ async function settleSpine(args) {
896
927
  params: state.run.params ?? {},
897
928
  routeSelected,
898
929
  routeUnselected,
899
- summaryJudge,
930
+ summaryJudge: stepJudge,
900
931
  ...leaseArg,
901
932
  });
902
933
  if (fin.kind !== "advanced")
@@ -919,6 +950,7 @@ async function settleSpine(args) {
919
950
  runId,
920
951
  params: state.run.params ?? {},
921
952
  stepOutputs,
953
+ engines: plan.execution.engines,
922
954
  gateLoop,
923
955
  ...(gateFeedback ? { gateFeedback } : {}),
924
956
  });
@@ -946,14 +978,14 @@ async function settleSpine(args) {
946
978
  workflowRef: state.run.workflowRef,
947
979
  stepPlan: sp,
948
980
  stepId: step.id,
949
- completionCriteria: step.completionCriteria ?? [],
981
+ completionCriteria: sp.gate.criteria,
950
982
  gateLoop,
951
983
  reduced,
952
984
  priorEvidence,
953
985
  params: state.run.params ?? {},
954
986
  routeSelected,
955
987
  routeUnselected,
956
- summaryJudge,
988
+ summaryJudge: stepJudge,
957
989
  ...leaseArg,
958
990
  });
959
991
  if (completion.kind === "advanced") {
@@ -977,13 +1009,6 @@ async function settleSpine(args) {
977
1009
  return getNextWorkflowStep(runId);
978
1010
  }
979
1011
  // ── Helpers ──────────────────────────────────────────────────────────────────
980
- function loadFrozenPlan(runId, planJson, planHash) {
981
- if (!planJson) {
982
- throw new UsageError(`Workflow run ${runId} predates frozen plans (no plan_json on the run row) and cannot be driven by ` +
983
- `\`akm workflow report\`. Use engine-driven mode: \`akm workflow run ${runId}\`.`);
984
- }
985
- return parseFrozenPlan(runId, planJson, planHash);
986
- }
987
1012
  /**
988
1013
  * Normalize a driver-supplied `--failure-reason` to the persisted taxonomy (PR
989
1014
  * #714 review round 2, #16). An external driver can type ANY string; storing it
@@ -1001,10 +1026,12 @@ function loadFrozenPlan(runId, planJson, planHash) {
1001
1026
  * unknown external reason is recorded for observability without ever
1002
1027
  * triggering retry.
1003
1028
  */
1004
- export function normalizeFailureReason(raw) {
1029
+ export function normalizeFailureReason(raw, sensitiveValues = []) {
1005
1030
  const trimmed = raw?.trim();
1006
1031
  if (!trimmed)
1007
1032
  return "reported_failure";
1033
+ if (redactSensitiveText(trimmed, sensitiveValues) !== trimmed)
1034
+ return "reported_failure";
1008
1035
  if (PROGRAM_RETRY_REASONS.includes(trimmed))
1009
1036
  return trimmed;
1010
1037
  const slug = trimmed
@@ -1015,11 +1042,13 @@ export function normalizeFailureReason(raw) {
1015
1042
  return `external:${slug || "unknown"}`;
1016
1043
  }
1017
1044
  /** Validate + shape the reported result into what `finishUnit` persists. */
1018
- function prepareResult(input, workUnit) {
1045
+ function prepareResult(input, workUnit, sensitiveValues) {
1019
1046
  if (input.status === "failed") {
1020
1047
  return {
1021
- resultJson: input.resultRaw !== undefined && input.resultRaw !== "" ? JSON.stringify(input.resultRaw) : null,
1022
- failureReason: normalizeFailureReason(input.failureReason),
1048
+ resultJson: input.resultRaw !== undefined && input.resultRaw !== ""
1049
+ ? JSON.stringify(redactSensitiveText(input.resultRaw, sensitiveValues))
1050
+ : null,
1051
+ failureReason: normalizeFailureReason(input.failureReason, sensitiveValues),
1023
1052
  };
1024
1053
  }
1025
1054
  // completed
@@ -1034,13 +1063,13 @@ function prepareResult(input, workUnit) {
1034
1063
  parsed = JSON.parse(raw);
1035
1064
  }
1036
1065
  catch (err) {
1037
- throw new UsageError(`Unit "${input.unitId}" result is not valid JSON (its output schema requires a JSON value): ${err instanceof Error ? err.message : String(err)}`, "INVALID_FLAG_VALUE");
1066
+ throw new UsageError(`Unit "${input.unitId}" result is not valid JSON (its output schema requires a JSON value): ${redactSensitiveText(err instanceof Error ? err.message : String(err), sensitiveValues)}`, "INVALID_FLAG_VALUE");
1038
1067
  }
1039
1068
  const errors = validateJsonSchemaSubset(parsed, workUnit.schema);
1040
1069
  if (errors.length > 0) {
1041
- throw new UsageError(`Unit "${input.unitId}" result failed validation against its declared output schema: ${errors.join("; ")}.`, "INVALID_FLAG_VALUE");
1070
+ throw new UsageError(`Unit "${input.unitId}" result failed validation against its declared output schema: ${redactSensitiveText(errors.join("; "), sensitiveValues)}.`, "INVALID_FLAG_VALUE");
1042
1071
  }
1043
- return { resultJson: JSON.stringify(parsed), failureReason: null };
1072
+ return { resultJson: JSON.stringify(redactSensitiveValue(parsed, sensitiveValues)), failureReason: null };
1044
1073
  }
1045
1074
  // Free-text unit: journal the text as a JSON string EXACTLY as the executor
1046
1075
  // does — `native-executor.ts` finishUnit uses `outcome.text ? JSON.stringify… :
@@ -1048,7 +1077,38 @@ function prepareResult(input, workUnit) {
1048
1077
  // Matching that keeps the promoted artifact and the dispatch row byte-identical
1049
1078
  // across the engine and report surfaces (the cardinal graph-parity rule), and
1050
1079
  // stays consistent with the FAILED branch above which also maps ""→null.
1051
- return { resultJson: input.resultRaw ? JSON.stringify(input.resultRaw) : null, failureReason: null };
1080
+ return {
1081
+ resultJson: input.resultRaw ? JSON.stringify(redactSensitiveText(input.resultRaw, sensitiveValues)) : null,
1082
+ failureReason: null,
1083
+ };
1084
+ }
1085
+ async function collectReportedUnitSensitiveValues(workUnit) {
1086
+ const values = new Set();
1087
+ for (const ref of workUnit.env ?? []) {
1088
+ const { resolveEnvBinding } = await import("../../commands/env/env-binding.js");
1089
+ for (const value of Object.values(resolveEnvBinding(ref).values))
1090
+ values.add(value);
1091
+ }
1092
+ const collectEngine = (engine) => {
1093
+ if (!engine)
1094
+ return;
1095
+ if (engine.kind === "llm") {
1096
+ for (const name of engine.credential?.names ?? []) {
1097
+ const value = process.env[name]?.trim();
1098
+ if (value)
1099
+ values.add(value);
1100
+ }
1101
+ return;
1102
+ }
1103
+ for (const name of engine.envPassthrough) {
1104
+ const value = process.env[name];
1105
+ if (!isEnvPassthroughValueSafeToExpose(name, value) && value)
1106
+ values.add(value);
1107
+ }
1108
+ };
1109
+ collectEngine(workUnit.engine);
1110
+ collectEngine(workUnit.fallbackEngine);
1111
+ return collectSensitiveValues(values);
1052
1112
  }
1053
1113
  /**
1054
1114
  * Assess the frozen plan's declared budget ceilings for ONE report, seeded from
@@ -62,20 +62,33 @@
62
62
  */
63
63
  import { randomUUID } from "node:crypto";
64
64
  import { UsageError } from "../../core/errors.js";
65
- import { warn } from "../../core/warn.js";
65
+ import { withMaintenanceStartBarrierAsync } from "../../core/maintenance-barrier.js";
66
66
  import { disposeDispatchResources } from "../../integrations/agent/runner-dispatch.js";
67
67
  import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
68
68
  import { assertRunParamsSatisfyPlan } from "../ir/params.js";
69
+ import { computePlanHash } from "../ir/plan-hash.js";
70
+ import { decodeWorkflowPlanV3 } from "../ir/schema.js";
71
+ import { requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
69
72
  import { completeWorkflowStep, getNextWorkflowStep } from "../runtime/runs.js";
70
- import { compileWorkflowAssetPlan, loadWorkflowAsset } from "../runtime/workflow-asset-loader.js";
73
+ import { frozenSummaryJudge } from "./frozen-judge.js";
71
74
  import { executeStepPlan } from "./native-executor.js";
72
75
  // Shared step semantics — route evaluation + cascaded-skip bookkeeping,
73
76
  // gate-evaluation journaling, and the whole step-completion path
74
77
  // (`finalizeExecutedStep`) live in step-work.ts so the engine loop and the R3
75
78
  // brief/report driver protocol share ONE implementation (no drift).
76
- import { activeGateLoop, cascadeSkippedRouter, finalizeExecutedStep, GATE_EVALUATION_PHASE, parseFrozenPlan, recoverGateFeedback, seedJournaledRouteDecisions, } from "./step-work.js";
79
+ import { activeGateLoop, cascadeSkippedRouter, finalizeExecutedStep, GATE_EVALUATION_PHASE, recoverGateFeedback, seedJournaledRouteDecisions, } from "./step-work.js";
77
80
  export async function runWorkflowSteps(options) {
78
81
  const next = await getNextWorkflowStep(options.target, options.params);
82
+ // Version/canonical/hash validation precedes every executable mutation,
83
+ // including lease acquisition. Historical rows remain inspectable/abandonable.
84
+ if (!next.done && !options.loadPlan) {
85
+ await withWorkflowRunsRepo((repo) => {
86
+ const row = repo.getRunById(next.run.id);
87
+ if (!row)
88
+ throw new UsageError(`Workflow run ${next.run.id} was not found.`);
89
+ requireExecutableWorkflowPlan(row);
90
+ });
91
+ }
79
92
  // Refuse non-active runs BEFORE any dispatch — completeWorkflowStep would
80
93
  // reject the completion anyway, but only after the units already ran (and
81
94
  // cost money). Mirror its preflight up front.
@@ -128,7 +141,7 @@ export async function runWorkflowSteps(options) {
128
141
  // hanging on the leaked handle. Runs even if lease release itself fails;
129
142
  // a teardown-time repository error must not skip dispatch cleanup.
130
143
  try {
131
- (options.disposeDispatchResources ?? disposeDispatchResources)();
144
+ await (options.disposeDispatchResources ?? disposeDispatchResources)();
132
145
  }
133
146
  catch {
134
147
  /* disposal is best-effort; never let cleanup mask the run outcome */
@@ -148,14 +161,14 @@ function leaseExpiry() {
148
161
  * arbiter — two racing invocations cannot both win.
149
162
  */
150
163
  async function acquireRunLease(runId, holder) {
151
- await withWorkflowRunsRepo((repo) => {
164
+ await withMaintenanceStartBarrierAsync(() => withWorkflowRunsRepo((repo) => {
152
165
  if (repo.acquireEngineLease(runId, holder, leaseExpiry(), new Date().toISOString()))
153
166
  return;
154
167
  const row = repo.getRunById(runId);
155
168
  throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row?.engine_lease_holder ?? "(unknown)"} ` +
156
169
  `(run lease expires ${row?.engine_lease_until ?? "(unknown)"}). A second \`akm workflow run\` would race it — ` +
157
170
  `wait for that invocation to finish or for the lease to expire.`);
158
- });
171
+ }));
159
172
  }
160
173
  /**
161
174
  * Renew the lease between steps. Losing the lease mid-run (it expired during
@@ -333,11 +346,14 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
333
346
  const journaledDispatches = journaledUnits.filter((row) => row.phase !== GATE_EVALUATION_PHASE);
334
347
  let unitsDispatched = journaledDispatches.reduce((sum, row) => sum + row.attempts, 0);
335
348
  let tokensUsed = journaledDispatches.reduce((sum, row) => sum + (row.tokens ?? 0), 0);
336
- // One plan per invocation: the test seam receives the workflow ref; the
337
- // default reads the run's frozen plan and never touches the asset file.
338
- const plan = options.loadPlan
339
- ? await options.loadPlan(next.run.workflowRef)
340
- : await loadFrozenPlan(next.run.id, next.run.workflowRef);
349
+ // The decoded/hash-verified row plan is the sole execution authority. The
350
+ // loader seam may assert an expected plan in tests, but can never replace it.
351
+ const plan = await loadFrozenPlan(next.run.id, next.run.workflowRef);
352
+ if (options.loadPlan) {
353
+ const expected = decodeWorkflowPlanV3(await options.loadPlan(next.run.workflowRef));
354
+ if (computePlanHash(expected) !== computePlanHash(plan))
355
+ throw new UsageError(`Injected workflow plan for run ${next.run.id} differs from its frozen plan.`);
356
+ }
341
357
  // Reviewer #12: the journaled params row must still satisfy the frozen param
342
358
  // schemas before the engine resolves any unit prompt from it. Applied on ALL
343
359
  // THREE driver surfaces (engine here, brief, report) so schema-violating
@@ -475,13 +491,14 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
475
491
  // Budget ceilings ride the FROZEN plan (addendum R2): a mid-run
476
492
  // asset edit can never loosen or tighten a run's budget.
477
493
  ...(plan.budget ? { budget: plan.budget } : {}),
494
+ ...(plan.execution ? { engines: plan.execution.engines } : {}),
478
495
  gateLoop,
479
496
  ...(gateFeedback ? { gateFeedback } : {}),
480
497
  // The heartbeat's signal is the effective dispatch signal: a lost
481
498
  // lease (or a caller abort) aborts in-flight units promptly.
482
499
  ...(dispatchSignal ? { signal: dispatchSignal } : {}),
483
500
  ...(options.dispatcher ? { dispatcher: options.dispatcher } : {}),
484
- ...(options.maxConcurrency !== undefined ? { maxConcurrency: options.maxConcurrency } : {}),
501
+ maxConcurrency: Math.min(options.maxConcurrency ?? Number.POSITIVE_INFINITY, plan.execution?.maxConcurrency ?? 1),
485
502
  });
486
503
  // If the heartbeat lost the lease WHILE this step dispatched, another
487
504
  // engine now owns the run — stop loudly BEFORE finalizing the step
@@ -510,7 +527,7 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
510
527
  workflowRef: next.run.workflowRef,
511
528
  stepId: step.id,
512
529
  stepPlan,
513
- completionCriteria: step.completionCriteria ?? [],
530
+ completionCriteria: stepPlan.gate.criteria,
514
531
  gateLoop,
515
532
  loopsRemaining: gateLoop < maxLoops,
516
533
  result,
@@ -518,7 +535,7 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
518
535
  params: next.run.params ?? {},
519
536
  routeSelected,
520
537
  routeUnselected,
521
- summaryJudge: options.summaryJudge,
538
+ summaryJudge: options.summaryJudge === undefined ? frozenSummaryJudge(plan, stepPlan.gate.judge) : options.summaryJudge,
522
539
  ...(options.requireGates ? { requireGates: true } : {}),
523
540
  leaseHolder,
524
541
  });
@@ -578,19 +595,15 @@ async function driveRun(options, initial, leaseHolder, heartbeat) {
578
595
  * canonical JSON). A mismatch means the journaled plan was tampered with
579
596
  * or corrupted — fail loudly, never silently recompile. The workflow
580
597
  * asset file is NEVER touched on this path.
581
- * - `plan_json` NULL the run predates frozen plans (created before
582
- * migration 006). Warn and fall back to compiling from the live asset,
583
- * preserving pre-006 behavior for in-flight legacy runs.
598
+ * Historical v2/null rows are inspection-only in the engine cutover. They are
599
+ * never recompiled from a mutable source asset.
584
600
  */
585
- async function loadFrozenPlan(runId, workflowRef) {
601
+ async function loadFrozenPlan(runId, _workflowRef) {
586
602
  const row = await withWorkflowRunsRepo((repo) => {
587
603
  const run = repo.getRunById(runId);
588
- return run ? { planJson: run.plan_json, planHash: run.plan_hash } : undefined;
604
+ return run;
589
605
  });
590
- if (row?.planJson) {
591
- return parseFrozenPlan(runId, row.planJson, row.planHash);
592
- }
593
- warn(`Workflow run ${runId} predates frozen plans (no plan_json on the run row); ` +
594
- `compiling the plan from the live asset ${workflowRef}. New runs freeze their plan at start.`);
595
- return compileWorkflowAssetPlan(await loadWorkflowAsset(workflowRef));
606
+ if (!row)
607
+ throw new UsageError(`Workflow run ${runId} was not found.`);
608
+ return requireExecutableWorkflowPlan(row);
596
609
  }
@@ -6,13 +6,12 @@
6
6
  *
7
7
  * A thin policy layer over `core/concurrent.ts` (`concurrentMap`) — the
8
8
  * existing semaphore-bounded pool is generalized, not forked. The scheduler
9
- * owns the engine-wide limits the plan requires:
9
+ * owns the limits the plan requires:
10
10
  *
11
- * - Engine-wide concurrency cap, applied on top of whatever per-step
12
- * concurrency the workflow declares. The cap is the `workflow.maxConcurrency`
13
- * akm config setting when set (clamped to
14
- * `[1, WORKFLOW_MAX_CONCURRENCY_CEILING]`), else the CPU-derived default
15
- * `min(16, cores − 2)` (the original Claude-Code-matching formula).
11
+ * - The effective width is the minimum of the map request, the frozen
12
+ * workflow cap, the selected LLM engine's frozen cap (when applicable),
13
+ * and the current host's CPU-derived safety cap. Reapplying host safety at
14
+ * dispatch matters when a frozen run resumes on a smaller machine.
16
15
  * - Cooperative cancellation via AbortSignal (workers stop claiming items;
17
16
  * the same signal is passed into each dispatch so in-flight units can be
18
17
  * preempted too).
@@ -24,9 +23,10 @@
24
23
  * units impossible to resume (peer review R1). Only work that really
25
24
  * dispatches consumes the cap.
26
25
  */
27
- import os from "node:os";
28
26
  import { concurrentMap } from "../../core/concurrent.js";
29
- import { loadConfig } from "../../core/config/config.js";
27
+ import { cpuDerivedUnitConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js";
28
+ import { WORKFLOW_MAX_MAP_EXPANSION } from "../resource-limits.js";
29
+ export { clampMaxConcurrency, cpuDerivedUnitConcurrency, WORKFLOW_MAX_CONCURRENCY_CEILING, } from "../concurrency-policy.js";
30
30
  /**
31
31
  * Hard ceiling on an EXPLICIT `workflow.maxConcurrency`. A user value above
32
32
  * this is clamped down (not rejected) so a config shared across machines with
@@ -34,32 +34,6 @@ import { loadConfig } from "../../core/config/config.js";
34
34
  * 64 is deliberately far above any sane fan-out width — it exists only to keep
35
35
  * a fat-fingered `100000` from spawning a runaway pool.
36
36
  */
37
- export const WORKFLOW_MAX_CONCURRENCY_CEILING = 64;
38
- /**
39
- * CPU-derived engine cap used when `workflow.maxConcurrency` is unset — the
40
- * original `min(16, cores−2)` formula (matching Claude Code), floored at 1.
41
- */
42
- export function cpuDerivedUnitConcurrency(cpuCount = os.cpus()?.length ?? 4) {
43
- return Math.min(16, Math.max(1, cpuCount - 2));
44
- }
45
- /** Clamp an explicit configured value into `[1, WORKFLOW_MAX_CONCURRENCY_CEILING]`. */
46
- export function clampMaxConcurrency(value) {
47
- return Math.min(WORKFLOW_MAX_CONCURRENCY_CEILING, Math.max(1, Math.floor(value)));
48
- }
49
- /**
50
- * Read `workflow.maxConcurrency` from config, fail-open to `undefined` (use the
51
- * CPU default) on any load error or a non-numeric value. Kept side-effect-free
52
- * and defensive: the scheduler must never fail a run because config is unwell.
53
- */
54
- function configuredMaxConcurrency() {
55
- try {
56
- const value = loadConfig().workflow?.maxConcurrency;
57
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
58
- }
59
- catch {
60
- return undefined;
61
- }
62
- }
63
37
  /**
64
38
  * Engine-wide ceiling on concurrent units. Precedence:
65
39
  * 1. An explicit `workflow.maxConcurrency` config value, clamped to
@@ -72,13 +46,11 @@ function configuredMaxConcurrency() {
72
46
  * @param configured Explicit config value seam (defaults to reading config);
73
47
  * pass `undefined` explicitly to force the CPU path in a test.
74
48
  */
75
- export function maxUnitConcurrency(cpuCount = os.cpus()?.length ?? 4, configured = configuredMaxConcurrency()) {
76
- if (configured !== undefined)
77
- return clampMaxConcurrency(configured);
78
- return cpuDerivedUnitConcurrency(cpuCount);
49
+ export function maxUnitConcurrency(cpuCount, configured) {
50
+ return workflowMaxConcurrency(configured, cpuCount);
79
51
  }
80
52
  /** Lifetime unit cap per run — a runaway-loop backstop, far above real use. */
81
- export const LIFETIME_UNIT_CAP = 1000;
53
+ export const LIFETIME_UNIT_CAP = WORKFLOW_MAX_MAP_EXPANSION;
82
54
  export class UnitCapExceededError extends Error {
83
55
  constructor(cap) {
84
56
  super(`workflow run exceeded the lifetime unit cap (${cap}). Aborting dispatch — check for a runaway fan-out.`);
@@ -94,7 +66,6 @@ export class UnitCapExceededError extends Error {
94
66
  * resume stay free.
95
67
  */
96
68
  export async function scheduleUnits(items, dispatch, options = {}) {
97
- const cap = options.maxConcurrency ?? maxUnitConcurrency();
98
- const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, cap));
69
+ const concurrency = Math.max(1, Math.min(options.concurrency ?? 1, options.maxConcurrency ?? Number.POSITIVE_INFINITY, options.llmConcurrency ?? Number.POSITIVE_INFINITY, options.hostConcurrency ?? cpuDerivedUnitConcurrency()));
99
70
  return concurrentMap(items, dispatch, concurrency, { signal: options.signal });
100
71
  }