akm-cli 0.9.11 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/STABILITY.md +6 -1
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/cli/shared.js +16 -4
  8. package/dist/cli.js +15 -13
  9. package/dist/commands/agent/agent-dispatch.js +8 -0
  10. package/dist/commands/command/execution-source-loader.js +25 -22
  11. package/dist/commands/command/portable-template.js +4 -26
  12. package/dist/commands/config-cli.js +10 -4
  13. package/dist/commands/env/env-binding.js +10 -3
  14. package/dist/commands/env/env-cli.js +7 -0
  15. package/dist/commands/env/secret-cli.js +15 -4
  16. package/dist/commands/health/checks.js +186 -71
  17. package/dist/commands/health.js +16 -4
  18. package/dist/commands/improve/distill/quality-gate.js +2 -2
  19. package/dist/commands/improve/distill.js +28 -12
  20. package/dist/commands/improve/execution.js +1 -2
  21. package/dist/commands/improve/extract.js +82 -56
  22. package/dist/commands/improve/improve-strategies.js +26 -8
  23. package/dist/commands/improve/improve.js +13 -0
  24. package/dist/commands/improve/preparation.js +9 -6
  25. package/dist/commands/improve/reflect.js +61 -77
  26. package/dist/commands/lint/index.js +3 -1
  27. package/dist/commands/migrate-cli.js +6 -4
  28. package/dist/commands/proposal/drain-policies.js +22 -2
  29. package/dist/commands/proposal/repository.js +4 -4
  30. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  31. package/dist/commands/proposal/validators/proposals.js +10 -19
  32. package/dist/commands/read/show.js +42 -31
  33. package/dist/commands/registry-cli.js +4 -2
  34. package/dist/commands/sources/init.js +4 -8
  35. package/dist/commands/sources/self-update.js +2 -2
  36. package/dist/commands/sources/source-clone.js +5 -7
  37. package/dist/commands/sources/sources-cli.js +3 -5
  38. package/dist/commands/tasks/tasks-cli.js +4 -12
  39. package/dist/commands/tasks/tasks.js +38 -35
  40. package/dist/commands/workflow-cli.js +17 -15
  41. package/dist/core/activation-policy.js +31 -3
  42. package/dist/core/adapter/execution-source.js +39 -11
  43. package/dist/core/asset/stash-meta.js +7 -41
  44. package/dist/core/common.js +8 -17
  45. package/dist/core/config/config-schema.js +3 -23
  46. package/dist/core/config/config-walker.js +50 -4
  47. package/dist/core/config/config.js +21 -5
  48. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  49. package/dist/core/config/schema/embedding.js +2 -2
  50. package/dist/core/config/schema/engines.js +2 -2
  51. package/dist/core/config/schema/index-config.js +19 -21
  52. package/dist/core/config/schema/primitives.js +21 -10
  53. package/dist/core/config/schema/sources-bundles.js +1 -6
  54. package/dist/core/errors.js +2 -3
  55. package/dist/core/improve-types.js +17 -0
  56. package/dist/core/json-schema.js +1 -11
  57. package/dist/core/maintenance-barrier.js +17 -2
  58. package/dist/core/paths.js +12 -15
  59. package/dist/core/state/migrations.js +28 -0
  60. package/dist/core/state-db.js +28 -1
  61. package/dist/core/write-source.js +6 -6
  62. package/dist/indexer/bundle-identity-guard.js +3 -0
  63. package/dist/indexer/ensure-index.js +5 -0
  64. package/dist/indexer/indexer.js +11 -3
  65. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  66. package/dist/indexer/passes/metadata.js +16 -5
  67. package/dist/indexer/search/search-fields.js +1 -30
  68. package/dist/integrations/agent/engine-resolution.js +15 -1
  69. package/dist/integrations/agent/model-map.js +16 -10
  70. package/dist/integrations/agent/prompts.js +13 -6
  71. package/dist/integrations/lockfile.js +22 -7
  72. package/dist/llm/client.js +16 -0
  73. package/dist/llm/index-passes.js +3 -2
  74. package/dist/output/shapes/passthrough.js +9 -3
  75. package/dist/output/shapes.js +5 -3
  76. package/dist/output/text/workflow-format.js +8 -1
  77. package/dist/scripts/akm-migrate-node.js +1461 -1267
  78. package/dist/scripts/akm-migrate.js +1460 -1266
  79. package/dist/setup/setup.js +14 -21
  80. package/dist/sources/include.js +150 -20
  81. package/dist/sources/providers/git-install.js +14 -12
  82. package/dist/sources/providers/git-provider.js +3 -3
  83. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  84. package/dist/sources/website-url.js +12 -4
  85. package/dist/storage/engines/sqlite-migrations.js +40 -10
  86. package/dist/storage/like-pattern.js +7 -0
  87. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  88. package/dist/storage/repositories/index-connection.js +27 -10
  89. package/dist/storage/repositories/index-entry-schema.js +19 -2
  90. package/dist/storage/repositories/index-schema.js +30 -9
  91. package/dist/storage/repositories/proposals-repository.js +2 -1
  92. package/dist/storage/repositories/task-history-repository.js +14 -7
  93. package/dist/storage/repositories/workflow-runs-repository.js +15 -1
  94. package/dist/storage/sqlite-read-snapshot.js +11 -9
  95. package/dist/tasks/backends/cron.js +34 -5
  96. package/dist/tasks/backends/launchd.js +23 -26
  97. package/dist/tasks/backends/schtasks.js +50 -3
  98. package/dist/tasks/frozen-script.js +2 -0
  99. package/dist/tasks/prepare/prepare.js +2 -7
  100. package/dist/tasks/prepare/script-capture.js +38 -6
  101. package/dist/tasks/schedule.js +154 -13
  102. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  103. package/dist/tasks/source/task-source-v4.js +0 -1
  104. package/dist/workflows/exec/child-workflow.js +2 -3
  105. package/dist/workflows/exec/exec-unit.js +3 -4
  106. package/dist/workflows/exec/run-workflow.js +19 -10
  107. package/dist/workflows/exec/step-work.js +35 -56
  108. package/dist/workflows/freeze/resolve-steps.js +19 -11
  109. package/dist/workflows/freeze/source-freeze.js +7 -0
  110. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  111. package/dist/workflows/freeze/targets/command.js +14 -2
  112. package/dist/workflows/ir/environment-v4.js +4 -2
  113. package/dist/workflows/ir/freeze-v4.js +2 -5
  114. package/dist/workflows/ir/plan-hash.js +0 -3
  115. package/dist/workflows/ir/schema-v4.js +14 -9
  116. package/dist/workflows/ir/schema.js +1 -3
  117. package/dist/workflows/resource-limits.js +35 -48
  118. package/dist/workflows/runtime/plan-classifier.js +89 -41
  119. package/dist/workflows/runtime/run-outputs.js +1 -21
  120. package/dist/workflows/runtime/runs.js +76 -150
  121. package/dist/workflows/source-files.js +28 -54
  122. package/dist/workflows/source-ir/program.js +2 -2
  123. package/dist/workflows/source-ir/semantics.js +5 -23
  124. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  125. package/docs/reference/cli.md +74 -17
  126. package/package.json +1 -1
  127. package/schemas/akm-config.json +5 -10
@@ -4,6 +4,7 @@
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { loadConfig } from "../../core/config/config.js";
6
6
  import { ConfigError } from "../../core/errors.js";
7
+ import { EXTRACT_INFRASTRUCTURE_SKIP_REASONS } from "../../core/improve-types.js";
7
8
  import { listPendingStateMigrations } from "../../core/state-db.js";
8
9
  import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
9
10
  import { lookupApiKeyFileValue, resolveEngine } from "../../integrations/agent/engine-resolution.js";
@@ -11,6 +12,37 @@ import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/e
11
12
  import { loadModelMap, mergeModelMapLayers, parseModelMapLayer, readInstalledModelMapText, resolveModelMapAlias, userModelMapPath, } from "../../integrations/agent/model-map.js";
12
13
  import { resolveImprovePlan } from "../improve/improve-strategies.js";
13
14
  import { ACTIVE_RUN_WARN_MS, TASK_FAIL_RATE_WARN } from "./types.js";
15
+ /** Probe one connection's reachability, once per endpoint; `undefined` when no probe seam is supplied. */
16
+ function probeConnectionReachable(connection, deps, cache) {
17
+ if (!deps.probeReachable)
18
+ return Promise.resolve(undefined);
19
+ const key = connection.endpoint.replace(/\/+$/, "");
20
+ let pending = cache.get(key);
21
+ if (!pending) {
22
+ pending = deps.probeReachable(connection);
23
+ cache.set(key, pending);
24
+ }
25
+ return pending;
26
+ }
27
+ function reachabilityEvidence(reach) {
28
+ return reach
29
+ ? { probed: true, reachable: reach.reachable, ...(reach.error ? { error: reach.error } : {}) }
30
+ : { probed: false, reachable: null };
31
+ }
32
+ /**
33
+ * #914: an unreachable LLM engine is a hard `fail` when it is the resolved
34
+ * `default-llm-engine` (extraction and improve depend on it) but only a
35
+ * `warn` elsewhere. Only a reachability warn escalates; a credential warn
36
+ * stays a warn.
37
+ */
38
+ function escalateDefaultLlmEngineFailure(result) {
39
+ if (result.status === "warn" && result.evidence?.reachable === false) {
40
+ return { ...result, status: "fail" };
41
+ }
42
+ return result;
43
+ }
44
+ /** Rolling window the `session-extraction` check reads from the extract ledger (#914). */
45
+ export const SESSION_EXTRACTION_LEDGER_WINDOW_DAYS = 7;
14
46
  function credentialAvailable(credential, env, apiKeyFile) {
15
47
  if (credential?.required)
16
48
  return credential.names.some((name) => Boolean(env[name]?.trim()));
@@ -21,7 +53,7 @@ function credentialAvailable(credential, env, apiKeyFile) {
21
53
  return lookupApiKeyFileValue(apiKeyFile) !== undefined;
22
54
  return true;
23
55
  }
24
- function runConfiguredEngineProbe(checkName, engineName, config, deps) {
56
+ async function runConfiguredEngineProbe(checkName, engineName, config, deps, reachabilityCache) {
25
57
  if (!engineName) {
26
58
  return {
27
59
  name: checkName,
@@ -102,52 +134,84 @@ function runConfiguredEngineProbe(checkName, engineName, config, deps) {
102
134
  fallbackEngine && !fallback ? "configured fallback LLM connection" : undefined,
103
135
  !fallbackCredentialAvailable ? "required fallback credential" : undefined,
104
136
  ].filter((value) => value !== undefined);
137
+ const sdkEvidence = {
138
+ engine: engineName,
139
+ platform: configuredEngine.platform,
140
+ runtimeKind: "sdk",
141
+ binary,
142
+ binaryAvailable,
143
+ package: "@opencode-ai/sdk",
144
+ packageAvailable,
145
+ model: effectiveModel ?? null,
146
+ configuredModel: configuredModel ?? null,
147
+ modelSource: configuredModel ? "sdk" : effectiveModel ? "fallback" : null,
148
+ fallbackEngine: fallbackEngine ?? null,
149
+ fallbackEndpoint: fallback?.connection.endpoint ?? null,
150
+ fallbackModel: fallback?.connection.model ?? null,
151
+ requiredCredentialAvailable: fallbackCredentialAvailable,
152
+ };
153
+ if (missing.length > 0) {
154
+ return {
155
+ name: checkName,
156
+ kind: "deterministic",
157
+ status: "warn",
158
+ confidence: "high",
159
+ message: `SDK engine "${engineName}" is incomplete: missing ${missing.join(", ")}.`,
160
+ evidence: sdkEvidence,
161
+ };
162
+ }
163
+ // The SDK's LLM fallback is the only endpoint this engine talks to;
164
+ // probe it once every local precondition above is satisfied (#914).
165
+ const reach = fallback ? await probeConnectionReachable(fallback.connection, deps, reachabilityCache) : undefined;
105
166
  return {
106
167
  name: checkName,
107
168
  kind: "deterministic",
108
- status: missing.length === 0 ? "pass" : "warn",
169
+ status: reach && !reach.reachable ? "warn" : "pass",
109
170
  confidence: "high",
110
- message: missing.length === 0
171
+ message: !fallback
111
172
  ? `SDK engine "${engineName}" is available.`
112
- : `SDK engine "${engineName}" is incomplete: missing ${missing.join(", ")}.`,
113
- evidence: {
114
- engine: engineName,
115
- platform: configuredEngine.platform,
116
- runtimeKind: "sdk",
117
- binary,
118
- binaryAvailable,
119
- package: "@opencode-ai/sdk",
120
- packageAvailable,
121
- model: effectiveModel ?? null,
122
- configuredModel: configuredModel ?? null,
123
- modelSource: configuredModel ? "sdk" : effectiveModel ? "fallback" : null,
124
- fallbackEngine: fallbackEngine ?? null,
125
- fallbackEndpoint: fallback?.connection.endpoint ?? null,
126
- fallbackModel: fallback?.connection.model ?? null,
127
- requiredCredentialAvailable: fallbackCredentialAvailable,
128
- },
173
+ : !reach
174
+ ? `SDK engine "${engineName}" is available. Reachability was not probed.`
175
+ : reach.reachable
176
+ ? `SDK engine "${engineName}" is available and its LLM fallback is reachable.`
177
+ : `SDK engine "${engineName}" is available, but its LLM fallback is not reachable: ${reach.error ?? "unknown error"}`,
178
+ evidence: fallback ? { ...sdkEvidence, ...reachabilityEvidence(reach) } : sdkEvidence,
129
179
  };
130
180
  }
131
181
  try {
132
182
  const runner = (deps.resolveEngine ?? resolveEngine)(engineName, config);
133
183
  if (runner.kind === "llm") {
134
184
  const requiredCredentialAvailable = credentialAvailable(runner.credential, env, runner.apiKeyFile);
185
+ const llmEvidence = {
186
+ engine: engineName,
187
+ platform: null,
188
+ runtimeKind: "llm",
189
+ model: runner.connection.model,
190
+ endpoint: runner.connection.endpoint,
191
+ requiredCredentialAvailable,
192
+ };
193
+ if (!requiredCredentialAvailable) {
194
+ return {
195
+ name: checkName,
196
+ kind: "deterministic",
197
+ status: "warn",
198
+ confidence: "high",
199
+ message: `LLM engine "${engineName}" is configured, but its required credential is unavailable.`,
200
+ evidence: llmEvidence,
201
+ };
202
+ }
203
+ const reach = await probeConnectionReachable(runner.connection, deps, reachabilityCache);
135
204
  return {
136
205
  name: checkName,
137
206
  kind: "deterministic",
138
- status: requiredCredentialAvailable ? "pass" : "warn",
207
+ status: reach && !reach.reachable ? "warn" : "pass",
139
208
  confidence: "high",
140
- message: requiredCredentialAvailable
141
- ? `LLM engine "${engineName}" is configured.`
142
- : `LLM engine "${engineName}" is configured, but its required credential is unavailable.`,
143
- evidence: {
144
- engine: engineName,
145
- platform: null,
146
- runtimeKind: "llm",
147
- model: runner.connection.model,
148
- endpoint: runner.connection.endpoint,
149
- requiredCredentialAvailable,
150
- },
209
+ message: !reach
210
+ ? `LLM engine "${engineName}" is configured. Reachability was not probed.`
211
+ : reach.reachable
212
+ ? `LLM engine "${engineName}" is configured and reachable.`
213
+ : `LLM engine "${engineName}" is not reachable: ${reach.error ?? "unknown error"}`,
214
+ evidence: { ...llmEvidence, ...reachabilityEvidence(reach) },
151
215
  };
152
216
  }
153
217
  const profile = runner.profile;
@@ -198,8 +262,10 @@ function projectEngineProbe(result, name) {
198
262
  }
199
263
  function projectSelectedEngineProbe(availability, engineName, checkName) {
200
264
  const result = availability.get(engineName);
201
- if (result)
202
- return projectEngineProbe(result, checkName);
265
+ if (result) {
266
+ const projected = projectEngineProbe(result, checkName);
267
+ return checkName === "default-llm-engine" ? escalateDefaultLlmEngineFailure(projected) : projected;
268
+ }
203
269
  return {
204
270
  name: checkName,
205
271
  kind: "deterministic",
@@ -245,30 +311,36 @@ function configuredEnginesProjection(engineNames, availability) {
245
311
  evidence: { engines },
246
312
  };
247
313
  }
248
- export function runDefaultEngineProbe(deps = {}) {
314
+ export async function runDefaultEngineProbe(deps = {}) {
249
315
  // Probe the effective view: an install with no `defaults.engine` but a usable
250
316
  // opencode binary DOES have a working default, and reporting otherwise would
251
317
  // contradict what `workflow run` / `task run` actually do.
252
318
  const { config } = withEngineFallback(deps.loadConfig?.() ?? loadConfig(), deps.which);
253
- return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps);
319
+ return runConfiguredEngineProbe("default-engine", config.defaults?.engine, config, deps, new Map());
254
320
  }
255
- export function runDefaultLlmEngineProbe(deps = {}) {
321
+ export async function runDefaultLlmEngineProbe(deps = {}) {
256
322
  const config = deps.loadConfig?.() ?? loadConfig();
257
- return runConfiguredEngineProbe("default-llm-engine", config.defaults?.llmEngine, config, deps);
323
+ const engineName = config.defaults?.llmEngine;
324
+ if (!engineName)
325
+ return unconfiguredEngineProbe("default-llm-engine");
326
+ const result = await runConfiguredEngineProbe("configured-engine", engineName, config, deps, new Map());
327
+ return projectSelectedEngineProbe(new Map([[engineName, result]]), engineName, "default-llm-engine");
258
328
  }
259
329
  /** Probe every explicitly configured engine without exposing connection or model material. */
260
- export function runConfiguredEnginesProbe(deps = {}) {
330
+ export async function runConfiguredEnginesProbe(deps = {}) {
261
331
  const config = deps.loadConfig?.() ?? loadConfig();
262
332
  const engineNames = Object.keys(config.engines ?? {}).sort();
263
- const availability = new Map(engineNames.map((engine) => [engine, runConfiguredEngineProbe("configured-engine", engine, config, deps)]));
333
+ const cache = new Map();
334
+ const availability = new Map(await Promise.all(engineNames.map(async (engine) => [engine, await runConfiguredEngineProbe("configured-engine", engine, config, deps, cache)])));
264
335
  return configuredEnginesProjection(engineNames, availability);
265
336
  }
266
337
  /**
267
338
  * Build the per-health-run availability snapshot. Each distinct selected or
268
- * explicitly configured engine is probed once; the three public checks are
269
- * projections of that immutable result set.
339
+ * explicitly configured engine is probed once (reachability probes further
340
+ * de-duped by endpoint, see {@link ReachabilityCache}); the three public
341
+ * checks are projections of that immutable result set.
270
342
  */
271
- export function runHealthEngineProbes(deps = {}) {
343
+ export async function runHealthEngineProbes(deps = {}) {
272
344
  const configured = deps.loadConfig?.() ?? loadConfig();
273
345
  const effective = withEngineFallback(configured, deps.which).config;
274
346
  const defaultEngineName = effective.defaults?.engine;
@@ -277,7 +349,8 @@ export function runHealthEngineProbes(deps = {}) {
277
349
  const probeNames = [...new Set([...explicitEngineNames, defaultEngineName, defaultLlmEngineName])]
278
350
  .filter((name) => name !== undefined)
279
351
  .sort();
280
- const availability = new Map(probeNames.map((engine) => [engine, runConfiguredEngineProbe("configured-engine", engine, effective, deps)]));
352
+ const cache = new Map();
353
+ const availability = new Map(await Promise.all(probeNames.map(async (engine) => [engine, await runConfiguredEngineProbe("configured-engine", engine, effective, deps, cache)])));
281
354
  return Object.freeze({
282
355
  defaultEngine: defaultEngineName
283
356
  ? projectSelectedEngineProbe(availability, defaultEngineName, "default-engine")
@@ -314,17 +387,32 @@ export function runActiveImproveStrategyProbe(deps = {}) {
314
387
  unavailableProcesses.push("triage.judgment");
315
388
  }
316
389
  }
390
+ // #913: name the engine each process actually resolved to, so a
391
+ // strategy-level `engine` pin that shadows `defaults.llmEngine` is
392
+ // visible on `akm health` instead of requiring config archaeology.
393
+ const engines = {};
394
+ for (const [name, process] of Object.entries(plan.processes)) {
395
+ if (process.enabled && process.runner)
396
+ engines[name] = process.runner.engine;
397
+ }
398
+ if (plan.triageJudgment)
399
+ engines["triage.judgment"] = plan.triageJudgment.engine;
400
+ const engineList = Object.entries(engines)
401
+ .sort(([a], [b]) => a.localeCompare(b))
402
+ .map(([process, engine]) => `${process}: "${engine}"`)
403
+ .join(", ");
317
404
  return {
318
405
  name: "active-improve-strategy",
319
406
  kind: "deterministic",
320
407
  status: unavailableProcesses.length === 0 ? "pass" : "warn",
321
408
  confidence: "high",
322
409
  message: unavailableProcesses.length === 0
323
- ? `Active improve strategy "${plan.strategy.name}" has available process engines.`
324
- : `Active improve strategy "${plan.strategy.name}" has unavailable required credentials for: ${unavailableProcesses.join(", ")}.`,
410
+ ? `Active improve strategy "${plan.strategy.name}" has available process engines${engineList ? ` (${engineList})` : ""}.`
411
+ : `Active improve strategy "${plan.strategy.name}" has unavailable required credentials for: ${unavailableProcesses.join(", ")}${engineList ? ` (engines: ${engineList})` : ""}.`,
325
412
  evidence: {
326
413
  strategy: plan.strategy.name,
327
414
  unavailableProcesses,
415
+ engines,
328
416
  },
329
417
  };
330
418
  }
@@ -437,8 +525,11 @@ export function runSelectedModelAliasesProbe(deps = {}) {
437
525
  const outcomes = selected.map(({ engine, alias, modelMapKey }) => {
438
526
  try {
439
527
  const resolution = resolveModelMapAlias(alias, modelMapKey, modelMap.map);
440
- return resolution.interpretation === "alias"
441
- ? { kind: "alias", evidence: { engine, alias, modelMapKey } }
528
+ if (resolution.interpretation === "alias") {
529
+ return { kind: "alias", evidence: { engine, alias, modelMapKey } };
530
+ }
531
+ return resolution.unmappedForEngine
532
+ ? { kind: "missing", evidence: { engine, alias, modelMapKey } }
442
533
  : { kind: "exact" };
443
534
  }
444
535
  catch {
@@ -664,34 +755,58 @@ export const HEALTH_CHECKS = [
664
755
  },
665
756
  },
666
757
  {
758
+ // #914: derived from the `extract_sessions_seen` LEDGER, not
759
+ // `improve_runs.result_json` — the hook-driven `akm proposal extract
760
+ // --session-id ...` (the standard Claude Code plugin setup) never writes
761
+ // an `improve_runs` row, so the old improve_runs-only source reported
762
+ // "not active" as `pass` unconditionally on a plugin-driven machine, no
763
+ // matter how healthy or broken extraction actually was.
667
764
  name: "session-extraction",
668
765
  channel: "advisory",
669
766
  run: (ctx) => {
670
- const sx = ctx.sessionExtraction;
671
- const sxWarnReasons = [];
672
- if (sx.warnings > 0)
673
- sxWarnReasons.push(`${sx.warnings} harness error(s)`);
674
- if (sx.ran && sx.sessionsScanned >= 5 && sx.proposalsCreated === 0)
675
- sxWarnReasons.push("no proposals generated across scanned sessions");
676
- return {
767
+ const { since: ledgerSince, rows } = ctx.sessionExtractionLedger;
768
+ const days = SESSION_EXTRACTION_LEDGER_WINDOW_DAYS;
769
+ const base = {
677
770
  name: "session-extraction",
678
771
  kind: "heuristic",
679
- status: sxWarnReasons.length > 0 ? "warn" : "pass",
680
- confidence: sx.ran ? "medium" : "low",
681
- message: sx.ran
682
- ? sxWarnReasons.length > 0
683
- ? `Session extraction degraded: ${sxWarnReasons.join("; ")}.`
684
- : `Session extraction healthy: ${sx.sessionsScanned} scanned, ${sx.sessionsExtracted} extracted, ${sx.proposalsCreated} proposal(s) created.`
685
- : "Session extraction not active (feature disabled or no harness available).",
686
- evidence: {
687
- ran: sx.ran,
688
- sessionsScanned: sx.sessionsScanned,
689
- sessionsExtracted: sx.sessionsExtracted,
690
- sessionsSkipped: sx.sessionsSkipped,
691
- proposalsCreated: sx.proposalsCreated,
692
- warnings: sx.warnings,
693
- durationMs: sx.durationMs,
694
- },
772
+ confidence: "medium",
773
+ evidence: { ledgerSince, ledgerRows: rows },
774
+ };
775
+ const totalRows = rows.reduce((sum, row) => sum + row.count, 0);
776
+ if (totalRows === 0) {
777
+ return { ...base, status: "unknown", message: `No extraction recorded in the last ${days} days.` };
778
+ }
779
+ // `read_failed` / `exception` land in the ledger as `failed`, not `skipped`.
780
+ const nothingHarvested = rows.every((row) => row.outcome === "skipped" || row.outcome === "failed");
781
+ const knownSkipReasons = [...new Set(rows.flatMap((row) => (row.skipReason ? [row.skipReason] : [])))].sort();
782
+ const allKnownReasonsAreInfra = knownSkipReasons.every((reason) => EXTRACT_INFRASTRUCTURE_SKIP_REASONS.includes(reason));
783
+ if (nothingHarvested && allKnownReasonsAreInfra) {
784
+ const reasonClauses = knownSkipReasons.map((reason) => {
785
+ const engines = [
786
+ ...new Set(rows.flatMap((row) => (row.skipReason === reason && row.engine ? [row.engine] : []))),
787
+ ].sort();
788
+ return engines.length > 0 ? `${reason} (engine ${engines.map((e) => `"${e}"`).join(", ")})` : reason;
789
+ });
790
+ if (rows.some((row) => row.skipReason === null))
791
+ reasonClauses.push("unknown reason");
792
+ const verb = rows.some((row) => row.outcome === "failed") ? "skipped or failed" : "skipped";
793
+ return {
794
+ ...base,
795
+ status: "warn",
796
+ message: `${totalRows} of ${totalRows} sessions in the last ${days} days ${verb}: ${reasonClauses.join(", ")}.`,
797
+ };
798
+ }
799
+ const outcomeCounts = new Map();
800
+ for (const row of rows)
801
+ outcomeCounts.set(row.outcome, (outcomeCounts.get(row.outcome) ?? 0) + row.count);
802
+ const outcomeSummary = [...outcomeCounts.entries()]
803
+ .sort(([a], [b]) => a.localeCompare(b))
804
+ .map(([outcome, count]) => `${outcome}: ${count}`)
805
+ .join(", ");
806
+ return {
807
+ ...base,
808
+ status: "pass",
809
+ message: `Session extraction active in the last ${days} days (${outcomeSummary}).`,
695
810
  };
696
811
  },
697
812
  },
@@ -3,7 +3,7 @@
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 { resolveStashDir } from "../core/common.js";
6
+ import { daysToMs, resolveStashDir } from "../core/common.js";
7
7
  import { loadConfig } from "../core/config/config.js";
8
8
  import { ConfigError, UsageError } from "../core/errors.js";
9
9
  import { readEvents } from "../core/events.js";
@@ -12,12 +12,14 @@ import { classifyPathAccess, describeInaccessiblePath } from "../core/path-acces
12
12
  import { getConfigPath, getDataDir, getDbPath, getStateDbPathInDataDir } from "../core/paths.js";
13
13
  import { listExistingTableNames, listPendingStateMigrations, openStateDatabase } from "../core/state-db.js";
14
14
  import { DURATION_UNITS, parseDuration, parseSinceToIso } from "../core/time.js";
15
+ import { probeLlmEndpoint } from "../llm/client.js";
16
+ import { getExtractOutcomeCountsSince } from "../storage/repositories/extract-sessions-repository.js";
15
17
  import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
16
18
  import { getAllEntries } from "../storage/repositories/index-entries-repository.js";
17
19
  import { queryTaskHistory } from "../storage/repositories/task-history-repository.js";
18
20
  import { pkgVersion } from "../version.js";
19
21
  import { collectImproveAdvisories } from "./health/advisories.js";
20
- import { HEALTH_CHECKS, runHealthEngineProbes, runPendingStateMigrationsCheck, } from "./health/checks.js";
22
+ import { HEALTH_CHECKS, runHealthEngineProbes, runPendingStateMigrationsCheck, SESSION_EXTRACTION_LEDGER_WINDOW_DAYS, } from "./health/checks.js";
21
23
  import { collectDataDirUsageAdvisory } from "./health/data-dir-usage.js";
22
24
  import { buildImproveSkipSummary, computeWallTimeStats, isAgentTaskHistoryRow, roundRate, summarizeImproveCompleted, summarizeImproveRuns, taskFailureDetail, } from "./health/improve-metrics.js";
23
25
  import { emptyLlmUsageAggregate, readLlmUsageAggregate } from "./health/llm-usage.js";
@@ -166,6 +168,11 @@ function gatherEgressConfigPhase() {
166
168
  }
167
169
  return { egressConfigView };
168
170
  }
171
+ /** Extract-ledger outcome counts for the `session-extraction` check's window, independent of `--since`. */
172
+ function gatherSessionExtractionLedgerPhase(db, now) {
173
+ const since = new Date(now() - daysToMs(SESSION_EXTRACTION_LEDGER_WINDOW_DAYS)).toISOString();
174
+ return { since, rows: getExtractOutcomeCountsSince(db, since) };
175
+ }
169
176
  /**
170
177
  * Assemble the window's improve-pipeline summary: invoked/completed/skipped
171
178
  * counts from events, the per-run result_json aggregate, wall-time stats, and
@@ -466,7 +473,7 @@ function unreadableStateDbCheck(detail) {
466
473
  evidence: { detail },
467
474
  };
468
475
  }
469
- export function akmHealth(options = {}) {
476
+ export async function akmHealth(options = {}) {
470
477
  validateAkmHealthOptions(options);
471
478
  const now = options.now ?? (() => Date.now());
472
479
  const since = parseHealthSince(options.since);
@@ -519,12 +526,16 @@ export function akmHealth(options = {}) {
519
526
  logsDb = undefined;
520
527
  }
521
528
  try {
529
+ // Network probes overlap the local database phases below; awaited where consumed.
530
+ const engineProbesPromise = runHealthEngineProbes({ probeReachable: options.probe ? probeLlmEndpoint : undefined });
531
+ engineProbesPromise.catch(() => undefined);
522
532
  const taskHistory = gatherTaskHistoryPhase(db, logsDb, since, stateDbPath, now);
523
533
  const { tableNames, missingTables, probe } = taskHistory;
524
534
  const { egressConfigView } = gatherEgressConfigPhase();
525
535
  const { improveSummary } = gatherImproveSummaryPhase(db, stateDbPath, since, now);
526
536
  advisories.push(...gatherAncillaryAdvisories(db, stateDbPath, since, improveSummary, options, egressConfigView));
527
- const engineProbes = runHealthEngineProbes();
537
+ const sessionExtractionLedger = gatherSessionExtractionLedgerPhase(db, now);
538
+ const engineProbes = await engineProbesPromise;
528
539
  // Run the ordered health-check registry. Each check projects the shared
529
540
  // context computed above into one HealthCheckResult; `channel` routes it to
530
541
  // hardChecks or advisories. Declaration order in HEALTH_CHECKS is the
@@ -544,6 +555,7 @@ export function akmHealth(options = {}) {
544
555
  stuckActiveTasks: taskHistory.stuckActiveTasks,
545
556
  worstTaskFailRate: taskHistory.worstTaskFailRate,
546
557
  sessionExtraction: improveSummary.sessionExtraction,
558
+ sessionExtractionLedger,
547
559
  autoAccept: improveSummary.autoAccept,
548
560
  engineProbes,
549
561
  };
@@ -200,7 +200,7 @@ async function runQualityJudge(feature, config, prompt, chat, options = {}) {
200
200
  parsed.score < 1 ||
201
201
  parsed.score > 5 ||
202
202
  typeof parsed.reason !== "string") {
203
- return { pass: false, score: -1, reason: "judge parse failed — cannot judge, failing closed" };
203
+ return { pass: false, score: -1, reason: "judge parse failed — routed to review", reviewNeeded: true };
204
204
  }
205
205
  // D-5 / #388: Three-band system (MT-Bench arXiv:2306.05685 — ~±0.5 judge variance).
206
206
  // >= 3.5: auto-queue as pending (pass: true)
@@ -219,7 +219,7 @@ async function runQualityJudge(feature, config, prompt, chat, options = {}) {
219
219
  // content verdict. Provider/runtime failures retain the fail-closed result.
220
220
  if (error instanceof ConfigError)
221
221
  throw error;
222
- return { pass: false, score: -1, reason: "judge timeout/error — cannot judge, failing closed" };
222
+ return { pass: false, score: -1, reason: "judge timeout/error — routed to review", reviewNeeded: true };
223
223
  }
224
224
  }
225
225
  /**
@@ -778,7 +778,7 @@ export async function akmDistill(options) {
778
778
  eventsCtx: options.eventsCtx,
779
779
  }));
780
780
  }
781
- const { content, descriptionSwapped } = assembleAndValidateDistillContent({
781
+ const assembled = assembleAndValidateDistillContent({
782
782
  raw,
783
783
  effectiveProposalKind,
784
784
  inputRef,
@@ -789,7 +789,11 @@ export async function akmDistill(options) {
789
789
  filteredFeedbackCount,
790
790
  eligMeta,
791
791
  eventsCtx: options.eventsCtx,
792
+ stash,
792
793
  });
794
+ if ("rejection" in assembled)
795
+ return withNotices(assembled.rejection);
796
+ const { content, descriptionSwapped } = assembled;
793
797
  const gate = await applyDistillQualityGate({
794
798
  config,
795
799
  options,
@@ -968,7 +972,7 @@ async function emitDistillLessonProposal(args) {
968
972
  * throwing a `UsageError` on any finding. Extracted verbatim from `akmDistill`.
969
973
  */
970
974
  function assembleAndValidateDistillContent(args) {
971
- const { raw, effectiveProposalKind, inputRef, durableInputRef, itemRef, effectiveLessonRef, exclusionSet, filteredFeedbackCount, eligMeta, eventsCtx, } = args;
975
+ const { raw, effectiveProposalKind, inputRef, durableInputRef, itemRef, effectiveLessonRef, exclusionSet, filteredFeedbackCount, eligMeta, eventsCtx, stash, } = args;
972
976
  // Structured-output path: when the provider honoured the JSON schema, `raw`
973
977
  // is a JSON object string (not a markdown blob). Try to parse it and assemble
974
978
  // the canonical `---\nfm\n---\n\nbody` form before using the markdown
@@ -1003,18 +1007,19 @@ function assembleAndValidateDistillContent(args) {
1003
1007
  content = repairLessonDescriptionTruncation(content);
1004
1008
  }
1005
1009
  // Parse + lint the lesson before creating the proposal. The lint is the
1006
- // canonical gate for required frontmatter (v1 spec §13). On failure we
1007
- // surface a structured error and exit non-zero but still emit
1008
- // `distill_invoked` so the failure is observable.
1009
- const findings = effectiveProposalKind === "knowledge"
1010
+ // canonical gate for required frontmatter (v1 spec §13): a field that is
1011
+ // genuinely missing or empty means there is no valid asset to write, so
1012
+ // that failure stays a hard reject — but still emit `distill_invoked` so
1013
+ // the failure is observable.
1014
+ const structuralFindings = effectiveProposalKind === "knowledge"
1010
1015
  ? validateKnowledgeContent(content, inputRef)
1011
1016
  : lintLessonContent(content, `distill:${inputRef}`).findings;
1012
1017
  // Additional lesson-only quality validators — reject the systematic failure
1013
1018
  // modes seen across 323 archived rejected proposals (see distill/content-repair).
1014
- if (effectiveProposalKind !== "knowledge" && findings.length === 0) {
1015
- findings.push(...collectLessonQualityFindings(content, inputRef));
1016
- }
1017
- if (findings.length > 0) {
1019
+ const qualityFindings = effectiveProposalKind !== "knowledge" && structuralFindings.length === 0
1020
+ ? collectLessonQualityFindings(content, inputRef)
1021
+ : [];
1022
+ if (structuralFindings.length > 0) {
1018
1023
  appendEvent({
1019
1024
  eventType: "distill_invoked",
1020
1025
  // Use item_ref when resolved, otherwise the input conceptId.
@@ -1023,16 +1028,27 @@ function assembleAndValidateDistillContent(args) {
1023
1028
  outcome: "validation_failed",
1024
1029
  proposalRef: effectiveLessonRef,
1025
1030
  proposalKind: effectiveProposalKind,
1026
- findingKinds: findings.map((f) => f.kind),
1031
+ findingKinds: structuralFindings.map((f) => f.kind),
1027
1032
  ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
1028
1033
  ...eligMeta,
1029
1034
  },
1030
1035
  }, eventsCtx);
1031
- const message = findings.map((f) => f.message).join("\n");
1036
+ const message = structuralFindings.map((f) => f.message).join("\n");
1032
1037
  throw new UsageError(`Distilled ${effectiveProposalKind} failed validation:\n${message}`, "MISSING_REQUIRED_ARGUMENT", effectiveProposalKind === "knowledge"
1033
1038
  ? "Knowledge proposals require a non-empty markdown body."
1034
1039
  : "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
1035
1040
  }
1041
+ if (qualityFindings.length > 0) {
1042
+ return {
1043
+ rejection: writeQualityRejection(stash, inputRef, effectiveLessonRef, content, 2.0, // below auto-accept threshold, signals review needed — no judge score exists for a structural/heuristic finding
1044
+ qualityFindings.map((f) => f.message).join("\n"), {
1045
+ reviewNeeded: true,
1046
+ proposalKind: effectiveProposalKind,
1047
+ findingKinds: qualityFindings.map((f) => f.kind),
1048
+ ...(exclusionSet.size > 0 ? { filteredFeedbackCount } : {}),
1049
+ }, eligMeta.eligibilitySource, eventsCtx),
1050
+ };
1051
+ }
1036
1052
  return { content, descriptionSwapped };
1037
1053
  }
1038
1054
  /**
@@ -2,7 +2,6 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { deepMergeConfig } from "../../core/config/deep-merge.js";
5
- import { ConfigError } from "../../core/errors.js";
6
5
  import { lowerResolvedExecutionRequest } from "../../integrations/agent/execution-lowering.js";
7
6
  import { prepareInlineExecution } from "../../integrations/agent/inline-execution.js";
8
7
  function own(value, key) {
@@ -51,7 +50,7 @@ export function resolveImproveLlmExecution(options) {
51
50
  if (!resolved)
52
51
  return null;
53
52
  if (resolved.runner.kind !== "llm") {
54
- throw new ConfigError(`Engine ${JSON.stringify(resolved.runner.engine ?? "unknown")} is not an LLM engine.`, "INVALID_CONFIG_FILE");
53
+ return null;
55
54
  }
56
55
  return { runner: resolved.runner, notices: resolved.notices };
57
56
  }