akm-cli 0.9.16 → 0.9.17-alpha.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 (50) hide show
  1. package/CHANGELOG.md +504 -0
  2. package/dist/assets/prompts/consolidate-system.md +4 -11
  3. package/dist/assets/prompts/graph-extract-user-prompt.md +5 -5
  4. package/dist/commands/health/accept-rate.js +6 -0
  5. package/dist/commands/health/checks.js +54 -0
  6. package/dist/commands/health/improve-metrics.js +1 -5
  7. package/dist/commands/health/report-view-model.js +0 -1
  8. package/dist/commands/health.js +10 -0
  9. package/dist/commands/improve/consolidate/chunking.js +19 -35
  10. package/dist/commands/improve/consolidate/merge.js +6 -9
  11. package/dist/commands/improve/consolidate.js +104 -91
  12. package/dist/commands/improve/distill/promote-memory.js +40 -2
  13. package/dist/commands/improve/distill/quality-gate.js +186 -23
  14. package/dist/commands/improve/distill.js +42 -8
  15. package/dist/commands/improve/eligibility.js +13 -3
  16. package/dist/commands/improve/improve-cli.js +32 -9
  17. package/dist/commands/improve/improve-strategies.js +23 -1
  18. package/dist/commands/improve/improve.js +121 -84
  19. package/dist/commands/improve/loop-stages.js +241 -108
  20. package/dist/commands/improve/preparation.js +50 -17
  21. package/dist/commands/improve/reflect.js +16 -5
  22. package/dist/commands/improve/shared.js +0 -10
  23. package/dist/commands/proposal/drain.js +79 -10
  24. package/dist/commands/proposal/proposal-types.js +21 -0
  25. package/dist/commands/proposal/repository.js +108 -29
  26. package/dist/commands/tasks/tasks.js +19 -2
  27. package/dist/core/asset/frontmatter.js +106 -1
  28. package/dist/core/config/config.js +5 -2
  29. package/dist/core/config/retired-experimental-keys-shim.js +62 -0
  30. package/dist/core/config/schema/improve-processes.js +29 -2
  31. package/dist/core/improve-result.js +9 -0
  32. package/dist/core/paths.js +7 -0
  33. package/dist/core/write-source.js +10 -2
  34. package/dist/indexer/ensure-index.js +52 -7
  35. package/dist/indexer/graph/graph-extraction.js +82 -8
  36. package/dist/indexer/passes/memory-inference.js +16 -1
  37. package/dist/llm/client.js +16 -2
  38. package/dist/llm/graph-extract.js +162 -18
  39. package/dist/scripts/akm-migrate-node.js +97 -36
  40. package/dist/scripts/akm-migrate.js +97 -36
  41. package/dist/storage/repositories/index-entries-repository.js +43 -0
  42. package/dist/storage/repositories/proposals-repository.js +4 -1
  43. package/dist/storage/state-db-integrity.js +123 -0
  44. package/dist/workflows/program/schema.js +1 -0
  45. package/docs/reference/cli.md +17 -7
  46. package/docs/reference/data-and-telemetry.md +1 -0
  47. package/package.json +1 -1
  48. package/schemas/akm-config.json +44 -0
  49. package/schemas/akm-workflow.json +1 -0
  50. package/dist/commands/improve/eval-cases.js +0 -52
@@ -25,7 +25,7 @@ import { toErrorMessage } from "../core/common.js";
25
25
  import { ConfigError } from "../core/errors.js";
26
26
  import { parseEmbeddedJsonResponse } from "../core/parse.js";
27
27
  import { warn, warnVerbose } from "../core/warn.js";
28
- import { isContextSizeError } from "./client.js";
28
+ import { isContextSizeError, isTransportFailure, LlmCallError } from "./client.js";
29
29
  import { tryLlmFeature } from "./feature-gate.js";
30
30
  import { callStructured } from "./structured-call.js";
31
31
  /**
@@ -33,7 +33,7 @@ import { callStructured } from "./structured-call.js";
33
33
  * Chosen to be visually clear and unlikely to appear verbatim in asset bodies.
34
34
  */
35
35
  const BATCH_ASSET_SEPARATOR = "=== ASSET";
36
- export const GRAPH_EXTRACT_PROMPT_VERSION = "v2";
36
+ export const GRAPH_EXTRACT_PROMPT_VERSION = "v3";
37
37
  /** Asset bodies longer than this are chunked instead of truncated. */
38
38
  const MAX_CHUNK_BODY_CHARS = 1600;
39
39
  /** Bodies longer than this are excluded from multi-asset batch prompts. */
@@ -44,10 +44,72 @@ const NON_ARRAY_BATCH_DISABLE_THRESHOLD = 2;
44
44
  const MAX_ENTITIES_PER_ASSET = 32;
45
45
  /** Hard cap on relations returned per asset. */
46
46
  const MAX_RELATIONS_PER_ASSET = 32;
47
+ /**
48
+ * Default cap on chunks processed per asset (R12b + R20) — overridable via
49
+ * `processes.graphExtraction.maxChunksPerAsset`. Without a cap, one long file
50
+ * chunked at MAX_CHUNK_BODY_CHARS could spend dozens of calls on a single
51
+ * asset (one file spent 21 of 27 run calls this way) before its output was
52
+ * sliced to MAX_ENTITIES_PER_ASSET/MAX_RELATIONS_PER_ASSET anyway.
53
+ */
54
+ const DEFAULT_MAX_CHUNKS_PER_ASSET = 8;
47
55
  const SYSTEM_PROMPT = systemPromptTemplate;
48
56
  const USER_PROMPT_PREFIX = userPromptTemplate
49
57
  .replace("{{MAX_ENTITIES}}", String(MAX_ENTITIES_PER_ASSET))
50
58
  .replace("{{MAX_RELATIONS}}", String(MAX_RELATIONS_PER_ASSET));
59
+ /**
60
+ * Strict JSON Schema for one asset's extraction payload (R12b, compacted for
61
+ * R12). Sent via `responseSchema` to providers that opt into structured
62
+ * output (`runner.connection.supportsJsonSchema` — same lift as
63
+ * memory-infer.ts's `DERIVED_MEMORY_JSON_SCHEMA`); the client silently drops
64
+ * it otherwise. `maxItems` mirrors MAX_ENTITIES_PER_ASSET/MAX_RELATIONS_PER_ASSET
65
+ * so a compliant provider cannot pay for output beyond what
66
+ * parseGraphExtraction keeps. Relations are compact `[from, type, to]`
67
+ * triples (`type` may be `""`) rather than `{"from","to","type"}` objects —
68
+ * the object-keyed form cost 10+ tokens per relation for no signal, and
69
+ * output tokens cost far more than prompt tokens. There is deliberately no
70
+ * relation-level `confidence` in the schema (the prompt never asks for one);
71
+ * `parseGraphExtraction` still reads it from a legacy object-shaped relation
72
+ * for backward compatibility. `confidence` stays at the extraction level —
73
+ * parseGraphExtraction reads it into the merged confidence.
74
+ * `additionalProperties: false` forbids anything else. Reused as the `items`
75
+ * schema of a batch call's array response (see
76
+ * {@link buildBatchResponseSchema}) so a single-asset and a batched call
77
+ * bound entities/relations identically.
78
+ */
79
+ const GRAPH_EXTRACTION_ITEM_SCHEMA = {
80
+ type: "object",
81
+ properties: {
82
+ entities: { type: "array", items: { type: "string" }, maxItems: MAX_ENTITIES_PER_ASSET },
83
+ relations: {
84
+ type: "array",
85
+ maxItems: MAX_RELATIONS_PER_ASSET,
86
+ items: {
87
+ type: "array",
88
+ items: { type: "string" },
89
+ minItems: 3,
90
+ maxItems: 3,
91
+ },
92
+ },
93
+ confidence: { type: "number" },
94
+ },
95
+ required: ["entities", "relations"],
96
+ additionalProperties: false,
97
+ };
98
+ /** Schema for {@link extractGraphFromBody}'s single-asset `responseSchema`. */
99
+ const GRAPH_EXTRACTION_JSON_SCHEMA = GRAPH_EXTRACTION_ITEM_SCHEMA;
100
+ /**
101
+ * Schema for {@link extractGraphFromBodies}' batch `responseSchema` — an
102
+ * array of exactly `count` {@link GRAPH_EXTRACTION_ITEM_SCHEMA} elements, one
103
+ * per asset in the batch, matching the batch prompt's contract.
104
+ */
105
+ function buildBatchResponseSchema(count) {
106
+ return {
107
+ type: "array",
108
+ minItems: count,
109
+ maxItems: count,
110
+ items: GRAPH_EXTRACTION_ITEM_SCHEMA,
111
+ };
112
+ }
51
113
  const GENERIC_ENTITIES = new Set([
52
114
  "agent",
53
115
  "application",
@@ -152,12 +214,14 @@ function mergeGraphExtractions(extractions) {
152
214
  const relationChunkCounts = new Map();
153
215
  let confidence;
154
216
  let truncationCount = 0;
217
+ let truncatedChunks = 0;
155
218
  let filteredGenericEntities = 0;
156
219
  let filteredInvalidRelations = 0;
157
220
  let filteredLowConfidenceRelations = 0;
158
221
  let firstFailureReason;
159
222
  for (const extraction of extractions) {
160
223
  truncationCount += extraction.truncationCount ?? 0;
224
+ truncatedChunks += extraction.truncatedChunks ?? 0;
161
225
  filteredGenericEntities += extraction.filteredGenericEntities ?? 0;
162
226
  filteredInvalidRelations += extraction.filteredInvalidRelations ?? 0;
163
227
  filteredLowConfidenceRelations += extraction.filteredLowConfidenceRelations ?? 0;
@@ -234,6 +298,7 @@ function mergeGraphExtractions(extractions) {
234
298
  reason,
235
299
  chunkCount: extractions.length,
236
300
  truncationCount,
301
+ truncatedChunks,
237
302
  filteredGenericEntities,
238
303
  filteredInvalidRelations,
239
304
  filteredLowConfidenceRelations,
@@ -283,13 +348,36 @@ function parseGraphExtraction(raw) {
283
348
  let filteredLowConfidenceRelations = 0;
284
349
  if (Array.isArray(item.relations)) {
285
350
  for (const relation of item.relations) {
286
- if (typeof relation !== "object" || relation === null || Array.isArray(relation)) {
351
+ // Compact triple form `[from, type, to]` (R12) — `type` may be "".
352
+ // Legacy `{"from","to","type","confidence"}` object form still parses
353
+ // for backward compatibility (older cached prompts, other callers).
354
+ let fromRaw;
355
+ let toRaw;
356
+ let typeRaw;
357
+ let confidenceRaw;
358
+ if (Array.isArray(relation)) {
359
+ if (relation.length !== 3 ||
360
+ typeof relation[0] !== "string" ||
361
+ typeof relation[1] !== "string" ||
362
+ typeof relation[2] !== "string") {
363
+ filteredInvalidRelations += 1;
364
+ continue;
365
+ }
366
+ fromRaw = normalizeEntityName(relation[0]);
367
+ typeRaw = relation[1];
368
+ toRaw = normalizeEntityName(relation[2]);
369
+ }
370
+ else if (typeof relation === "object" && relation !== null) {
371
+ const rel = relation;
372
+ fromRaw = typeof rel.from === "string" ? normalizeEntityName(rel.from) : "";
373
+ toRaw = typeof rel.to === "string" ? normalizeEntityName(rel.to) : "";
374
+ typeRaw = typeof rel.type === "string" ? rel.type : undefined;
375
+ confidenceRaw = rel.confidence;
376
+ }
377
+ else {
287
378
  filteredInvalidRelations += 1;
288
379
  continue;
289
380
  }
290
- const rel = relation;
291
- const fromRaw = typeof rel.from === "string" ? normalizeEntityName(rel.from) : "";
292
- const toRaw = typeof rel.to === "string" ? normalizeEntityName(rel.to) : "";
293
381
  if (!fromRaw || !toRaw) {
294
382
  filteredInvalidRelations += 1;
295
383
  continue;
@@ -300,12 +388,12 @@ function parseGraphExtraction(raw) {
300
388
  filteredInvalidRelations += 1;
301
389
  continue;
302
390
  }
303
- const type = typeof rel.type === "string" ? normalizeRelationType(rel.type) : undefined;
391
+ const type = typeRaw !== undefined ? normalizeRelationType(typeRaw) : undefined;
304
392
  if (type !== undefined && GENERIC_RELATION_TYPES.has(type)) {
305
393
  filteredInvalidRelations += 1;
306
394
  continue;
307
395
  }
308
- const confidence = parseConfidence(rel.confidence);
396
+ const confidence = parseConfidence(confidenceRaw);
309
397
  if (confidence !== undefined && confidence < MIN_RELATION_CONFIDENCE) {
310
398
  filteredLowConfidenceRelations += 1;
311
399
  continue;
@@ -356,8 +444,8 @@ function parseGraphExtraction(raw) {
356
444
  *
357
445
  * Expected model output (valid JSON array, no prose):
358
446
  * [
359
- * {"entities":["ServiceA","ServiceB"],"relations":[{"from":"ServiceA","to":"ServiceB","type":"integrates with"}]},
360
- * {"entities":["Terraform","Prod cluster"],"relations":[{"from":"Terraform","to":"Prod cluster","type":"provisions"}]},
447
+ * {"entities":["ServiceA","ServiceB"],"relations":[["ServiceA","integrates with","ServiceB"]]},
448
+ * {"entities":["Terraform","Prod cluster"],"relations":[["Terraform","provisions","Prod cluster"]]},
361
449
  * {"entities":[],"relations":[]}
362
450
  * ]
363
451
  *
@@ -389,10 +477,10 @@ function buildBatchUserPrompt(bodies) {
389
477
  return (`Extract entities and relations from the N=${count} assets below.\n\n` +
390
478
  `Rules:\n` +
391
479
  `- Output ONLY a JSON array of exactly ${count} objects, one per asset, preserving input order.\n` +
392
- `- Each object: {"entities": ["Entity One", ...], "relations": [{"from": "A", "to": "B", "type": "uses"}, ...]}\n` +
480
+ `- Each object: {"entities": ["Entity One", ...], "relations": [["A", "uses", "B"], ...]}\n` +
393
481
  `- Entities are short, canonical noun phrases (project names, services, tools, people, file/dir names, technical concepts).\n` +
394
- `- Relations connect two entities that both appear in that asset's entities array.\n` +
395
- `- "type" is a short verb phrase (e.g. "uses", "depends on", "owns"). Optional; omit when unsure.\n` +
482
+ `- Each relation is a 3-element array: [from, type, to]. Relations connect two entities that both appear in that asset's entities array.\n` +
483
+ `- "type" is a short verb phrase (e.g. "uses", "depends on", "owns"). Use "" when unsure.\n` +
396
484
  `- Drop pleasantries, meta-commentary, and timestamps.\n` +
397
485
  `- Limit to at most ${MAX_ENTITIES_PER_ASSET} entities and ${MAX_RELATIONS_PER_ASSET} relations per asset.\n` +
398
486
  `- Use {"entities":[],"relations":[]} for assets with no extractable graph content.\n` +
@@ -504,12 +592,26 @@ export async function extractGraphFromBodies(llmRunner, bodies, signal, akmConfi
504
592
  }
505
593
  const systemPrompt = buildBatchSystemPrompt();
506
594
  const userPrompt = buildBatchUserPrompt(nonEmptyBodies);
595
+ // Same responseSchema lift as extractGraphFromBody (R12b), scoped to this
596
+ // batch's asset count so a compliant provider bounds every element's
597
+ // entities/relations by the same maxItems as the single-asset path.
598
+ const batchResponseSchema = buildBatchResponseSchema(nonEmptyBodies.length);
507
599
  const truncatedBodies = nonEmptyBodies.filter((body) => body.length > MAX_BATCH_BODY_CHARS).length;
508
600
  if (truncatedBodies > 0) {
509
601
  warnVerbose(`graph extraction (batch): ${truncatedBodies}/${nonEmptyBodies.length} asset body/bodies exceed the batch body threshold of ${MAX_BATCH_BODY_CHARS} chars.`);
510
602
  }
511
603
  let batchContextError = false;
512
604
  let nonArrayResponse = false;
605
+ // R2: a dead/erroring provider must not be hammered with a per-asset
606
+ // fallback retry for every body in the batch — that is what turned one
607
+ // outage into 15,453 additional retry attempts. `isTransportFailure`
608
+ // (shared with client.ts's `isRetryable` — see its definition) covers
609
+ // `provider_error`, `network_error`, and `provider_html_error`: the
610
+ // provider itself is failing (a dead endpoint more often raises
611
+ // `network_error` or `provider_html_error` than a plain 5xx), not that this
612
+ // particular response was malformed; skip the fallback and record every
613
+ // asset as failed instead.
614
+ let batchProviderError = false;
513
615
  const batchOutcome = await tryLlmFeature("graph_extraction", akmConfig, async () => {
514
616
  try {
515
617
  const raw = await callGraphLlm(llmRunner, [
@@ -519,6 +621,7 @@ export async function extractGraphFromBodies(llmRunner, bodies, signal, akmConfi
519
621
  temperature: 0.1,
520
622
  timeoutMs: llmRunner.timeoutMs,
521
623
  signal,
624
+ responseSchema: batchResponseSchema,
522
625
  onRetryAttempt: () => bumpTelemetry(options.telemetry, "retryAttempts"),
523
626
  }, options.lease, options.onNotices);
524
627
  if (!raw)
@@ -535,7 +638,12 @@ export async function extractGraphFromBodies(llmRunner, bodies, signal, akmConfi
535
638
  const retryRaw = await callGraphLlm(llmRunner, [
536
639
  { role: "system", content: buildBatchRetrySystemPrompt() },
537
640
  { role: "user", content: userPrompt },
538
- ], { temperature: 0, timeoutMs: llmRunner.timeoutMs, signal }, options.lease, options.onNotices);
641
+ ], {
642
+ temperature: 0,
643
+ timeoutMs: llmRunner.timeoutMs,
644
+ signal,
645
+ responseSchema: batchResponseSchema,
646
+ }, options.lease, options.onNotices);
539
647
  parsed = retryRaw ? parseEmbeddedJsonResponse(retryRaw, { expect: "array" }) : undefined;
540
648
  }
541
649
  if (!Array.isArray(parsed)) {
@@ -564,6 +672,12 @@ export async function extractGraphFromBodies(llmRunner, bodies, signal, akmConfi
564
672
  warn(`graph extraction (batch): context size exceeded for ${nonEmptyBodies.length} asset(s); ` +
565
673
  `skipping batch. promptChars=${userPrompt.length}${formatContextHint(llmRunner)}`);
566
674
  }
675
+ else if (err instanceof LlmCallError && isTransportFailure(err)) {
676
+ batchProviderError = true;
677
+ bumpTelemetry(options.telemetry, "failureCount", nonEmptyBodies.length);
678
+ warn(`graph extraction (batch): provider error (${err.code}) for ${nonEmptyBodies.length} asset(s); ` +
679
+ `skipping per-asset fallback retries. promptChars=${userPrompt.length}${formatContextHint(llmRunner)}: ${errMsg}`);
680
+ }
567
681
  else {
568
682
  warn(`graph extraction (batch) failed for ${nonEmptyBodies.length} asset(s); ` +
569
683
  `promptChars=${userPrompt.length}${formatContextHint(llmRunner)}: ${errMsg}`);
@@ -581,6 +695,13 @@ export async function extractGraphFromBodies(llmRunner, bodies, signal, akmConfi
581
695
  if (batchResult !== null) {
582
696
  applySuccessfulBatchResults(results, batchResult, nonEmptyBodies, nonEmptyIndices, batchState);
583
697
  }
698
+ else if (batchProviderError) {
699
+ // No per-asset fallback against a failing provider — record every asset
700
+ // in this batch as a genuine failure so it is neither silently empty nor
701
+ // retried again below.
702
+ for (const origIdx of nonEmptyIndices)
703
+ results[origIdx] = { entities: [], relations: [], status: "failed", reason: "llm_error" };
704
+ }
584
705
  if (batchContextError && nonEmptyBodies.length > 1) {
585
706
  const splitAt = Math.ceil(nonEmptyBodies.length / 2);
586
707
  const left = await extractGraphFromBodies(llmRunner, nonEmptyBodies.slice(0, splitAt), signal, akmConfig, onFallback, options);
@@ -601,6 +722,8 @@ export async function extractGraphFromBodies(llmRunner, bodies, signal, akmConfi
601
722
  const fallbackIndices = nonEmptyIndices.filter((_origIdx, j) => {
602
723
  if (batchContextError)
603
724
  return false; // skip individual retries on context error
725
+ if (batchProviderError)
726
+ return false; // skip individual retries against a failing provider
604
727
  // Result is still empty → needs a fallback call.
605
728
  if (batchResult === null)
606
729
  return true;
@@ -653,17 +776,34 @@ export async function extractGraphFromBody(llmRunner, body, signal, akmConfig, o
653
776
  bumpTelemetry(options.telemetry, "truncationCount", chunked.truncationCount);
654
777
  warnVerbose(`graph extraction: split a long asset into ${chunked.chunks.length} chunk(s) with ${chunked.truncationCount} hard split(s).`);
655
778
  }
656
- if (chunked.chunks.length > 1) {
779
+ // R12b + R20: bound per-asset cost by capping how many chunks of a long
780
+ // asset are ever sent to the LLM. Excess chunks are dropped, never
781
+ // processed — the coverage loss is recorded as truncatedChunks rather than
782
+ // silently absorbed.
783
+ const maxChunksPerAsset = options.maxChunksPerAsset ?? DEFAULT_MAX_CHUNKS_PER_ASSET;
784
+ const cappedChunks = chunked.chunks.length > maxChunksPerAsset ? chunked.chunks.slice(0, maxChunksPerAsset) : chunked.chunks;
785
+ const truncatedChunkCount = chunked.chunks.length - cappedChunks.length;
786
+ if (truncatedChunkCount > 0) {
787
+ bumpTelemetry(options.telemetry, "truncatedChunks", truncatedChunkCount);
788
+ warnVerbose(`graph extraction: capped a long asset to ${cappedChunks.length} of ${chunked.chunks.length} chunk(s) ` +
789
+ `(maxChunksPerAsset=${maxChunksPerAsset}); ${truncatedChunkCount} chunk(s) not processed.`);
790
+ }
791
+ if (cappedChunks.length > 1) {
657
792
  const chunkResults = [];
658
- for (const chunk of chunked.chunks) {
793
+ for (const chunk of cappedChunks) {
659
794
  chunkResults.push(await extractGraphFromBody(llmRunner, chunk, signal, akmConfig, onFallback, options));
660
795
  }
661
796
  const merged = mergeGraphExtractions(chunkResults);
662
797
  merged.truncationCount = (merged.truncationCount ?? 0) + chunked.truncationCount;
798
+ merged.truncatedChunks = (merged.truncatedChunks ?? 0) + truncatedChunkCount;
663
799
  return merged;
664
800
  }
665
- const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody}`;
666
- return callStructured({
801
+ // When capped down to exactly one chunk from a body that originally split
802
+ // into more, that single surviving chunk (not the full trimmedBody) is what
803
+ // must be sent — otherwise the cap would have no effect on prompt size.
804
+ const bodyForCall = truncatedChunkCount > 0 ? (cappedChunks[0] ?? trimmedBody) : trimmedBody;
805
+ const userPrompt = `${USER_PROMPT_PREFIX}${bodyForCall}`;
806
+ const result = await callStructured({
667
807
  feature: "graph_extraction",
668
808
  akmConfig,
669
809
  runner: llmRunner,
@@ -676,6 +816,7 @@ export async function extractGraphFromBody(llmRunner, body, signal, akmConfig, o
676
816
  temperature: 0.1,
677
817
  timeoutMs: llmRunner.timeoutMs,
678
818
  signal,
819
+ responseSchema: GRAPH_EXTRACTION_JSON_SCHEMA,
679
820
  onRetryAttempt: () => bumpTelemetry(options.telemetry, "retryAttempts"),
680
821
  },
681
822
  onNotices: options.onNotices,
@@ -718,6 +859,9 @@ export async function extractGraphFromBody(llmRunner, body, signal, akmConfig, o
718
859
  fallback: empty(),
719
860
  onFallback,
720
861
  });
862
+ if (truncatedChunkCount > 0)
863
+ result.truncatedChunks = (result.truncatedChunks ?? 0) + truncatedChunkCount;
864
+ return result;
721
865
  }
722
866
  // deduplicateGraph lives in src/indexer/graph/graph-dedup.ts (pure utility, no
723
867
  // LLM calls) — import it from there directly. The re-export that used to live
@@ -12792,6 +12792,7 @@ var init_schema = __esm(() => {
12792
12792
  content_policy_reject: true,
12793
12793
  unsupported_type: true,
12794
12794
  no_change: true,
12795
+ quality_rejected: true,
12795
12796
  aborted: true
12796
12797
  };
12797
12798
  PROGRAM_RETRY_REASONS = Object.keys(RETRY_REASON_SET);
@@ -76098,6 +76099,7 @@ var IMPROVE_PROCESS_BASE_FIELDS = {
76098
76099
  timeoutMs: exports_external.union([positiveInt, exports_external.null()]).optional()
76099
76100
  };
76100
76101
  var allowedTypesField = exports_external.array(exports_external.string().min(1)).optional();
76102
+ var excludeRefPrefixesField = exports_external.array(exports_external.string().min(1)).optional();
76101
76103
  var processLimitField = positiveInt.optional();
76102
76104
  var qualityGateField = exports_external.object({ enabled: exports_external.boolean().optional() }).passthrough().optional();
76103
76105
  var contradictionDetectionField = exports_external.object({ enabled: exports_external.boolean().optional() }).passthrough().optional();
@@ -76138,6 +76140,7 @@ var antiCollapseField = exports_external.object({
76138
76140
  }).passthrough().optional();
76139
76141
  var REFLECT_PROCESS_FIELDS = {
76140
76142
  allowedTypes: allowedTypesField,
76143
+ excludeRefPrefixes: excludeRefPrefixesField,
76141
76144
  limit: processLimitField,
76142
76145
  qualityGate: qualityGateField,
76143
76146
  lowValueFilter: lowValueFilterField
@@ -76169,7 +76172,8 @@ var GRAPH_EXTRACTION_PROCESS_FIELDS = {
76169
76172
  topN: positiveInt.optional(),
76170
76173
  includeTypes: exports_external.array(exports_external.string().min(1)).min(1).optional(),
76171
76174
  batchSize: positiveInt.optional(),
76172
- fullScan: exports_external.boolean().optional()
76175
+ fullScan: exports_external.boolean().optional(),
76176
+ maxChunksPerAsset: positiveInt.optional()
76173
76177
  };
76174
76178
  var EXTRACT_PROCESS_FIELDS = {
76175
76179
  defaultSince: exports_external.string().min(1).optional(),
@@ -76220,6 +76224,15 @@ function checkRetiredProcessKeys(value, ctx) {
76220
76224
  }
76221
76225
  }
76222
76226
  }
76227
+ function rejectExcludeRefPrefixesOutsideReflect(value, ctx) {
76228
+ if ("excludeRefPrefixes" in value) {
76229
+ ctx.addIssue({
76230
+ code: exports_external.ZodIssueCode.custom,
76231
+ path: ["excludeRefPrefixes"],
76232
+ message: "excludeRefPrefixes is only valid on the reflect process"
76233
+ });
76234
+ }
76235
+ }
76223
76236
  var ImproveProcessConfigSchema = exports_external.object({
76224
76237
  ...IMPROVE_PROCESS_BASE_FIELDS,
76225
76238
  ...REFLECT_PROCESS_FIELDS,
@@ -76232,8 +76245,8 @@ var ImproveProcessConfigSchema = exports_external.object({
76232
76245
  ...PROACTIVE_MAINTENANCE_PROCESS_FIELDS
76233
76246
  }).passthrough().superRefine(checkRetiredProcessKeys);
76234
76247
  var ReflectProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...REFLECT_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
76235
- var DistillProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...DISTILL_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
76236
- var ConsolidateProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...CONSOLIDATE_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
76248
+ var DistillProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...DISTILL_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys).superRefine(rejectExcludeRefPrefixesOutsideReflect);
76249
+ var ConsolidateProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...CONSOLIDATE_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys).superRefine(rejectExcludeRefPrefixesOutsideReflect);
76237
76250
  var MemoryInferenceProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...MEMORY_INFERENCE_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
76238
76251
  var GraphExtractionProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...GRAPH_EXTRACTION_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
76239
76252
  var ExtractProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...EXTRACT_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
@@ -77658,6 +77671,31 @@ function upgradeConfigVersion(raw, sourcePath) {
77658
77671
 
77659
77672
  // src/core/config/config.ts
77660
77673
  init_legacy_source_shape_shim();
77674
+
77675
+ // src/core/config/retired-experimental-keys-shim.ts
77676
+ init_common();
77677
+ init_warn();
77678
+ var RETIRED_EXPERIMENTAL_KEYS = ["workflowEngine"];
77679
+ function retiredExperimentalKeysIn(raw) {
77680
+ const experimental = raw.experimental;
77681
+ if (!isRecord(experimental))
77682
+ return [];
77683
+ return RETIRED_EXPERIMENTAL_KEYS.filter((key) => (key in experimental));
77684
+ }
77685
+ function stripRetiredExperimentalKeys(raw, sourcePath) {
77686
+ const present = retiredExperimentalKeysIn(raw);
77687
+ if (present.length === 0)
77688
+ return raw;
77689
+ const original = raw.experimental;
77690
+ const experimental = { ...original };
77691
+ for (const key of present)
77692
+ delete experimental[key];
77693
+ const where = sourcePath ? ` at ${sourcePath}` : "";
77694
+ warnOnce(`config:retired-experimental-key${sourcePath ? `:${sourcePath}` : ""}`, `Config${where} uses the retired experimental key(s) ${present.join(", ")} — ignored in memory. Run \`akm migrate apply\` to remove ${present.length === 1 ? "it" : "them"} from the config file and silence this warning.`);
77695
+ return { ...raw, experimental };
77696
+ }
77697
+
77698
+ // src/core/config/config.ts
77661
77699
  init_paths();
77662
77700
  init_warn();
77663
77701
  var DEFAULT_CONFIG = {
@@ -77717,7 +77755,8 @@ function loadUserConfig() {
77717
77755
  function runConfigFilePipeline(text, sourcePath) {
77718
77756
  const versioned = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
77719
77757
  const parsedRaw = migrateLegacySourceShape(versioned, sourcePath);
77720
- return liftExtraParamsOrThrow(parsedRaw, sourcePath);
77758
+ const liftedRaw = liftExtraParamsOrThrow(parsedRaw, sourcePath);
77759
+ return stripRetiredExperimentalKeys(liftedRaw, sourcePath);
77721
77760
  }
77722
77761
  function liftExtraParamsOrThrow(parsedRaw, sourcePath) {
77723
77762
  const where = sourcePath ? ` at ${sourcePath}` : "";
@@ -86376,8 +86415,11 @@ function assertAkmAssetWrite(source, allowedAdapters = ["akm"]) {
86376
86415
  return;
86377
86416
  throw new UsageError(`Bundle "${source.name}" uses adapter "${source.adapterId}", which does not support AKM asset writes.`, "INVALID_FLAG_VALUE");
86378
86417
  }
86418
+ function isWriteCapableSourceKind(kind) {
86419
+ return kind === "filesystem" || kind === "git";
86420
+ }
86379
86421
  function adaptConfiguredSource(runtime) {
86380
- if (runtime.type !== "filesystem" && runtime.type !== "git") {
86422
+ if (!isWriteCapableSourceKind(runtime.type)) {
86381
86423
  throw new ConfigError(`write-source: source "${runtime.name}" has unsupported kind "${runtime.type}" for writes. ` + "Writes are only defined for `filesystem` and `git` sources.", "INVALID_CONFIG_FILE", 'Use `kind: "filesystem"` or `kind: "git"` for writable sources.');
86382
86424
  }
86383
86425
  const kind = runtime.type;
@@ -93213,32 +93255,7 @@ var GLOBAL_OUTPUT_ARGS2 = {
93213
93255
  init_errors();
93214
93256
 
93215
93257
  // scripts/akm-migrate/help.txt
93216
- var help_default = `Usage: akm-migrate <command> [options]
93217
-
93218
- The one migration tool for an akm installation. Every historical shape akm
93219
- has ever written lives here; the CLI proper reads only current schemas.
93220
- \`status\` and \`apply\` run every step, in order, and print one combined JSON
93221
- plan (exit 1 when any step is blocked):
93222
-
93223
- 1. legacy config \`extraParams\` keys lifted onto first-class engine fields
93224
- 2. scheduler grants bound to the configured source installation that was
93225
- approved (stale grants whose bundle no longer exists are removed)
93226
- 3. pending state.db migrations, historical-destructive ones included,
93227
- with a verified sibling safety copy (the only path that admits them)
93228
- 4. task-v2 files to task v3, then task-v3 files to task source v4
93229
- 5. superseded pre-0.9.0 \`.akm\` residue and stale filesystem transactions
93230
- 6. live \`.akm\` writers relocated to \`$STATE\`/\`$CACHE\`, for every local
93231
- bundle (distill-rejected, eval-cases, measurement verdicts, and stale
93232
- improve-pipeline locks — a lock a live run still holds is left alone)
93233
-
93234
- \`akm migrate status|apply\` wraps this executable; \`akm upgrade\` runs
93235
- \`apply\` after its install step, so an image that ships akm can put either
93236
- in its entrypoint (a current installation is a no-op).
93237
-
93238
- Commands:
93239
- status Inspect every pending migration without changing anything.
93240
- apply [--dry-run] Back up and apply every pending migration.
93241
- `;
93258
+ var help_default = "Usage: akm-migrate <command> [options]\n\nThe one migration tool for an akm installation. Every historical shape akm\nhas ever written lives here; the CLI proper reads only current schemas.\n`status` and `apply` run every step, in order, and print one combined JSON\nplan (exit 1 when any step is blocked):\n\n 1. legacy config `extraParams` keys lifted onto first-class engine fields\n 2. retired `experimental.*` config keys removed (today `workflowEngine`)\n 3. scheduler grants bound to the configured source installation that was\n approved (stale grants whose bundle no longer exists are removed)\n 4. pending state.db migrations, historical-destructive ones included,\n with a verified sibling safety copy (the only path that admits them)\n 5. source-owned schedule enablement converted to host-local scheduler\n grants\n 6. task-v2 files to task v3, then task-v3 files to task source v4\n 7. superseded pre-0.9.0 `.akm` residue and stale filesystem transactions\n 8. live `.akm` writers relocated to `$STATE`/`$CACHE`, for every local\n bundle (distill-rejected, eval-cases, measurement verdicts, and stale\n improve-pipeline locks — a lock a live run still holds is left alone)\n\n`akm migrate status|apply` wraps this executable; `akm upgrade` runs\n`apply` after its install step, so an image that ships akm can put either\nin its entrypoint (a current installation is a no-op).\n\nCommands:\n status Inspect every pending migration without changing anything.\n apply [--dry-run] Back up and apply every pending migration.\n";
93242
93259
 
93243
93260
  // scripts/akm-migrate/run-migrate.ts
93244
93261
  init_common();
@@ -94774,10 +94791,44 @@ function applyConfigExtraParamsLift(configPath) {
94774
94791
  return { applied: true, lifted, conflicts: [] };
94775
94792
  }
94776
94793
 
94794
+ // scripts/akm-migrate/migrate/config-retired-experimental-keys.ts
94795
+ function readRawConfig2(configPath) {
94796
+ const text = readConfigText(configPath);
94797
+ if (text === undefined)
94798
+ return;
94799
+ return parseConfigText(text, configPath);
94800
+ }
94801
+ function findConfigRetiredExperimentalKeys(configPath) {
94802
+ const raw = readRawConfig2(configPath);
94803
+ if (!raw)
94804
+ return { removed: [] };
94805
+ return { removed: retiredExperimentalKeysIn(raw).map((key) => `experimental.${key}`) };
94806
+ }
94807
+ function applyConfigRetiredExperimentalKeys(configPath) {
94808
+ const raw = readRawConfig2(configPath);
94809
+ if (!raw)
94810
+ return { applied: false, removed: [] };
94811
+ const retired = retiredExperimentalKeysIn(raw);
94812
+ if (retired.length === 0)
94813
+ return { applied: false, removed: [] };
94814
+ const experimental = { ...raw.experimental };
94815
+ for (const key of retired)
94816
+ delete experimental[key];
94817
+ const config = { ...raw, experimental };
94818
+ const release = acquireConfigLock();
94819
+ try {
94820
+ backupExistingConfig(configPath);
94821
+ writeConfigAtomic(configPath, config);
94822
+ } finally {
94823
+ release();
94824
+ }
94825
+ return { applied: true, removed: retired.map((key) => `experimental.${key}`) };
94826
+ }
94827
+
94777
94828
  // scripts/akm-migrate/migrate/config-scheduler-source-ids.ts
94778
94829
  init_asset_ref();
94779
94830
  init_bundle_id();
94780
- function readRawConfig2(configPath) {
94831
+ function readRawConfig3(configPath) {
94781
94832
  const text = readConfigText(configPath);
94782
94833
  return text === undefined ? undefined : parseConfigText(text, configPath);
94783
94834
  }
@@ -94832,13 +94883,13 @@ function implicitBundleSourceId(raw, bundleId) {
94832
94883
  return implicitId === bundleId ? filesystemBundleSourceId(root) : undefined;
94833
94884
  }
94834
94885
  function findConfigSchedulerSourceIdMigration(configPath) {
94835
- const raw = readRawConfig2(configPath);
94886
+ const raw = readRawConfig3(configPath);
94836
94887
  return Object.freeze({ changes: Object.freeze(raw ? migrationChanges(raw) : []) });
94837
94888
  }
94838
94889
  function applyConfigSchedulerSourceIdMigration(configPath) {
94839
94890
  const release = acquireConfigLock();
94840
94891
  try {
94841
- const raw = readRawConfig2(configPath);
94892
+ const raw = readRawConfig3(configPath);
94842
94893
  if (!raw)
94843
94894
  return Object.freeze({ applied: false, changes: Object.freeze([]) });
94844
94895
  const changes = migrationChanges(raw);
@@ -97986,7 +98037,7 @@ var GATE_OUTCOMES = {
97986
98037
  "auto-rejected": true
97987
98038
  };
97988
98039
  function validatePresentMetadata(meta) {
97989
- const stringFields = ["sourceRun", "beforeHash", "backupContent"];
98040
+ const stringFields = ["sourceRun", "beforeHash", "beforeHashNormalized", "backupContent"];
97990
98041
  for (const field of stringFields) {
97991
98042
  if (Object.hasOwn(meta, field) && typeof meta[field] !== "string")
97992
98043
  invalidPresentField(field);
@@ -98066,6 +98117,7 @@ function proposalRowToProposal(row) {
98066
98117
  changes,
98067
98118
  ...proposedTarget !== undefined ? { proposedTarget } : {},
98068
98119
  ...typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {},
98120
+ ...typeof meta.beforeHashNormalized === "string" ? { beforeHashNormalized: meta.beforeHashNormalized } : {},
98069
98121
  ...meta.review !== undefined ? { review: meta.review } : {},
98070
98122
  ...typeof meta.confidence === "number" ? { confidence: meta.confidence } : {},
98071
98123
  ...meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {},
@@ -98092,6 +98144,8 @@ function proposalToRowValues(proposal, stashDir) {
98092
98144
  metaObj.proposedTarget = currentProposalTarget(proposal.proposedTarget);
98093
98145
  if (proposal.beforeHash !== undefined)
98094
98146
  metaObj.beforeHash = proposal.beforeHash;
98147
+ if (proposal.beforeHashNormalized !== undefined)
98148
+ metaObj.beforeHashNormalized = proposal.beforeHashNormalized;
98095
98149
  if (proposal.sourceRun !== undefined)
98096
98150
  metaObj.sourceRun = proposal.sourceRun;
98097
98151
  if (proposal.review !== undefined)
@@ -104055,12 +104109,16 @@ async function runMigration(options) {
104055
104109
  if (apply && configExtraParams.applied)
104056
104110
  resetConfigCache();
104057
104111
  const pendingLift = apply ? undefined : configExtraParams.pending;
104112
+ const configRetiredExperimentalKeys = apply ? applyConfigRetiredExperimentalKeys(configPath) : { pending: findConfigRetiredExperimentalKeys(configPath) };
104113
+ if (apply && configRetiredExperimentalKeys.applied)
104114
+ resetConfigCache();
104058
104115
  if (pendingLift && pendingLift.lifted.length > 0) {
104059
104116
  return {
104060
104117
  schemaVersion: 1,
104061
104118
  status: "blocked",
104062
104119
  blockers: pendingLift.lifted,
104063
104120
  configExtraParams,
104121
+ configRetiredExperimentalKeys,
104064
104122
  stateMigrations: { pending: listPendingStateMigrations() }
104065
104123
  };
104066
104124
  }
@@ -104075,6 +104133,7 @@ async function runMigration(options) {
104075
104133
  blockers: pendingSchedulerBindings.changes.map((change) => `${change.kind === "bind" ? "bind" : "drop"} scheduler activation ${change.ref}` + (change.reason ? `: ${change.reason}` : "")),
104076
104134
  configExtraParams,
104077
104135
  configSchedulerSourceIds,
104136
+ configRetiredExperimentalKeys,
104078
104137
  stateMigrations: { pending: listPendingStateMigrations() }
104079
104138
  };
104080
104139
  }
@@ -104094,12 +104153,14 @@ async function runMigration(options) {
104094
104153
  }
104095
104154
  const stateStatus = "pending" in stateMigrations && stateMigrations.pending.length > 0 ? "ready" : "current";
104096
104155
  const schedulerStatus = "pending" in schedulerActivation && schedulerActivation.pending.length > 0 ? "ready" : "current";
104156
+ const retiredKeysStatus = "pending" in configRetiredExperimentalKeys && configRetiredExperimentalKeys.pending.removed.length > 0 ? "ready" : "current";
104097
104157
  return {
104098
104158
  schemaVersion: 1,
104099
- status: worstStatus(worstStatus(worstStatus(taskV3.status, taskV4.status), stateStatus), schedulerStatus),
104159
+ status: worstStatus(worstStatus(worstStatus(worstStatus(taskV3.status, taskV4.status), stateStatus), schedulerStatus), retiredKeysStatus),
104100
104160
  blockers: [...taskV3.blockers, ...taskV4.blockers],
104101
104161
  configExtraParams,
104102
104162
  configSchedulerSourceIds,
104163
+ configRetiredExperimentalKeys,
104103
104164
  stateMigrations,
104104
104165
  schedulerActivation,
104105
104166
  taskV3Migration: taskV3.taskV3Migration,