akm-cli 0.9.11 → 0.9.13

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 (134) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/STABILITY.md +6 -1
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/assets/workflows/workflow-template.md +4 -0
  8. package/dist/cli/shared.js +16 -4
  9. package/dist/cli.js +15 -13
  10. package/dist/commands/agent/agent-dispatch.js +8 -0
  11. package/dist/commands/command/execution-source-loader.js +25 -22
  12. package/dist/commands/command/portable-template.js +4 -26
  13. package/dist/commands/config-cli.js +10 -4
  14. package/dist/commands/env/env-binding.js +10 -3
  15. package/dist/commands/env/env-cli.js +7 -0
  16. package/dist/commands/env/secret-cli.js +15 -4
  17. package/dist/commands/health/checks.js +186 -71
  18. package/dist/commands/health.js +16 -4
  19. package/dist/commands/improve/distill/quality-gate.js +2 -2
  20. package/dist/commands/improve/distill.js +28 -12
  21. package/dist/commands/improve/execution.js +1 -2
  22. package/dist/commands/improve/extract.js +82 -56
  23. package/dist/commands/improve/improve-strategies.js +26 -8
  24. package/dist/commands/improve/improve.js +14 -0
  25. package/dist/commands/improve/preparation.js +9 -6
  26. package/dist/commands/improve/reflect.js +61 -77
  27. package/dist/commands/lint/base-linter.js +10 -0
  28. package/dist/commands/lint/index.js +3 -1
  29. package/dist/commands/migrate-cli.js +6 -4
  30. package/dist/commands/proposal/drain-policies.js +22 -2
  31. package/dist/commands/proposal/drain.js +48 -6
  32. package/dist/commands/proposal/proposal-cli.js +1 -0
  33. package/dist/commands/proposal/repository.js +4 -4
  34. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  35. package/dist/commands/proposal/validators/proposals.js +10 -19
  36. package/dist/commands/read/show.js +42 -31
  37. package/dist/commands/registry-cli.js +4 -2
  38. package/dist/commands/sources/init.js +4 -8
  39. package/dist/commands/sources/self-update.js +2 -2
  40. package/dist/commands/sources/source-clone.js +5 -7
  41. package/dist/commands/sources/sources-cli.js +3 -5
  42. package/dist/commands/tasks/tasks-cli.js +4 -12
  43. package/dist/commands/tasks/tasks.js +38 -35
  44. package/dist/commands/workflow-cli.js +17 -15
  45. package/dist/core/activation-policy.js +31 -3
  46. package/dist/core/adapter/execution-source.js +39 -11
  47. package/dist/core/asset/stash-meta.js +7 -41
  48. package/dist/core/common.js +8 -17
  49. package/dist/core/config/config-schema.js +3 -23
  50. package/dist/core/config/config-walker.js +56 -6
  51. package/dist/core/config/config.js +42 -17
  52. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  53. package/dist/core/config/schema/embedding.js +2 -2
  54. package/dist/core/config/schema/engines.js +2 -2
  55. package/dist/core/config/schema/index-config.js +19 -21
  56. package/dist/core/config/schema/primitives.js +27 -10
  57. package/dist/core/config/schema/sources-bundles.js +1 -6
  58. package/dist/core/errors.js +4 -3
  59. package/dist/core/improve-types.js +17 -0
  60. package/dist/core/json-schema.js +1 -11
  61. package/dist/core/maintenance-barrier.js +17 -2
  62. package/dist/core/paths.js +12 -15
  63. package/dist/core/state/migrations.js +28 -0
  64. package/dist/core/state-db.js +28 -1
  65. package/dist/core/write-source.js +6 -6
  66. package/dist/indexer/bundle-identity-guard.js +3 -0
  67. package/dist/indexer/ensure-index.js +5 -0
  68. package/dist/indexer/indexer.js +11 -3
  69. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  70. package/dist/indexer/passes/metadata.js +16 -5
  71. package/dist/indexer/search/search-fields.js +1 -30
  72. package/dist/integrations/agent/engine-resolution.js +15 -1
  73. package/dist/integrations/agent/model-map.js +16 -10
  74. package/dist/integrations/agent/prompts.js +13 -6
  75. package/dist/integrations/lockfile.js +22 -7
  76. package/dist/llm/client.js +28 -8
  77. package/dist/llm/embedders/remote.js +3 -2
  78. package/dist/llm/index-passes.js +3 -2
  79. package/dist/output/shapes/passthrough.js +9 -3
  80. package/dist/output/shapes.js +50 -3
  81. package/dist/output/text/proposal-format.js +5 -0
  82. package/dist/output/text/workflow-format.js +8 -1
  83. package/dist/scripts/akm-migrate-node.js +1737 -1392
  84. package/dist/scripts/akm-migrate.js +1736 -1391
  85. package/dist/setup/setup.js +14 -21
  86. package/dist/sources/include.js +150 -20
  87. package/dist/sources/providers/git-install.js +14 -12
  88. package/dist/sources/providers/git-provider.js +3 -3
  89. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  90. package/dist/sources/website-url.js +12 -4
  91. package/dist/storage/engines/sqlite-migrations.js +40 -10
  92. package/dist/storage/like-pattern.js +7 -0
  93. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  94. package/dist/storage/repositories/index-connection.js +27 -10
  95. package/dist/storage/repositories/index-entry-schema.js +19 -2
  96. package/dist/storage/repositories/index-schema.js +30 -9
  97. package/dist/storage/repositories/proposals-repository.js +2 -1
  98. package/dist/storage/repositories/task-history-repository.js +14 -7
  99. package/dist/storage/repositories/workflow-runs-repository.js +133 -11
  100. package/dist/storage/sqlite-read-snapshot.js +11 -9
  101. package/dist/tasks/backends/cron.js +34 -5
  102. package/dist/tasks/backends/launchd.js +23 -26
  103. package/dist/tasks/backends/schtasks.js +50 -3
  104. package/dist/tasks/frozen-script.js +2 -0
  105. package/dist/tasks/prepare/prepare.js +2 -7
  106. package/dist/tasks/prepare/script-capture.js +38 -6
  107. package/dist/tasks/schedule.js +154 -13
  108. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  109. package/dist/tasks/source/task-source-v4.js +0 -1
  110. package/dist/workflows/exec/child-workflow.js +2 -3
  111. package/dist/workflows/exec/exec-unit.js +3 -4
  112. package/dist/workflows/exec/run-workflow.js +20 -11
  113. package/dist/workflows/exec/step-work.js +76 -56
  114. package/dist/workflows/freeze/resolve-steps.js +19 -11
  115. package/dist/workflows/freeze/source-freeze.js +7 -0
  116. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  117. package/dist/workflows/freeze/targets/command.js +14 -2
  118. package/dist/workflows/ir/environment-v4.js +4 -2
  119. package/dist/workflows/ir/freeze-v4.js +2 -5
  120. package/dist/workflows/ir/plan-hash.js +0 -3
  121. package/dist/workflows/ir/schema-v4.js +14 -9
  122. package/dist/workflows/ir/schema.js +1 -3
  123. package/dist/workflows/parser.js +1 -1
  124. package/dist/workflows/resource-limits.js +35 -48
  125. package/dist/workflows/runtime/plan-classifier.js +89 -41
  126. package/dist/workflows/runtime/run-outputs.js +1 -21
  127. package/dist/workflows/runtime/runs.js +104 -154
  128. package/dist/workflows/source-files.js +28 -54
  129. package/dist/workflows/source-ir/program.js +2 -2
  130. package/dist/workflows/source-ir/semantics.js +5 -23
  131. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  132. package/docs/reference/cli.md +92 -17
  133. package/package.json +1 -1
  134. package/schemas/akm-config.json +5 -10
@@ -31,6 +31,7 @@ import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js
31
31
  import { ConfigError, UsageError } from "../../core/errors.js";
32
32
  import { appendEvent } from "../../core/events.js";
33
33
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../../core/file-lock.js";
34
+ import { EXTRACT_INFRASTRUCTURE_SKIP_REASONS } from "../../core/improve-types.js";
34
35
  import { tryAcquireMaintenanceBarrier } from "../../core/maintenance-barrier.js";
35
36
  import { redactErrorBody } from "../../core/redaction.js";
36
37
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
@@ -214,6 +215,24 @@ export function resolveStandaloneExtractPlan(config, selection) {
214
215
  });
215
216
  }
216
217
  // ── Helpers ──────────────────────────────────────────────────────────────────
218
+ /** An extract envelope for a run that processed no sessions; `engine`/`engineKind` only once a runner is resolved. */
219
+ function emptyExtractResult(args) {
220
+ return {
221
+ schemaVersion: 1,
222
+ ok: args.ok,
223
+ shape: "extract-result",
224
+ dryRun: args.dryRun,
225
+ type: args.type,
226
+ sessionsProcessed: 0,
227
+ sessionsSkipped: 0,
228
+ candidatesCreated: 0,
229
+ proposals: [],
230
+ sessions: [],
231
+ warnings: [args.warning],
232
+ durationMs: Date.now() - args.startMs,
233
+ ...(args.llmRunner ? { engine: args.llmRunner.engine, engineKind: args.llmRunner.kind } : {}),
234
+ };
235
+ }
217
236
  /**
218
237
  * Parse a since-string into an absolute ms-epoch cutoff. Accepts:
219
238
  * - ISO timestamps (parsed via Date.parse)
@@ -853,6 +872,7 @@ function recordExtractSessionOutcome(args) {
853
872
  preFilterInputCount: result.preFilter.inputCount,
854
873
  preFilterOutputCount: result.preFilter.outputCount,
855
874
  preFilterTruncatedCount: result.preFilter.truncatedCount,
875
+ engine: result.engine,
856
876
  ...(result.skipReason ? { skipReason: result.skipReason } : {}),
857
877
  ...(result.sessionLogPath ? { logPath: result.sessionLogPath } : {}),
858
878
  ...(result.sessionAssetRef ? { sessionAssetRef: result.sessionAssetRef } : {}),
@@ -864,7 +884,8 @@ function recordExtractSessionOutcome(args) {
864
884
  }
865
885
  }
866
886
  function accountExtractSessionResult(result, triageEnabled, output) {
867
- output.sessions.push(result);
887
+ const stamped = { ...result, engine: output.engine };
888
+ output.sessions.push(stamped);
868
889
  if (triageEnabled) {
869
890
  const preempted = result.skipReason === "read_failed" ||
870
891
  result.skipReason === "too_short" ||
@@ -883,6 +904,7 @@ function accountExtractSessionResult(result, triageEnabled, output) {
883
904
  else
884
905
  output.processedCount += 1;
885
906
  output.allProposalIds.push(...result.proposalIds);
907
+ return stamped;
886
908
  }
887
909
  /**
888
910
  * Iterate the discovered candidate sessions: enforce the per-run cap, take the
@@ -913,6 +935,7 @@ async function runExtractSessionLoop(args) {
913
935
  standardsContext: extractStandardsContext,
914
936
  };
915
937
  const output = {
938
+ engine: llmRunner.engine,
916
939
  sessions: [],
917
940
  processedCount: 0,
918
941
  skippedCount: 0,
@@ -947,14 +970,14 @@ async function runExtractSessionLoop(args) {
947
970
  break;
948
971
  const { summary } = plan;
949
972
  if (plan.kind === "skip") {
950
- accountExtractSessionResult(plan.result, triage.enabled, output);
973
+ const accounted = accountExtractSessionResult(plan.result, triage.enabled, output);
951
974
  recordExtractSessionOutcome({
952
975
  stateDb,
953
976
  trackingEnabled,
954
977
  dryRun,
955
978
  harness: harness.name,
956
979
  summary,
957
- result: plan.result,
980
+ result: accounted,
958
981
  sourceRun,
959
982
  });
960
983
  continue;
@@ -988,14 +1011,14 @@ async function runExtractSessionLoop(args) {
988
1011
  triage,
989
1012
  });
990
1013
  if ("skip" in executionGate) {
991
- accountExtractSessionResult(executionGate.skip, triage.enabled, output);
1014
+ const accounted = accountExtractSessionResult(executionGate.skip, triage.enabled, output);
992
1015
  recordExtractSessionOutcome({
993
1016
  stateDb,
994
1017
  trackingEnabled,
995
1018
  dryRun,
996
1019
  harness: harness.name,
997
1020
  summary,
998
- result: executionGate.skip,
1021
+ result: accounted,
999
1022
  sourceRun,
1000
1023
  });
1001
1024
  refillModelSlot();
@@ -1009,14 +1032,14 @@ async function runExtractSessionLoop(args) {
1009
1032
  for (const warning of result.warnings)
1010
1033
  topLevelWarnings.push(`session ${summary.sessionId}: ${warning}`);
1011
1034
  }
1012
- accountExtractSessionResult(result, triage.enabled, output);
1035
+ const accounted = accountExtractSessionResult(result, triage.enabled, output);
1013
1036
  recordExtractSessionOutcome({
1014
1037
  stateDb,
1015
1038
  trackingEnabled,
1016
1039
  dryRun,
1017
1040
  harness: harness.name,
1018
1041
  summary,
1019
- result,
1042
+ result: accounted,
1020
1043
  sourceRun,
1021
1044
  });
1022
1045
  }
@@ -1098,9 +1121,11 @@ function resolveExtractRunConfig(options, config, extractProcess, activeProfile)
1098
1121
  // Cap on NEW sessions LLM-processed per run; 0 disables. Absent = default.
1099
1122
  // Bounds per-run wall time / LLM cost so a backlog can't push a run past its
1100
1123
  // task timeout — the overflow stays unseen and is picked up by later runs.
1101
- const maxSessionsPerRun = typeof extractProcess?.maxSessionsPerRun === "number"
1102
- ? extractProcess.maxSessionsPerRun
1103
- : DEFAULT_MAX_SESSIONS_PER_RUN;
1124
+ const maxSessionsPerRun = options.since
1125
+ ? 0
1126
+ : typeof extractProcess?.maxSessionsPerRun === "number"
1127
+ ? extractProcess.maxSessionsPerRun
1128
+ : DEFAULT_MAX_SESSIONS_PER_RUN;
1104
1129
  // Default discovery window — process config can override the built-in 24h.
1105
1130
  const effectiveSince = options.since ?? extractProcess?.defaultSince;
1106
1131
  // #626 — resolve the triage gate config once per run. Default-off → the
@@ -1166,7 +1191,7 @@ function resolveExtractRunConfig(options, config, extractProcess, activeProfile)
1166
1191
  * not-found envelope) or the discovery-window listing. Extracted verbatim from
1167
1192
  * `akmExtract`; the 48h default-since floor and location filter are unchanged.
1168
1193
  */
1169
- function discoverExtractCandidates(options, harness, effectiveSince, startMs, dryRun) {
1194
+ function discoverExtractCandidates(options, harness, effectiveSince, startMs, dryRun, llmRunner) {
1170
1195
  if (options.sessionId) {
1171
1196
  const all = harness.listSessions({
1172
1197
  ...(options.location ? { location: options.location } : {}),
@@ -1174,20 +1199,14 @@ function discoverExtractCandidates(options, harness, effectiveSince, startMs, dr
1174
1199
  const target = all.find((s) => s.sessionId === options.sessionId);
1175
1200
  if (!target) {
1176
1201
  return {
1177
- notFound: {
1178
- schemaVersion: 1,
1202
+ notFound: emptyExtractResult({
1179
1203
  ok: false,
1180
- shape: "extract-result",
1181
1204
  dryRun,
1182
1205
  type: options.type,
1183
- sessionsProcessed: 0,
1184
- sessionsSkipped: 0,
1185
- candidatesCreated: 0,
1186
- proposals: [],
1187
- sessions: [],
1188
- warnings: [`session ${options.sessionId} not found for harness ${options.type}`],
1189
- durationMs: Date.now() - startMs,
1190
- },
1206
+ warning: `session ${options.sessionId} not found for harness ${options.type}`,
1207
+ startMs,
1208
+ llmRunner,
1209
+ }),
1191
1210
  };
1192
1211
  }
1193
1212
  return { candidates: [target] };
@@ -1286,6 +1305,28 @@ function emitExtractTriageEvent(args) {
1286
1305
  },
1287
1306
  }, eventsCtx);
1288
1307
  }
1308
+ /**
1309
+ * Count every session's `skipReason` (#912) and push one warning line per
1310
+ * infrastructure reason in {@link EXTRACT_INFRASTRUCTURE_SKIP_REASONS}.
1311
+ * `undefined` when nothing was skipped, so the envelope carries no key.
1312
+ */
1313
+ function buildExtractSkipAggregate(sessions, engine, warnings) {
1314
+ const counts = {};
1315
+ for (const session of sessions) {
1316
+ if (!session.skipReason)
1317
+ continue;
1318
+ counts[session.skipReason] = (counts[session.skipReason] ?? 0) + 1;
1319
+ }
1320
+ if (Object.keys(counts).length === 0)
1321
+ return undefined;
1322
+ const total = sessions.length;
1323
+ for (const reason of EXTRACT_INFRASTRUCTURE_SKIP_REASONS) {
1324
+ const n = counts[reason];
1325
+ if (n)
1326
+ warnings.push(`${n} of ${total} sessions skipped: ${reason} (engine "${engine}")`);
1327
+ }
1328
+ return counts;
1329
+ }
1289
1330
  export async function akmExtract(options) {
1290
1331
  const startMs = Date.now();
1291
1332
  if (!options.type || options.type.trim() === "") {
@@ -1310,20 +1351,13 @@ export async function akmExtract(options) {
1310
1351
  (options.improveProfile ? resolveProcessEnabled("extract", options.improveProfile) : true);
1311
1352
  // Feature-gate early so we get a clean "skipped because disabled" envelope.
1312
1353
  if (!extractEnabled) {
1313
- return {
1314
- schemaVersion: 1,
1354
+ return emptyExtractResult({
1315
1355
  ok: true,
1316
- shape: "extract-result",
1317
1356
  dryRun,
1318
1357
  type: options.type,
1319
- sessionsProcessed: 0,
1320
- sessionsSkipped: 0,
1321
- candidatesCreated: 0,
1322
- proposals: [],
1323
- sessions: [],
1324
- warnings: ["extract is disabled by the selected improve strategy"],
1325
- durationMs: Date.now() - startMs,
1326
- };
1358
+ warning: "extract is disabled by the selected improve strategy",
1359
+ startMs,
1360
+ });
1327
1361
  }
1328
1362
  const { timeoutMs, llmRunner, onNotices, getNotices, maxTotalChars, minContentChars, maxSessionsPerRun, effectiveSince, triage, sessionIndexing, } = resolveExtractRunConfig(options, config, extractProcess, activeProfile);
1329
1363
  // WI-9.10: construct this run's RunContext (extracted to
@@ -1331,39 +1365,27 @@ export async function akmExtract(options) {
1331
1365
  const ctx = buildExtractRunContext({ options, config, stashDir, dryRun, sourceRun, llmRunner });
1332
1366
  const harness = resolveHarness(options.type, options.harnesses);
1333
1367
  if (!harness) {
1334
- return {
1335
- schemaVersion: 1,
1368
+ return emptyExtractResult({
1336
1369
  ok: false,
1337
- shape: "extract-result",
1338
1370
  dryRun,
1339
1371
  type: options.type,
1340
- sessionsProcessed: 0,
1341
- sessionsSkipped: 0,
1342
- candidatesCreated: 0,
1343
- proposals: [],
1344
- sessions: [],
1345
- warnings: [`no available harness matches type "${options.type}" (check that the platform is installed)`],
1346
- durationMs: Date.now() - startMs,
1347
- };
1372
+ warning: `no available harness matches type "${options.type}" (check that the platform is installed)`,
1373
+ startMs,
1374
+ llmRunner,
1375
+ });
1348
1376
  }
1349
1377
  if (!harness.isAvailable()) {
1350
- return {
1351
- schemaVersion: 1,
1378
+ return emptyExtractResult({
1352
1379
  ok: false,
1353
- shape: "extract-result",
1354
1380
  dryRun,
1355
1381
  type: options.type,
1356
- sessionsProcessed: 0,
1357
- sessionsSkipped: 0,
1358
- candidatesCreated: 0,
1359
- proposals: [],
1360
- sessions: [],
1361
- warnings: [`harness ${options.type} is registered but reports not-available (no session data on this machine)`],
1362
- durationMs: Date.now() - startMs,
1363
- };
1382
+ warning: `harness ${options.type} is registered but reports not-available (no session data on this machine)`,
1383
+ startMs,
1384
+ llmRunner,
1385
+ });
1364
1386
  }
1365
1387
  // Decide which sessions to process: explicit sessionId OR discovery via since.
1366
- const discovery = discoverExtractCandidates(options, harness, effectiveSince, startMs, dryRun);
1388
+ const discovery = discoverExtractCandidates(options, harness, effectiveSince, startMs, dryRun, llmRunner);
1367
1389
  if ("notFound" in discovery)
1368
1390
  return discovery.notFound;
1369
1391
  const candidates = discovery.candidates;
@@ -1449,6 +1471,7 @@ export async function akmExtract(options) {
1449
1471
  if (loopResult.deferred > 0) {
1450
1472
  topLevelWarnings.push(`Reached maxSessionsPerRun=${maxSessionsPerRun}; ${loopResult.deferred} session(s) deferred to a later run.`);
1451
1473
  }
1474
+ const skipReasons = buildExtractSkipAggregate(sessions, llmRunner.engine, topLevelWarnings);
1452
1475
  emitExtractTriageEvent({
1453
1476
  modelPlanCount,
1454
1477
  triageEnabled: triage.enabled,
@@ -1475,6 +1498,9 @@ export async function akmExtract(options) {
1475
1498
  warnings: topLevelWarnings,
1476
1499
  durationMs: Date.now() - startMs,
1477
1500
  ...(getNotices().length > 0 ? { notices: getNotices() } : {}),
1501
+ ...(skipReasons ? { skipReasons } : {}),
1502
+ engine: llmRunner.engine,
1503
+ engineKind: llmRunner.kind,
1478
1504
  };
1479
1505
  }
1480
1506
  /**
@@ -88,39 +88,56 @@ export function resolveImprovePlan(name, config, options = {}) {
88
88
  }
89
89
  function buildImprovePlan(strategy, config, options) {
90
90
  const processes = {};
91
+ const engineUnavailable = [];
91
92
  for (const processName of Object.keys(IMPROVE_PROCESS_ENGINE_CAPABILITIES)) {
92
- const processConfig = cloneAndFreeze(strategy.config.processes?.[processName] ?? {});
93
- const enabled = processConfig.enabled === true;
93
+ const sourceProcessConfig = strategy.config.processes?.[processName] ?? {};
94
+ const enabled = sourceProcessConfig.enabled === true;
94
95
  let runner = null;
95
96
  let notices = [];
96
97
  if (IMPROVE_PROCESS_ENGINE_CAPABILITIES[processName] !== "llm" || !enabled) {
97
- processes[processName] = Object.freeze({ enabled, config: processConfig, runner });
98
+ processes[processName] = Object.freeze({ enabled, config: cloneAndFreeze(sourceProcessConfig), runner });
98
99
  continue;
99
100
  }
100
101
  // Validation itself is structural and always runs. Only its optional repair
101
102
  // step needs a model, so disabling repair must not create an LLM preflight.
102
- if (processName !== "validation" || options.repairValidationFailures !== false) {
103
+ const skipsRepairEngine = processName === "validation" && options.repairValidationFailures === false;
104
+ if (!skipsRepairEngine) {
103
105
  const resolved = resolveImproveLlmExecution({
104
106
  config,
105
107
  profile: strategy.config,
106
- process: processConfig,
108
+ process: sourceProcessConfig,
107
109
  processName,
108
110
  });
109
111
  runner = resolved?.runner ?? null;
110
112
  notices = resolved?.notices ?? [];
111
113
  }
112
- if (!runner && !(processName === "validation" && options.repairValidationFailures === false)) {
113
- throw new ConfigError(`Enabled improve process "${processName}" requires an LLM engine. Set defaults.llmEngine or improve.strategies.${strategy.name}.processes.${processName}.engine.`, "LLM_NOT_CONFIGURED");
114
+ if (!runner && !skipsRepairEngine) {
115
+ const configKey = `improve.strategies.${strategy.name}.processes.${processName}.engine`;
116
+ engineUnavailable.push({
117
+ process: processName,
118
+ configKey,
119
+ reason: `requires an LLM engine that is not configured. Set defaults.llmEngine or ${configKey}`,
120
+ });
121
+ processes[processName] = Object.freeze({
122
+ enabled: false,
123
+ config: cloneAndFreeze({ ...sourceProcessConfig, enabled: false }),
124
+ runner: null,
125
+ });
126
+ continue;
114
127
  }
115
128
  if (runner)
116
129
  runner = cloneAndFreeze(runner);
117
130
  processes[processName] = Object.freeze({
118
131
  enabled,
119
- config: processConfig,
132
+ config: cloneAndFreeze(sourceProcessConfig),
120
133
  runner,
121
134
  ...(notices.length > 0 ? { notices: cloneAndFreeze(notices) } : {}),
122
135
  });
123
136
  }
137
+ if (engineUnavailable.length > 0 && !Object.values(processes).some((process) => process.enabled)) {
138
+ const names = engineUnavailable.map((item) => `"${item.process}"`).join(", ");
139
+ throw new ConfigError(`No improve process can run: ${names} ${engineUnavailable.length === 1 ? "requires" : "require"} an LLM engine that is not configured. Set defaults.llmEngine, or the per-process engine key named for each.`, "LLM_NOT_CONFIGURED");
140
+ }
124
141
  const triage = strategy.config.processes?.triage;
125
142
  const judgmentEnabled = triage?.judgment?.enabled === true;
126
143
  const triageJudgmentResolution = processes.triage.enabled && judgmentEnabled
@@ -156,5 +173,6 @@ function buildImprovePlan(strategy, config, options) {
156
173
  ...(triageJudgmentResolution?.notices.length
157
174
  ? { triageJudgmentNotices: cloneAndFreeze(triageJudgmentResolution.notices) }
158
175
  : {}),
176
+ engineUnavailable: Object.freeze(engineUnavailable),
159
177
  });
160
178
  }
@@ -1132,6 +1132,19 @@ async function runImproveStageSequence(args) {
1132
1132
  },
1133
1133
  }, eventsCtx);
1134
1134
  }
1135
+ for (const item of resolvedPlan.engineUnavailable) {
1136
+ warn(`[improve] ${item.process} skipped — it ${item.reason}.`);
1137
+ appendEvent({
1138
+ eventType: "improve_skipped",
1139
+ ref: undefined,
1140
+ metadata: {
1141
+ strategy: selectedStrategy.name,
1142
+ reason: "engine_unavailable",
1143
+ process: item.process,
1144
+ configKey: item.configKey,
1145
+ },
1146
+ }, eventsCtx);
1147
+ }
1135
1148
  // Single prep->loop->post-loop pass, run under the invocation's lock.
1136
1149
  // Accumulators are direct assignments from the single pass's results.
1137
1150
  let preparation;
@@ -1367,6 +1380,7 @@ function finalizeImproveResult(args) {
1367
1380
  promoted: triageDrain.promoted.length,
1368
1381
  rejected: triageDrain.rejected.length,
1369
1382
  deferred: triageDrain.deferred.length,
1383
+ failed: triageDrain.failed.length,
1370
1384
  skippedByCap: triageDrain.skippedByCap.length,
1371
1385
  },
1372
1386
  }
@@ -25,7 +25,7 @@ import { isAutonomyLaneAllowed } from "./autonomy-gate.js";
25
25
  import { akmConsolidate, inspectConsolidationPool } from "./consolidate.js";
26
26
  import { computeSafeChunkSize, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
27
27
  // Eligibility / candidate-selection predicates live in ./eligibility.
28
- import { buildLatestFeedbackTsMap, buildLatestProposalTsMap, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, isSignalDeltaEligible, } from "./eligibility.js";
28
+ import { buildLatestFeedbackTsMap, buildLatestProposalTsMap, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, isSignalDeltaEligible, resolveImproveScope, } from "./eligibility.js";
29
29
  import { akmExtract, countNewExtractCandidates } from "./extract.js";
30
30
  import { computeValenceScore, FEEDBACK_WEIGHT, UTILITY_WEIGHT } from "./feedback-valence.js";
31
31
  import { applyMemoryCleanup } from "./memory/memory-improve.js";
@@ -178,17 +178,20 @@ function evaluateConsolidationEligibility(args) {
178
178
  // #553 minPoolSize guard: skip consolidation when the eligible memory pool is
179
179
  // below a minimum size, rather than spending an LLM pass on a handful of
180
180
  // memories. This is an INDEPENDENT skip condition from #551's mtime pool-delta
181
- // gate — either can skip. Default 500; `minPoolSize: 0` disables the guard.
182
- // Evaluated against the eligible-pool count BEFORE entering the LLM loop so a
183
- // skip costs ZERO LLM calls.
184
- const CONSOLIDATE_DEFAULT_MIN_POOL_SIZE = 500;
181
+ // gate — either can skip. Default 0 (disabled) every built-in strategy
182
+ // used to ship 500, which meant `akm improve --strategy consolidate`, typed
183
+ // by a human, silently did nothing on almost every real install. Evaluated
184
+ // against the eligible-pool count BEFORE entering the LLM loop so a skip
185
+ // costs ZERO LLM calls when an operator opts back into a floor.
186
+ const CONSOLIDATE_DEFAULT_MIN_POOL_SIZE = 0;
185
187
  const configuredMinPoolSize = improveProfile?.processes?.consolidate?.minPoolSize;
186
188
  const minPoolSize = typeof configuredMinPoolSize === "number" ? configuredMinPoolSize : CONSOLIDATE_DEFAULT_MIN_POOL_SIZE;
187
189
  const eligiblePoolSize = typeof memorySummary.eligible === "number" ? memorySummary.eligible : 0;
190
+ const userNamedStrategyOrScope = options.strategy !== undefined || resolveImproveScope(options.scope).mode === "ref";
188
191
  // volumeTriggered means the pool already exceeds the volume threshold (100),
189
192
  // so a force-triggered run never trips the pool-size guard. The guard only
190
193
  // engages when minPoolSize > 0 and the eligible pool is strictly below it.
191
- const poolBelowMinSize = !volumeTriggered && minPoolSize > 0 && eligiblePoolSize < minPoolSize;
194
+ const poolBelowMinSize = !volumeTriggered && !userNamedStrategyOrScope && minPoolSize > 0 && eligiblePoolSize < minPoolSize;
192
195
  return {
193
196
  volumeTriggered,
194
197
  consolidationOnCooldown,