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
@@ -3,6 +3,8 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
+ import { acquireMaintenanceActivitySync } from "../core/maintenance-barrier.js";
7
+ import { ensureMigrationBackup, getMigrationBackupDir } from "../core/migration-backup.js";
6
8
  import { getWorkflowDbPath } from "../core/paths.js";
7
9
  import { openDatabase } from "../storage/database.js";
8
10
  import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
@@ -42,18 +44,55 @@ import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
42
44
  */
43
45
  // ── Public API ───────────────────────────────────────────────────────────────
44
46
  export function openWorkflowDatabase(dbPath = getWorkflowDbPath()) {
45
- const dir = path.dirname(dbPath);
46
- if (!fs.existsSync(dir)) {
47
- fs.mkdirSync(dir, { recursive: true });
47
+ const isCanonical = path.resolve(dbPath) === path.resolve(getWorkflowDbPath());
48
+ const releaseActivity = isCanonical ? acquireMaintenanceActivitySync("workflow-db") : undefined;
49
+ let db;
50
+ try {
51
+ // Preserve an originally absent workflow.db in the recovery manifest. SQLite
52
+ // creates the file at open time, so the cutover bundle must exist first.
53
+ if (isCanonical && !fs.existsSync(getMigrationBackupDir()))
54
+ ensureMigrationBackup();
55
+ const dir = path.dirname(dbPath);
56
+ if (!fs.existsSync(dir)) {
57
+ fs.mkdirSync(dir, { recursive: true });
58
+ }
59
+ db = openDatabase(dbPath);
60
+ // #589: 30 s busy timeout, matching index.db / state.db. Without it the
61
+ // default is 0 ms, so any concurrent writer fails immediately with
62
+ // SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
63
+ applyStandardPragmas(db, { dataDir: dir });
64
+ ensureBaseSchema(db);
65
+ runMigrations(db, { ensureCutoverBackup: isCanonical });
66
+ if (!releaseActivity)
67
+ return db;
68
+ const openedDb = db;
69
+ let closed = false;
70
+ return {
71
+ prepare: openedDb.prepare.bind(openedDb),
72
+ exec: openedDb.exec.bind(openedDb),
73
+ run: openedDb.run.bind(openedDb),
74
+ transaction: openedDb.transaction.bind(openedDb),
75
+ get inTransaction() {
76
+ return openedDb.inTransaction;
77
+ },
78
+ close() {
79
+ if (closed)
80
+ return;
81
+ closed = true;
82
+ try {
83
+ openedDb.close();
84
+ }
85
+ finally {
86
+ releaseActivity();
87
+ }
88
+ },
89
+ };
90
+ }
91
+ catch (error) {
92
+ db?.close();
93
+ releaseActivity?.();
94
+ throw error;
48
95
  }
49
- const db = openDatabase(dbPath);
50
- // #589: 30 s busy timeout, matching index.db / state.db. Without it the
51
- // default is 0 ms, so any concurrent writer fails immediately with
52
- // SQLITE_BUSY. #628: journal_mode is configurable via AKM_SQLITE_JOURNAL_MODE.
53
- applyStandardPragmas(db, { dataDir: dir });
54
- ensureBaseSchema(db);
55
- runMigrations(db);
56
- return db;
57
96
  }
58
97
  export function closeWorkflowDatabase(db) {
59
98
  db.close();
@@ -300,6 +339,13 @@ const MIGRATIONS = [
300
339
  ALTER TABLE workflow_run_units ADD COLUMN claim_expires_at TEXT;
301
340
  `,
302
341
  },
342
+ {
343
+ id: "010-ir-v3-engine",
344
+ up: `
345
+ ALTER TABLE workflow_runs ADD COLUMN plan_ir_version INTEGER;
346
+ ALTER TABLE workflow_run_units ADD COLUMN engine TEXT;
347
+ `,
348
+ },
303
349
  ];
304
350
  /**
305
351
  * Stable id of the scope_key migration. Exported for bootstrap detection and
@@ -345,6 +391,12 @@ function bootstrapPreVersioningDb(db) {
345
391
  *
346
392
  * Called automatically by {@link openWorkflowDatabase}.
347
393
  */
348
- export function runMigrations(db) {
349
- runSqliteMigrations(db, MIGRATIONS, { bootstrap: bootstrapPreVersioningDb });
394
+ export function runMigrations(db, options) {
395
+ runSqliteMigrations(db, MIGRATIONS, {
396
+ bootstrap: bootstrapPreVersioningDb,
397
+ beforeMigration(migration) {
398
+ if (options?.ensureCutoverBackup && migration.id === "010-ir-v3-engine")
399
+ ensureMigrationBackup();
400
+ },
401
+ });
350
402
  }
@@ -35,10 +35,11 @@ import { NotFoundError, UsageError } from "../../core/errors.js";
35
35
  import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
36
36
  import { getCurrentWorkflowScopeKey } from "../authoring/scope-key.js";
37
37
  import { assertRunParamsSatisfyPlan } from "../ir/params.js";
38
+ import { frozenStepRows, requireExecutableWorkflowPlan } from "../runtime/plan-classifier.js";
38
39
  import { snapshotRunForDriver } from "../runtime/runs.js";
39
40
  import { evaluateStaleUnits } from "../runtime/unit-checkin.js";
40
41
  import { detectSecretShapedParams } from "./param-secrets.js";
41
- import { activeGateLoop, assertJournaledRouteSelectionsValid, computeStepWorkList, evaluateRoute, GATE_EVALUATION_PHASE, isWorkListFullyTerminal, parseFrozenPlan, recoverGateFeedback, selectUnitAttemptRow, stepOutputsFromEvidence, } from "./step-work.js";
42
+ import { activeGateLoop, assertJournaledRouteSelectionsValid, computeStepWorkList, evaluateRoute, GATE_EVALUATION_PHASE, isWorkListFullyTerminal, recoverGateFeedback, selectUnitAttemptRow, stepOutputsFromEvidence, } from "./step-work.js";
42
43
  const EMPTY_WORK_LIST = { isFanOut: false, reducer: null, itemCount: 0, units: [] };
43
44
  // ── Entry point ──────────────────────────────────────────────────────────────
44
45
  /**
@@ -54,8 +55,6 @@ export async function buildWorkflowBrief(target) {
54
55
  // the active step between the spine read and the unit-journal read. A bare run
55
56
  // id never auto-starts (we resolved to a concrete id above).
56
57
  const { next, run: runRow, units } = await snapshotRunForDriver(runId);
57
- const planJson = runRow.plan_json;
58
- const planHash = runRow.plan_hash;
59
58
  const leaseHolder = runRow.engine_lease_holder;
60
59
  const leaseUntil = runRow.engine_lease_until;
61
60
  const run = {
@@ -144,7 +143,7 @@ export async function buildWorkflowBrief(target) {
144
143
  // Load the FROZEN plan the engine executes (migration 006). A legacy run
145
144
  // (NULL plan_json) has no plan for brief to read — point at engine-driven
146
145
  // mode, which still handles pre-006 runs by compiling from the asset.
147
- const plan = loadFrozenPlanForBrief(run.id, planJson, planHash);
146
+ const plan = requireExecutableWorkflowPlan(runRow);
148
147
  // Reviewer #12: the journaled params row must still satisfy the frozen param
149
148
  // schemas — a violation is post-start corruption, loud on the brief surface
150
149
  // too (mirrors the frozen-plan hash check and the tampered-params divergence).
@@ -171,13 +170,16 @@ export async function buildWorkflowBrief(target) {
171
170
  const gateFeedback = recoverGateFeedback(units, stepState.id, gateLoop);
172
171
  const isRouteOnly = !!stepPlan.route && !stepPlan.root;
173
172
  const kind = isRouteOnly ? "route" : stepPlan.route ? "execute-and-route" : "execute";
174
- const criteria = stepState.completionCriteria ?? [];
173
+ const criteria = stepPlan.gate.criteria;
174
+ const instructions = frozenStepRows(plan).find((step) => step.stepId === stepState.id)?.instructions;
175
+ if (instructions === undefined)
176
+ throw new UsageError(`Step "${stepState.id}" has no frozen instructions.`);
175
177
  const step = {
176
178
  stepId: stepState.id,
177
- title: stepState.title,
178
- sequenceIndex: stepState.sequenceIndex ?? 0,
179
+ title: stepPlan.title,
180
+ sequenceIndex: stepPlan.sequenceIndex,
179
181
  kind,
180
- instructions: stepState.instructions,
182
+ instructions,
181
183
  gate: {
182
184
  criteria,
183
185
  maxLoops: Math.max(1, stepPlan.gate.maxLoops ?? 1),
@@ -209,6 +211,7 @@ export async function buildWorkflowBrief(target) {
209
211
  runId: run.id,
210
212
  params: run.params,
211
213
  stepOutputs,
214
+ engines: plan.execution.engines,
212
215
  gateLoop,
213
216
  ...(gateFeedback ? { gateFeedback } : {}),
214
217
  });
@@ -315,15 +318,19 @@ function makeSpineToken(runId, stepId, gateLoop, watermark) {
315
318
  }
316
319
  // ── Helpers ──────────────────────────────────────────────────────────────────
317
320
  function toBriefUnit(runId, unit, stepId, journaled, ctx) {
321
+ if (!unit.engine || !unit.invocation) {
322
+ throw new UsageError(`Unit "${unit.unitId}" has no complete frozen engine attribution.`);
323
+ }
318
324
  const action = deriveUnitAction(unit, journaled, ctx);
319
325
  const report = reportCommandForAction(runId, unit, stepId, action, journaled?.claim_holder ?? null);
320
326
  return {
321
327
  unitId: unit.unitId,
322
328
  nodeId: unit.nodeId,
323
329
  index: unit.index,
324
- runner: unit.runner,
325
- ...(unit.profile ? { profile: unit.profile } : {}),
326
- ...(unit.model ? { model: unit.model } : {}),
330
+ engine: unit.invocation.engine,
331
+ runtimeKind: unit.runner,
332
+ platform: unit.engine.kind === "agent" ? unit.engine.platform : null,
333
+ model: unit.invocation.model,
327
334
  timeoutMs: unit.timeoutMs,
328
335
  ...(unit.schema ? { outputSchema: unit.schema } : {}),
329
336
  // Env asset REF names only — brief never resolves bindings, so no secret
@@ -442,19 +449,6 @@ function buildMessage(step, workList, route, gateLoop, settleState) {
442
449
  }
443
450
  return `Active step "${step.stepId}" expects ${n} unit(s)${loopNote}. Execute them, then report each result.`;
444
451
  }
445
- /**
446
- * brief-specific frozen-plan loader: unlike the engine's loader, a NULL
447
- * plan_json is a hard, actionable error rather than a warn-and-compile — brief
448
- * describes the frozen plan the engine executes, and a legacy run has none.
449
- */
450
- function loadFrozenPlanForBrief(runId, planJson, planHash) {
451
- if (!planJson) {
452
- throw new UsageError(`Workflow run ${runId} predates frozen plans (no plan_json on the run row) and cannot be described by ` +
453
- `\`akm workflow brief\`. Drive it with engine-driven mode instead: \`akm workflow run ${runId}\` ` +
454
- `(which compiles a legacy run's plan from the live asset).`);
455
- }
456
- return parseFrozenPlan(runId, planJson, planHash);
457
- }
458
452
  /**
459
453
  * Resolve `target` to a concrete run id WITHOUT starting anything. A run id
460
454
  * resolves directly; a workflow ref resolves to its active run in the current
@@ -0,0 +1,47 @@
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 { deepMergeConfig } from "../../core/config/deep-merge.js";
5
+ import { ConfigError } from "../../core/errors.js";
6
+ /** Build a gate judge from a v3 catalog entry without consulting live config. */
7
+ export function frozenSummaryJudge(plan, invocation) {
8
+ if (!invocation)
9
+ return null;
10
+ const engine = plan.execution?.engines[invocation.engine];
11
+ if (!engine || engine.kind !== "llm")
12
+ throw new ConfigError(`Frozen gate engine "${invocation.engine}" is unavailable.`, "INVALID_CONFIG_FILE");
13
+ return async ({ system, user }) => {
14
+ const { chatCompletion } = await import("../../llm/client.js");
15
+ return chatCompletion(materialize(engine, invocation), [
16
+ { role: "system", content: system },
17
+ { role: "user", content: user },
18
+ ], {
19
+ timeoutMs: invocation.timeoutMs,
20
+ });
21
+ };
22
+ }
23
+ function materialize(engine, invocation) {
24
+ let apiKey;
25
+ for (const name of engine.credential?.names ?? []) {
26
+ const value = process.env[name]?.trim();
27
+ if (value) {
28
+ apiKey = value;
29
+ break;
30
+ }
31
+ }
32
+ if (engine.credential?.required && !apiKey)
33
+ throw new ConfigError(`Required engine credential ${engine.credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
34
+ const base = {
35
+ provider: engine.provider,
36
+ endpoint: engine.endpoint,
37
+ model: invocation.model ?? engine.model,
38
+ ...(engine.temperature !== undefined ? { temperature: engine.temperature } : {}),
39
+ ...(engine.maxTokens !== undefined ? { maxTokens: engine.maxTokens } : {}),
40
+ ...(engine.supportsJsonSchema !== undefined ? { supportsJsonSchema: engine.supportsJsonSchema } : {}),
41
+ ...(engine.extraParams ? { extraParams: engine.extraParams } : {}),
42
+ ...(engine.contextLength !== undefined ? { contextLength: engine.contextLength } : {}),
43
+ ...(engine.enableThinking !== undefined ? { enableThinking: engine.enableThinking } : {}),
44
+ ...(apiKey ? { apiKey } : {}),
45
+ };
46
+ return (invocation.llm ? deepMergeConfig(base, invocation.llm) : base);
47
+ }
@@ -129,9 +129,11 @@
129
129
  * - This module NEVER writes step rows: advancing the gated spine is the
130
130
  * engine loop's job (`run-workflow.ts`) via `completeWorkflowStep`.
131
131
  */
132
+ import { deepMergeConfig } from "../../core/config/deep-merge.js";
132
133
  import { ConfigError } from "../../core/errors.js";
133
134
  import { appendEvent } from "../../core/events.js";
134
135
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
136
+ import { collectSensitiveValues, isEnvPassthroughValueSafeToExpose, redactSensitiveValue } from "../../core/redaction.js";
135
137
  import { runStructured } from "../../core/structured.js";
136
138
  import { warn } from "../../core/warn.js";
137
139
  import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
@@ -254,6 +256,7 @@ export async function executeStepPlan(plan, ctx) {
254
256
  runId: ctx.runId,
255
257
  params: ctx.params,
256
258
  stepOutputs: stepOutputsFromEvidence(ctx.evidence),
259
+ engines: ctx.engines ?? {},
257
260
  ...(ctx.gateLoop !== undefined ? { gateLoop: ctx.gateLoop } : {}),
258
261
  ...(ctx.gateFeedback ? { gateFeedback: ctx.gateFeedback } : {}),
259
262
  });
@@ -310,7 +313,8 @@ export async function executeStepPlan(plan, ctx) {
310
313
  // attempt in dispatchJournaledAttempt.
311
314
  let worktreeBase;
312
315
  if (willDispatch && template.isolation === "worktree") {
313
- if (template.runner === "llm") {
316
+ const engine = template.invocation ? ctx.engines?.[template.invocation.engine] : undefined;
317
+ if (engine?.kind === "llm") {
314
318
  return failedStep(dispatched, `Step "${plan.stepId}" declares isolation: worktree on an llm unit — the llm runner has no ` +
315
319
  `working directory to isolate. Use the agent or sdk runner for worktree-isolated units.`);
316
320
  }
@@ -357,6 +361,12 @@ export async function executeStepPlan(plan, ctx) {
357
361
  ...(onExceeded ? { onExceeded } : {}),
358
362
  });
359
363
  let outcomes;
364
+ const selectedEngine = template.invocation ? ctx.engines?.[template.invocation.engine] : undefined;
365
+ const selectedLlmEngine = selectedEngine?.kind === "llm"
366
+ ? selectedEngine
367
+ : selectedEngine?.kind === "agent" && selectedEngine.fallbackLlmEngine
368
+ ? ctx.engines?.[selectedEngine.fallbackLlmEngine]
369
+ : undefined;
360
370
  try {
361
371
  outcomes = await scheduleUnits(workUnits, (workUnit) => runUnit({
362
372
  plan,
@@ -372,6 +382,7 @@ export async function executeStepPlan(plan, ctx) {
372
382
  concurrency: workList.list.concurrency,
373
383
  signal,
374
384
  maxConcurrency: ctx.maxConcurrency,
385
+ ...(selectedLlmEngine?.kind === "llm" ? { llmConcurrency: selectedLlmEngine.concurrency } : {}),
375
386
  });
376
387
  }
377
388
  finally {
@@ -427,22 +438,32 @@ async function runUnit(input) {
427
438
  if (!workUnit.resolved.ok) {
428
439
  return { unitId, ok: false, failureReason: "expression_error", error: workUnit.resolved.error };
429
440
  }
441
+ if (!workUnit.engine || !workUnit.invocation) {
442
+ return {
443
+ unitId,
444
+ ok: false,
445
+ failureReason: "dispatch_error",
446
+ error: `unit "${unitId}" has no frozen engine snapshot and cannot be dispatched`,
447
+ };
448
+ }
430
449
  // The prompt (and therefore the input hash) was built once with the BASE
431
450
  // unit id by computeStepWorkList: a retry re-dispatches the SAME input, the
432
451
  // `~r<n>` suffix is journal bookkeeping only.
433
452
  const { prompt, inputHash } = workUnit.resolved;
453
+ const sensitiveValues = collectWorkflowDispatchSensitiveValues(workUnit, env);
434
454
  const request = {
435
455
  runId: ctx.runId,
436
456
  stepId: plan.stepId,
437
457
  unitId,
438
458
  nodeId: workUnit.nodeId,
439
459
  prompt,
440
- runner: workUnit.runner,
441
- ...(workUnit.profile ? { profile: workUnit.profile } : {}),
442
- ...(workUnit.model ? { model: workUnit.model } : {}),
460
+ engine: workUnit.engine,
461
+ ...(workUnit.fallbackEngine ? { fallbackEngine: workUnit.fallbackEngine } : {}),
462
+ invocation: workUnit.invocation,
443
463
  timeoutMs: workUnit.timeoutMs,
444
464
  ...(workUnit.schema ? { schema: workUnit.schema } : {}),
445
465
  ...(env ? { env } : {}),
466
+ ...(sensitiveValues.length > 0 ? { sensitiveValues } : {}),
446
467
  ...(input.signal ? { signal: input.signal } : {}),
447
468
  };
448
469
  // Bounded retry (IR v2 failure policy): attempt 0 journals under the base
@@ -560,7 +581,8 @@ async function dispatchJournaledAttempt(input) {
560
581
  parentUnitId: workUnit.isFanOut ? `${plan.stepId}.map` : null,
561
582
  phase: null,
562
583
  runner: workUnit.runner,
563
- model: workUnit.model ?? null,
584
+ engine: request.engine.name,
585
+ model: request.invocation.model,
564
586
  inputHash,
565
587
  worktreePath: worktreePath ?? null,
566
588
  startedAt: new Date().toISOString(),
@@ -573,7 +595,7 @@ async function dispatchJournaledAttempt(input) {
573
595
  ref: ctx.workflowRef,
574
596
  metadata: { runId: ctx.runId, stepId: plan.stepId, unitId: attemptId },
575
597
  });
576
- const outcome = await dispatchUnit(request, dispatcher);
598
+ const outcome = redactUnitOutcome(await dispatchUnit(request, dispatcher), request.sensitiveValues ?? []);
577
599
  await enqueueUnitWrite(async () => {
578
600
  await withWorkflowRunsRepo((repo) => repo.finishUnit({
579
601
  runId: ctx.runId,
@@ -729,8 +751,8 @@ async function resolveEnvBindings(refs) {
729
751
  * agent → `executeRunner` → `runAgent` (per-harness AgentCommandBuilder)
730
752
  * sdk → `executeRunner` → `runOpencodeSdk`
731
753
  *
732
- * `inherit` resolves against config: the node/step profile, else
733
- * `defaults.agent` (sdk when the profile is opencode-sdk), else `defaults.llm`.
754
+ * Every v3 invocation names a frozen engine; no live profile/default fallback
755
+ * is consulted during dispatch.
734
756
  */
735
757
  /**
736
758
  * Build the platform-agnostic {@link import("../../integrations/agent/builder-shared.js").AgentDispatchRequest}
@@ -756,15 +778,14 @@ async function resolveEnvBindings(refs) {
756
778
  export function buildAgentDispatchRequest(request, prompt) {
757
779
  return {
758
780
  prompt,
759
- ...(request.model ? { model: request.model } : {}),
781
+ ...(request.invocation.model ? { model: request.invocation.model } : {}),
782
+ ...(request.invocation.model ? { modelIsExact: true } : {}),
760
783
  ...(request.schema ? { schema: request.schema } : {}),
761
784
  };
762
785
  }
763
786
  export const defaultUnitDispatcher = async (request, feedback) => {
764
- const { loadConfig } = await import("../../core/config/config.js");
765
- const config = loadConfig();
766
787
  const prompt = feedback ? `${request.prompt}\n\n${feedback}` : request.prompt;
767
- const resolved = await resolveUnitRunner(request, config);
788
+ const resolved = frozenUnitRunner(request);
768
789
  // `env` bindings can only reach a child process. The agent (CLI) runner
769
790
  // spawns one per call, and the sdk runner now injects them for real via the
770
791
  // env-keyed opencode server registry (sdk-runner.ts module doc, open seam
@@ -794,15 +815,10 @@ export const defaultUnitDispatcher = async (request, feedback) => {
794
815
  }
795
816
  if (resolved.kind === "llm") {
796
817
  const { chatCompletion, LlmCallError } = await import("../../llm/client.js");
797
- const { resolveModel } = await import("../../integrations/agent/model-aliases.js");
798
- const connection = request.model
799
- ? { ...resolved.connection, model: resolveModel(request.model, "llm", undefined, config.modelAliases) }
800
- : resolved.connection;
818
+ const connection = resolved.connection;
801
819
  try {
802
820
  const text = await chatCompletion(connection, [{ role: "user", content: prompt }], {
803
- // null = author declared `timeout: none` — cap at the max signed
804
- // 32-bit delay (setTimeout's ceiling, ~24.8 days ≈ unbounded here).
805
- timeoutMs: request.timeoutMs === null ? 2 ** 31 - 1 : request.timeoutMs,
821
+ timeoutMs: request.timeoutMs,
806
822
  ...(request.signal ? { signal: request.signal } : {}),
807
823
  // Native structured output where the connection supports it; the
808
824
  // executor's subset validator still runs downstream either way.
@@ -821,8 +837,16 @@ export const defaultUnitDispatcher = async (request, feedback) => {
821
837
  }
822
838
  }
823
839
  const { executeRunner } = await import("../../integrations/agent/runner-dispatch.js");
824
- const profile = request.model ? { ...resolved.profile, model: request.model } : resolved.profile;
825
- const result = await executeRunner({ kind: resolved.kind, profile }, prompt, {
840
+ const profile = request.invocation.model
841
+ ? { ...resolved.profile, model: request.invocation.model, modelIsExact: true }
842
+ : resolved.profile;
843
+ const result = await executeRunner(resolved.kind === "sdk"
844
+ ? {
845
+ kind: "sdk",
846
+ profile,
847
+ ...(resolved.fallbackConnection ? { fallbackConnection: resolved.fallbackConnection } : {}),
848
+ }
849
+ : { kind: "agent", profile }, prompt, {
826
850
  stdio: "captured",
827
851
  parseOutput: "text",
828
852
  timeoutMs: request.timeoutMs,
@@ -863,6 +887,36 @@ export const defaultUnitDispatcher = async (request, feedback) => {
863
887
  ...(result.usage ? { usage: result.usage } : {}),
864
888
  };
865
889
  };
890
+ function collectWorkflowDispatchSensitiveValues(workUnit, env) {
891
+ const values = new Set(Object.values(env ?? {}));
892
+ const addCredential = (engine) => {
893
+ if (!engine)
894
+ return;
895
+ if (engine.kind === "llm") {
896
+ for (const name of engine.credential?.names ?? []) {
897
+ const value = process.env[name]?.trim();
898
+ if (value)
899
+ values.add(value);
900
+ }
901
+ return;
902
+ }
903
+ for (const name of engine.envPassthrough) {
904
+ const value = process.env[name];
905
+ if (!isEnvPassthroughValueSafeToExpose(name, value) && value)
906
+ values.add(value);
907
+ }
908
+ };
909
+ addCredential(workUnit.engine);
910
+ addCredential(workUnit.fallbackEngine);
911
+ return collectSensitiveValues(values);
912
+ }
913
+ function redactUnitOutcome(outcome, sensitiveValues) {
914
+ const redacted = redactSensitiveValue(outcome, sensitiveValues);
915
+ if (outcome.failureReason !== undefined && redacted.failureReason !== outcome.failureReason) {
916
+ redacted.failureReason = "reported_failure";
917
+ }
918
+ return redacted;
919
+ }
866
920
  /**
867
921
  * Map a typed {@link import("../../llm/client.js").LlmCallErrorCode} into the
868
922
  * persisted `AgentFailureReason` taxonomy (agent/spawn.ts) — the ONLY
@@ -897,58 +951,63 @@ export function llmFailureReasonFor(code) {
897
951
  }
898
952
  }
899
953
  /**
900
- * Resolve the harness `resultExtractor` for an agent profile, mirroring the
901
- * platform routing of `getCommandBuilder`: the profile's explicit
902
- * `commandBuilder` wins, else its name (with the `-headless` builtin-variant
903
- * suffix stripped, same derivation as BUILTIN_BUILDERS). Unknown/custom
904
- * platforms resolve to no extractor — raw stdout passes through unchanged.
954
+ * Resolve the harness `resultExtractor` from the canonical platform frozen
955
+ * from the named engine. Unknown platforms pass raw stdout through unchanged.
905
956
  */
906
957
  async function resolveHarnessExtractor(profile) {
907
958
  const { getHarness } = await import("../../integrations/harnesses/index.js");
908
- const platform = profile.commandBuilder ?? profile.name;
909
- const harness = getHarness(platform) ?? getHarness(platform.replace(/-headless$/, ""));
959
+ const harness = getHarness(profile.platform ?? profile.name);
910
960
  return harness?.resultExtractor;
911
961
  }
912
- async function resolveUnitRunner(request, config) {
913
- const requested = request.runner;
914
- if (requested === "llm") {
915
- const connection = request.profile
916
- ? config.profiles?.llm?.[request.profile]
917
- : await requireDefaultLlm(config, request);
918
- if (!connection) {
919
- throw new ConfigError(`Workflow unit "${request.unitId}" wants llm profile "${request.profile}", which is not in profiles.llm.`, "LLM_NOT_CONFIGURED");
920
- }
921
- return { kind: "llm", connection };
922
- }
923
- const profileName = request.profile ?? config.defaults?.agent;
924
- if (profileName) {
925
- const { resolveProfileFromConfig } = await import("../../integrations/agent/config.js");
926
- const profile = resolveProfileFromConfig(profileName, config);
927
- if (!profile) {
928
- throw new ConfigError(`Workflow unit "${request.unitId}" wants agent profile "${profileName}", which cannot be resolved. ` +
929
- `Define profiles.agent."${profileName}" or set defaults.agent.`, "INVALID_CONFIG_FILE");
930
- }
931
- const inferred = profile.sdkMode ? "sdk" : "agent";
932
- if (requested !== "inherit" && requested !== inferred) {
933
- throw new ConfigError(`Workflow unit "${request.unitId}" declares runner "${requested}" but profile "${profileName}" is ${profile.sdkMode ? "an opencode-sdk (sdk) profile" : "a CLI (agent) profile"}.`, "INVALID_CONFIG_FILE");
934
- }
935
- return { kind: inferred, profile };
936
- }
937
- if (requested === "inherit") {
938
- const connection = await requireDefaultLlm(config, request, /* soft */ true);
939
- if (connection)
940
- return { kind: "llm", connection };
962
+ /** Reconstruct the existing RunnerSpec substrate from the frozen allowlist only. */
963
+ function frozenUnitRunner(request) {
964
+ const snapshot = request.engine;
965
+ if (snapshot.kind === "llm") {
966
+ return { kind: "llm", connection: materializeFrozenLlm(snapshot, request.invocation) };
941
967
  }
942
- throw new ConfigError(`Workflow unit "${request.unitId}" has no runnable backend: set defaults.agent or defaults.llm in config.json, ` +
943
- `or declare a runner/profile on the unit.`, "INVALID_CONFIG_FILE");
968
+ const profile = {
969
+ name: snapshot.name,
970
+ platform: snapshot.platform,
971
+ bin: snapshot.bin,
972
+ args: snapshot.args,
973
+ stdio: "captured",
974
+ envPassthrough: snapshot.envPassthrough,
975
+ parseOutput: "text",
976
+ ...(snapshot.workspace ? { workspace: snapshot.workspace } : {}),
977
+ ...(request.invocation?.model ? { model: request.invocation.model } : {}),
978
+ ...(request.invocation?.model ? { modelIsExact: true } : {}),
979
+ };
980
+ if (snapshot.runnerKind === "agent")
981
+ return { kind: "agent", profile };
982
+ // The catalog is supplied transitively by the work-list only for hashing; the
983
+ // SDK runner receives a frozen fallback copied into the request by its caller.
984
+ const fallback = request.fallbackEngine ? materializeFrozenLlm(request.fallbackEngine, undefined) : undefined;
985
+ return { kind: "sdk", profile, ...(fallback ? { fallbackConnection: fallback } : {}) };
944
986
  }
945
- async function requireDefaultLlm(config, request, soft = false) {
946
- const { getDefaultLlmConfig } = await import("../../core/config/config.js");
947
- const connection = getDefaultLlmConfig(config);
948
- if (!connection && !soft) {
949
- throw new ConfigError(`Workflow unit "${request.unitId}" declares runner "llm" but no default LLM is configured (defaults.llm).`, "LLM_NOT_CONFIGURED");
987
+ function materializeFrozenLlm(snapshot, invocation) {
988
+ let apiKey;
989
+ for (const name of snapshot.credential?.names ?? []) {
990
+ const candidate = process.env[name]?.trim();
991
+ if (candidate) {
992
+ apiKey = candidate;
993
+ break;
994
+ }
950
995
  }
951
- return connection ?? undefined;
996
+ if (snapshot.credential?.required && !apiKey)
997
+ throw new ConfigError(`Required engine credential ${snapshot.credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
998
+ const base = {
999
+ provider: snapshot.provider,
1000
+ endpoint: snapshot.endpoint,
1001
+ model: invocation?.model ?? snapshot.model,
1002
+ ...(snapshot.temperature !== undefined ? { temperature: snapshot.temperature } : {}),
1003
+ ...(snapshot.maxTokens !== undefined ? { maxTokens: snapshot.maxTokens } : {}),
1004
+ ...(snapshot.supportsJsonSchema !== undefined ? { supportsJsonSchema: snapshot.supportsJsonSchema } : {}),
1005
+ ...(snapshot.extraParams ? { extraParams: snapshot.extraParams } : {}),
1006
+ ...(snapshot.contextLength !== undefined ? { contextLength: snapshot.contextLength } : {}),
1007
+ ...(snapshot.enableThinking !== undefined ? { enableThinking: snapshot.enableThinking } : {}),
1008
+ ...(apiKey ? { apiKey } : {}),
1009
+ };
1010
+ return invocation?.llm ? deepMergeConfig(base, invocation.llm) : base;
952
1011
  }
953
1012
  // ── Small helpers ────────────────────────────────────────────────────────────
954
1013
  /**