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
@@ -12791,6 +12791,7 @@ var init_schema = __esm(() => {
12791
12791
  content_policy_reject: true,
12792
12792
  unsupported_type: true,
12793
12793
  no_change: true,
12794
+ quality_rejected: true,
12794
12795
  aborted: true
12795
12796
  };
12796
12797
  PROGRAM_RETRY_REASONS = Object.keys(RETRY_REASON_SET);
@@ -75426,6 +75427,7 @@ var IMPROVE_PROCESS_BASE_FIELDS = {
75426
75427
  timeoutMs: exports_external.union([positiveInt, exports_external.null()]).optional()
75427
75428
  };
75428
75429
  var allowedTypesField = exports_external.array(exports_external.string().min(1)).optional();
75430
+ var excludeRefPrefixesField = exports_external.array(exports_external.string().min(1)).optional();
75429
75431
  var processLimitField = positiveInt.optional();
75430
75432
  var qualityGateField = exports_external.object({ enabled: exports_external.boolean().optional() }).passthrough().optional();
75431
75433
  var contradictionDetectionField = exports_external.object({ enabled: exports_external.boolean().optional() }).passthrough().optional();
@@ -75466,6 +75468,7 @@ var antiCollapseField = exports_external.object({
75466
75468
  }).passthrough().optional();
75467
75469
  var REFLECT_PROCESS_FIELDS = {
75468
75470
  allowedTypes: allowedTypesField,
75471
+ excludeRefPrefixes: excludeRefPrefixesField,
75469
75472
  limit: processLimitField,
75470
75473
  qualityGate: qualityGateField,
75471
75474
  lowValueFilter: lowValueFilterField
@@ -75497,7 +75500,8 @@ var GRAPH_EXTRACTION_PROCESS_FIELDS = {
75497
75500
  topN: positiveInt.optional(),
75498
75501
  includeTypes: exports_external.array(exports_external.string().min(1)).min(1).optional(),
75499
75502
  batchSize: positiveInt.optional(),
75500
- fullScan: exports_external.boolean().optional()
75503
+ fullScan: exports_external.boolean().optional(),
75504
+ maxChunksPerAsset: positiveInt.optional()
75501
75505
  };
75502
75506
  var EXTRACT_PROCESS_FIELDS = {
75503
75507
  defaultSince: exports_external.string().min(1).optional(),
@@ -75548,6 +75552,15 @@ function checkRetiredProcessKeys(value, ctx) {
75548
75552
  }
75549
75553
  }
75550
75554
  }
75555
+ function rejectExcludeRefPrefixesOutsideReflect(value, ctx) {
75556
+ if ("excludeRefPrefixes" in value) {
75557
+ ctx.addIssue({
75558
+ code: exports_external.ZodIssueCode.custom,
75559
+ path: ["excludeRefPrefixes"],
75560
+ message: "excludeRefPrefixes is only valid on the reflect process"
75561
+ });
75562
+ }
75563
+ }
75551
75564
  var ImproveProcessConfigSchema = exports_external.object({
75552
75565
  ...IMPROVE_PROCESS_BASE_FIELDS,
75553
75566
  ...REFLECT_PROCESS_FIELDS,
@@ -75560,8 +75573,8 @@ var ImproveProcessConfigSchema = exports_external.object({
75560
75573
  ...PROACTIVE_MAINTENANCE_PROCESS_FIELDS
75561
75574
  }).passthrough().superRefine(checkRetiredProcessKeys);
75562
75575
  var ReflectProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...REFLECT_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
75563
- var DistillProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...DISTILL_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
75564
- var ConsolidateProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...CONSOLIDATE_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
75576
+ var DistillProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...DISTILL_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys).superRefine(rejectExcludeRefPrefixesOutsideReflect);
75577
+ var ConsolidateProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...CONSOLIDATE_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys).superRefine(rejectExcludeRefPrefixesOutsideReflect);
75565
75578
  var MemoryInferenceProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...MEMORY_INFERENCE_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
75566
75579
  var GraphExtractionProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...GRAPH_EXTRACTION_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
75567
75580
  var ExtractProcessConfigSchema = exports_external.object({ ...IMPROVE_PROCESS_BASE_FIELDS, ...EXTRACT_PROCESS_FIELDS }).passthrough().superRefine(checkRetiredProcessKeys);
@@ -76986,6 +76999,31 @@ function upgradeConfigVersion(raw, sourcePath) {
76986
76999
 
76987
77000
  // src/core/config/config.ts
76988
77001
  init_legacy_source_shape_shim();
77002
+
77003
+ // src/core/config/retired-experimental-keys-shim.ts
77004
+ init_common();
77005
+ init_warn();
77006
+ var RETIRED_EXPERIMENTAL_KEYS = ["workflowEngine"];
77007
+ function retiredExperimentalKeysIn(raw) {
77008
+ const experimental = raw.experimental;
77009
+ if (!isRecord(experimental))
77010
+ return [];
77011
+ return RETIRED_EXPERIMENTAL_KEYS.filter((key) => (key in experimental));
77012
+ }
77013
+ function stripRetiredExperimentalKeys(raw, sourcePath) {
77014
+ const present = retiredExperimentalKeysIn(raw);
77015
+ if (present.length === 0)
77016
+ return raw;
77017
+ const original = raw.experimental;
77018
+ const experimental = { ...original };
77019
+ for (const key of present)
77020
+ delete experimental[key];
77021
+ const where = sourcePath ? ` at ${sourcePath}` : "";
77022
+ warnOnce(`config:retired-experimental-key${sourcePath ? `:${sourcePath}` : ""}`, `Config${where} uses the retired experimental key(s) ${present.join(", ")} \u2014 ignored in memory. Run \`akm migrate apply\` to remove ${present.length === 1 ? "it" : "them"} from the config file and silence this warning.`);
77023
+ return { ...raw, experimental };
77024
+ }
77025
+
77026
+ // src/core/config/config.ts
76989
77027
  init_paths();
76990
77028
  init_warn();
76991
77029
  var DEFAULT_CONFIG = {
@@ -77045,7 +77083,8 @@ function loadUserConfig() {
77045
77083
  function runConfigFilePipeline(text, sourcePath) {
77046
77084
  const versioned = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
77047
77085
  const parsedRaw = migrateLegacySourceShape(versioned, sourcePath);
77048
- return liftExtraParamsOrThrow(parsedRaw, sourcePath);
77086
+ const liftedRaw = liftExtraParamsOrThrow(parsedRaw, sourcePath);
77087
+ return stripRetiredExperimentalKeys(liftedRaw, sourcePath);
77049
77088
  }
77050
77089
  function liftExtraParamsOrThrow(parsedRaw, sourcePath) {
77051
77090
  const where = sourcePath ? ` at ${sourcePath}` : "";
@@ -85704,8 +85743,11 @@ function assertAkmAssetWrite(source, allowedAdapters = ["akm"]) {
85704
85743
  return;
85705
85744
  throw new UsageError(`Bundle "${source.name}" uses adapter "${source.adapterId}", which does not support AKM asset writes.`, "INVALID_FLAG_VALUE");
85706
85745
  }
85746
+ function isWriteCapableSourceKind(kind) {
85747
+ return kind === "filesystem" || kind === "git";
85748
+ }
85707
85749
  function adaptConfiguredSource(runtime) {
85708
- if (runtime.type !== "filesystem" && runtime.type !== "git") {
85750
+ if (!isWriteCapableSourceKind(runtime.type)) {
85709
85751
  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.');
85710
85752
  }
85711
85753
  const kind = runtime.type;
@@ -93169,32 +93211,7 @@ var GLOBAL_OUTPUT_ARGS = {
93169
93211
  init_errors();
93170
93212
 
93171
93213
  // scripts/akm-migrate/help.txt
93172
- var help_default = `Usage: akm-migrate <command> [options]
93173
-
93174
- The one migration tool for an akm installation. Every historical shape akm
93175
- has ever written lives here; the CLI proper reads only current schemas.
93176
- \`status\` and \`apply\` run every step, in order, and print one combined JSON
93177
- plan (exit 1 when any step is blocked):
93178
-
93179
- 1. legacy config \`extraParams\` keys lifted onto first-class engine fields
93180
- 2. scheduler grants bound to the configured source installation that was
93181
- approved (stale grants whose bundle no longer exists are removed)
93182
- 3. pending state.db migrations, historical-destructive ones included,
93183
- with a verified sibling safety copy (the only path that admits them)
93184
- 4. task-v2 files to task v3, then task-v3 files to task source v4
93185
- 5. superseded pre-0.9.0 \`.akm\` residue and stale filesystem transactions
93186
- 6. live \`.akm\` writers relocated to \`$STATE\`/\`$CACHE\`, for every local
93187
- bundle (distill-rejected, eval-cases, measurement verdicts, and stale
93188
- improve-pipeline locks \u2014 a lock a live run still holds is left alone)
93189
-
93190
- \`akm migrate status|apply\` wraps this executable; \`akm upgrade\` runs
93191
- \`apply\` after its install step, so an image that ships akm can put either
93192
- in its entrypoint (a current installation is a no-op).
93193
-
93194
- Commands:
93195
- status Inspect every pending migration without changing anything.
93196
- apply [--dry-run] Back up and apply every pending migration.
93197
- `;
93214
+ 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 \u2014 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";
93198
93215
 
93199
93216
  // scripts/akm-migrate/run-migrate.ts
93200
93217
  init_common();
@@ -94730,10 +94747,44 @@ function applyConfigExtraParamsLift(configPath) {
94730
94747
  return { applied: true, lifted, conflicts: [] };
94731
94748
  }
94732
94749
 
94750
+ // scripts/akm-migrate/migrate/config-retired-experimental-keys.ts
94751
+ function readRawConfig2(configPath) {
94752
+ const text = readConfigText(configPath);
94753
+ if (text === undefined)
94754
+ return;
94755
+ return parseConfigText(text, configPath);
94756
+ }
94757
+ function findConfigRetiredExperimentalKeys(configPath) {
94758
+ const raw = readRawConfig2(configPath);
94759
+ if (!raw)
94760
+ return { removed: [] };
94761
+ return { removed: retiredExperimentalKeysIn(raw).map((key) => `experimental.${key}`) };
94762
+ }
94763
+ function applyConfigRetiredExperimentalKeys(configPath) {
94764
+ const raw = readRawConfig2(configPath);
94765
+ if (!raw)
94766
+ return { applied: false, removed: [] };
94767
+ const retired = retiredExperimentalKeysIn(raw);
94768
+ if (retired.length === 0)
94769
+ return { applied: false, removed: [] };
94770
+ const experimental = { ...raw.experimental };
94771
+ for (const key of retired)
94772
+ delete experimental[key];
94773
+ const config = { ...raw, experimental };
94774
+ const release = acquireConfigLock();
94775
+ try {
94776
+ backupExistingConfig(configPath);
94777
+ writeConfigAtomic(configPath, config);
94778
+ } finally {
94779
+ release();
94780
+ }
94781
+ return { applied: true, removed: retired.map((key) => `experimental.${key}`) };
94782
+ }
94783
+
94733
94784
  // scripts/akm-migrate/migrate/config-scheduler-source-ids.ts
94734
94785
  init_asset_ref();
94735
94786
  init_bundle_id();
94736
- function readRawConfig2(configPath) {
94787
+ function readRawConfig3(configPath) {
94737
94788
  const text = readConfigText(configPath);
94738
94789
  return text === undefined ? undefined : parseConfigText(text, configPath);
94739
94790
  }
@@ -94788,13 +94839,13 @@ function implicitBundleSourceId(raw, bundleId) {
94788
94839
  return implicitId === bundleId ? filesystemBundleSourceId(root2) : undefined;
94789
94840
  }
94790
94841
  function findConfigSchedulerSourceIdMigration(configPath) {
94791
- const raw = readRawConfig2(configPath);
94842
+ const raw = readRawConfig3(configPath);
94792
94843
  return Object.freeze({ changes: Object.freeze(raw ? migrationChanges(raw) : []) });
94793
94844
  }
94794
94845
  function applyConfigSchedulerSourceIdMigration(configPath) {
94795
94846
  const release = acquireConfigLock();
94796
94847
  try {
94797
- const raw = readRawConfig2(configPath);
94848
+ const raw = readRawConfig3(configPath);
94798
94849
  if (!raw)
94799
94850
  return Object.freeze({ applied: false, changes: Object.freeze([]) });
94800
94851
  const changes = migrationChanges(raw);
@@ -97942,7 +97993,7 @@ var GATE_OUTCOMES = {
97942
97993
  "auto-rejected": true
97943
97994
  };
97944
97995
  function validatePresentMetadata(meta) {
97945
- const stringFields = ["sourceRun", "beforeHash", "backupContent"];
97996
+ const stringFields = ["sourceRun", "beforeHash", "beforeHashNormalized", "backupContent"];
97946
97997
  for (const field of stringFields) {
97947
97998
  if (Object.hasOwn(meta, field) && typeof meta[field] !== "string")
97948
97999
  invalidPresentField(field);
@@ -98022,6 +98073,7 @@ function proposalRowToProposal(row) {
98022
98073
  changes,
98023
98074
  ...proposedTarget !== undefined ? { proposedTarget } : {},
98024
98075
  ...typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {},
98076
+ ...typeof meta.beforeHashNormalized === "string" ? { beforeHashNormalized: meta.beforeHashNormalized } : {},
98025
98077
  ...meta.review !== undefined ? { review: meta.review } : {},
98026
98078
  ...typeof meta.confidence === "number" ? { confidence: meta.confidence } : {},
98027
98079
  ...meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {},
@@ -98048,6 +98100,8 @@ function proposalToRowValues(proposal, stashDir) {
98048
98100
  metaObj.proposedTarget = currentProposalTarget(proposal.proposedTarget);
98049
98101
  if (proposal.beforeHash !== undefined)
98050
98102
  metaObj.beforeHash = proposal.beforeHash;
98103
+ if (proposal.beforeHashNormalized !== undefined)
98104
+ metaObj.beforeHashNormalized = proposal.beforeHashNormalized;
98051
98105
  if (proposal.sourceRun !== undefined)
98052
98106
  metaObj.sourceRun = proposal.sourceRun;
98053
98107
  if (proposal.review !== undefined)
@@ -104011,12 +104065,16 @@ async function runMigration(options) {
104011
104065
  if (apply && configExtraParams.applied)
104012
104066
  resetConfigCache();
104013
104067
  const pendingLift = apply ? undefined : configExtraParams.pending;
104068
+ const configRetiredExperimentalKeys = apply ? applyConfigRetiredExperimentalKeys(configPath) : { pending: findConfigRetiredExperimentalKeys(configPath) };
104069
+ if (apply && configRetiredExperimentalKeys.applied)
104070
+ resetConfigCache();
104014
104071
  if (pendingLift && pendingLift.lifted.length > 0) {
104015
104072
  return {
104016
104073
  schemaVersion: 1,
104017
104074
  status: "blocked",
104018
104075
  blockers: pendingLift.lifted,
104019
104076
  configExtraParams,
104077
+ configRetiredExperimentalKeys,
104020
104078
  stateMigrations: { pending: listPendingStateMigrations() }
104021
104079
  };
104022
104080
  }
@@ -104031,6 +104089,7 @@ async function runMigration(options) {
104031
104089
  blockers: pendingSchedulerBindings.changes.map((change) => `${change.kind === "bind" ? "bind" : "drop"} scheduler activation ${change.ref}` + (change.reason ? `: ${change.reason}` : "")),
104032
104090
  configExtraParams,
104033
104091
  configSchedulerSourceIds,
104092
+ configRetiredExperimentalKeys,
104034
104093
  stateMigrations: { pending: listPendingStateMigrations() }
104035
104094
  };
104036
104095
  }
@@ -104050,12 +104109,14 @@ async function runMigration(options) {
104050
104109
  }
104051
104110
  const stateStatus = "pending" in stateMigrations && stateMigrations.pending.length > 0 ? "ready" : "current";
104052
104111
  const schedulerStatus = "pending" in schedulerActivation && schedulerActivation.pending.length > 0 ? "ready" : "current";
104112
+ const retiredKeysStatus = "pending" in configRetiredExperimentalKeys && configRetiredExperimentalKeys.pending.removed.length > 0 ? "ready" : "current";
104053
104113
  return {
104054
104114
  schemaVersion: 1,
104055
- status: worstStatus(worstStatus(worstStatus(taskV3.status, taskV4.status), stateStatus), schedulerStatus),
104115
+ status: worstStatus(worstStatus(worstStatus(worstStatus(taskV3.status, taskV4.status), stateStatus), schedulerStatus), retiredKeysStatus),
104056
104116
  blockers: [...taskV3.blockers, ...taskV4.blockers],
104057
104117
  configExtraParams,
104058
104118
  configSchedulerSourceIds,
104119
+ configRetiredExperimentalKeys,
104059
104120
  stateMigrations,
104060
104121
  schedulerActivation,
104061
104122
  taskV3Migration: taskV3.taskV3Migration,
@@ -684,6 +684,22 @@ export function getIndexedFilePaths(db) {
684
684
  .all();
685
685
  return new Set(rows.map((r) => r.file_path));
686
686
  }
687
+ /**
688
+ * `entries.file_path -> content_hash` for every currently-indexed file that
689
+ * carries a stored hash. Used by per-file staleness detection
690
+ * (`hasNewerIndexableFiles`, R6) to tell "edited after the last build" apart
691
+ * from "already re-indexed with this exact content after the last build" —
692
+ * `indexWrittenAssets` upserts a fresh row (and its `content_hash`) without
693
+ * bumping `builtAt`, so mtime alone cannot distinguish the two cases.
694
+ */
695
+ export function getIndexedFileHashes(db) {
696
+ const rows = db
697
+ .prepare(`SELECT file_path, content_hash FROM entries
698
+ WHERE file_path IS NOT NULL AND file_path <> ''
699
+ AND content_hash IS NOT NULL`)
700
+ .all();
701
+ return new Map(rows.map((r) => [r.file_path, r.content_hash]));
702
+ }
687
703
  /**
688
704
  * Resolve a single `entries.file_path` by primary key, or `undefined` if no
689
705
  * row matches.
@@ -732,6 +748,33 @@ export function getEntryByRef(db, ref) {
732
748
  const id = findEntryIdByRef(db, ref);
733
749
  return id === undefined ? null : { id };
734
750
  }
751
+ /** Build a {@link LiveRefSnapshot} from the current `entries` table. */
752
+ export function getLiveRefSnapshot(db) {
753
+ const rows = db.prepare("SELECT item_ref FROM entries").all();
754
+ const itemRefs = new Set();
755
+ const conceptIds = new Set();
756
+ for (const { item_ref } of rows) {
757
+ itemRefs.add(item_ref);
758
+ const boundary = item_ref.indexOf("//");
759
+ if (boundary >= 0)
760
+ conceptIds.add(item_ref.slice(boundary + 2));
761
+ }
762
+ return { itemRefs, conceptIds };
763
+ }
764
+ /**
765
+ * `getEntryByRef`'s liveness check (bundle-qualified exact match, or bare
766
+ * `//conceptId` suffix match across bundles, each tried with the `.md`
767
+ * toggle {@link withMdVariants} applies) against a prebuilt
768
+ * {@link LiveRefSnapshot} instead of the database — no query per call.
769
+ */
770
+ export function isRefLiveInSnapshot(snapshot, ref) {
771
+ const parsed = parseBundleRef(ref);
772
+ const conceptVariants = withMdVariants(parsed.conceptId);
773
+ if (parsed.bundle !== undefined) {
774
+ return conceptVariants.some((conceptId) => snapshot.itemRefs.has(`${parsed.bundle}//${conceptId}`));
775
+ }
776
+ return conceptVariants.some((conceptId) => snapshot.conceptIds.has(conceptId));
777
+ }
735
778
  /**
736
779
  * The fully-qualified `item_ref` (`<bundle>//<conceptId>`, the durable stored
737
780
  * spelling — spec §11.1 D-R3) for an entry `id`, or `null` when the row is gone
@@ -107,7 +107,7 @@ const GATE_OUTCOMES = {
107
107
  "auto-rejected": true,
108
108
  };
109
109
  function validatePresentMetadata(meta) {
110
- const stringFields = ["sourceRun", "beforeHash", "backupContent"];
110
+ const stringFields = ["sourceRun", "beforeHash", "beforeHashNormalized", "backupContent"];
111
111
  for (const field of stringFields) {
112
112
  if (Object.hasOwn(meta, field) && typeof meta[field] !== "string")
113
113
  invalidPresentField(field);
@@ -224,6 +224,7 @@ export function proposalRowToProposal(row) {
224
224
  changes,
225
225
  ...(proposedTarget !== undefined ? { proposedTarget } : {}),
226
226
  ...(typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {}),
227
+ ...(typeof meta.beforeHashNormalized === "string" ? { beforeHashNormalized: meta.beforeHashNormalized } : {}),
227
228
  ...(meta.review !== undefined ? { review: meta.review } : {}),
228
229
  ...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
229
230
  ...(meta.gateDecision !== undefined ? { gateDecision: meta.gateDecision } : {}),
@@ -274,6 +275,8 @@ export function proposalToRowValues(proposal, stashDir) {
274
275
  metaObj.proposedTarget = currentProposalTarget(proposal.proposedTarget);
275
276
  if (proposal.beforeHash !== undefined)
276
277
  metaObj.beforeHash = proposal.beforeHash;
278
+ if (proposal.beforeHashNormalized !== undefined)
279
+ metaObj.beforeHashNormalized = proposal.beforeHashNormalized;
277
280
  if (proposal.sourceRun !== undefined)
278
281
  metaObj.sourceRun = proposal.sourceRun;
279
282
  if (proposal.review !== undefined)
@@ -0,0 +1,123 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * state.db integrity + reclaimable-space probes (R0, tier0-0917).
6
+ *
7
+ * `akm health`'s `state-db-integrity` check (src/commands/health/checks.ts)
8
+ * is a pure projection like every other check, so the actual IO lives here:
9
+ * a read-only `PRAGMA quick_check` and a read-only freelist/page-count read.
10
+ * Both open their own short-lived connection via the plain {@link openDatabase}
11
+ * opener — deliberately bypassing `openStateDatabase`'s managed-open/migration
12
+ * machinery (src/core/state-db.ts), since a corrupt database must not need a
13
+ * clean migration-ledger read just to report itself as corrupt.
14
+ *
15
+ * {@link vacuumStateDbIfReclaimable} is the post-purge VACUUM step: given an
16
+ * already-open read-write connection (VACUUM cannot run inside a transaction,
17
+ * and a read-only handle cannot run it at all) and a freelist reading, it
18
+ * VACUUMs only when the freelist ratio crosses {@link STATE_DB_FREELIST_WARN_RATIO}
19
+ * and never throws — a locked/busy database is reported, not raised.
20
+ *
21
+ * @module storage/state-db-integrity
22
+ */
23
+ import { appendEvent } from "../core/events.js";
24
+ import { openDatabase } from "./database.js";
25
+ import { SQLITE_BUSY_TIMEOUT_MS } from "./sqlite-pragmas.js";
26
+ /** How many corruption errors `PRAGMA quick_check` collects before it stops scanning and returns. */
27
+ const QUICK_CHECK_ERROR_LIMIT = 10;
28
+ /** Above this fraction of free pages, `state-db-integrity` warns and a post-purge pass VACUUMs. */
29
+ export const STATE_DB_FREELIST_WARN_RATIO = 0.5;
30
+ /** Event appended by {@link vacuumStateDbIfReclaimable} after a successful VACUUM. */
31
+ export const STATE_DB_VACUUMED_EVENT = "state_db_vacuumed";
32
+ function firstColumn(row) {
33
+ return row === undefined ? undefined : Object.values(row)[0];
34
+ }
35
+ function openReadonlyStateDb(dbPath) {
36
+ const db = openDatabase(dbPath, { readonly: true, create: false });
37
+ // Read-only handles cannot run journal_mode/foreign_keys (write operations),
38
+ // but busy_timeout is legal — see openReadonlyExistingDatabase's identical
39
+ // rationale in src/storage/repositories/index-connection.ts.
40
+ db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
41
+ return db;
42
+ }
43
+ /**
44
+ * Run `PRAGMA quick_check(N)` against `dbPath` read-only. Sub-second on a
45
+ * healthy multi-hundred-MB file; on a corrupt one, `N` bounds how many errors
46
+ * SQLite collects before it stops scanning, which keeps the check's runtime
47
+ * bounded even against a badly corrupt file.
48
+ */
49
+ export function runStateDbQuickCheck(dbPath) {
50
+ let db;
51
+ try {
52
+ db = openReadonlyStateDb(dbPath);
53
+ const rows = db.prepare(`PRAGMA quick_check(${QUICK_CHECK_ERROR_LIMIT})`).all();
54
+ const lines = rows.map((row) => String(firstColumn(row)));
55
+ const ok = lines.length === 1 && lines[0] === "ok";
56
+ return { ok, lines };
57
+ }
58
+ catch (err) {
59
+ return { ok: false, lines: [], error: err instanceof Error ? err.message : String(err) };
60
+ }
61
+ finally {
62
+ db?.close();
63
+ }
64
+ }
65
+ /**
66
+ * Read `PRAGMA freelist_count` / `PRAGMA page_count` off an already-open
67
+ * connection. Shared by {@link getStateDbFreelistInfo} (which opens its own
68
+ * read-only handle) and the post-purge VACUUM call site, which must read the
69
+ * freelist off the same read-write connection the purge just used rather
70
+ * than open a second one.
71
+ */
72
+ export function readFreelistInfo(db) {
73
+ const freelistCount = Number(firstColumn(db.prepare("PRAGMA freelist_count").get()) ?? 0);
74
+ const pageCount = Number(firstColumn(db.prepare("PRAGMA page_count").get()) ?? 0);
75
+ return { freelistCount, pageCount, ratio: pageCount > 0 ? freelistCount / pageCount : 0 };
76
+ }
77
+ /** Read `PRAGMA freelist_count` / `PRAGMA page_count` — how much of state.db is reclaimable by VACUUM. */
78
+ export function getStateDbFreelistInfo(dbPath) {
79
+ let db;
80
+ try {
81
+ db = openReadonlyStateDb(dbPath);
82
+ return readFreelistInfo(db);
83
+ }
84
+ catch (err) {
85
+ return { freelistCount: 0, pageCount: 0, ratio: 0, error: err instanceof Error ? err.message : String(err) };
86
+ }
87
+ finally {
88
+ db?.close();
89
+ }
90
+ }
91
+ /**
92
+ * VACUUM `db` when `freelist.ratio` exceeds {@link STATE_DB_FREELIST_WARN_RATIO},
93
+ * appending a {@link STATE_DB_VACUUMED_EVENT} recording pages before/after.
94
+ * Intended to run immediately after the retention purge, on the same
95
+ * read-write connection the purge just used. Never throws: a locked/busy
96
+ * database (another writer holds the file right now) is reported via
97
+ * `reason: "busy"` rather than raised, since this is opportunistic
98
+ * maintenance and must not fail the purge pass it follows.
99
+ *
100
+ * The event is appended via `appendEvent` (not a direct `insertEvent` on
101
+ * `db`) so it honors the caller's `EventsContext` — `readOnly` suppresses
102
+ * the write and an injected `now` is used for `ts` — the same as every
103
+ * other event `runRetentionPurgePass` appends in this callback.
104
+ */
105
+ export function vacuumStateDbIfReclaimable(db, freelist, eventsCtx) {
106
+ if (freelist.ratio <= STATE_DB_FREELIST_WARN_RATIO) {
107
+ return { ran: false, reason: "below-threshold", pagesBefore: freelist.pageCount };
108
+ }
109
+ try {
110
+ db.exec("VACUUM");
111
+ }
112
+ catch (err) {
113
+ const message = err instanceof Error ? err.message : String(err);
114
+ const busy = /busy|locked/i.test(message);
115
+ return { ran: false, reason: busy ? "busy" : "error", pagesBefore: freelist.pageCount, error: message };
116
+ }
117
+ const pagesAfter = Number(firstColumn(db.prepare("PRAGMA page_count").get()) ?? 0);
118
+ appendEvent({
119
+ eventType: STATE_DB_VACUUMED_EVENT,
120
+ metadata: { pagesBefore: freelist.pageCount, pagesAfter, freelistRatioBefore: freelist.ratio },
121
+ }, eventsCtx);
122
+ return { ran: true, pagesBefore: freelist.pageCount, pagesAfter };
123
+ }
@@ -25,6 +25,7 @@ const RETRY_REASON_SET = {
25
25
  content_policy_reject: true,
26
26
  unsupported_type: true,
27
27
  no_change: true,
28
+ quality_rejected: true,
28
29
  aborted: true,
29
30
  };
30
31
  export const PROGRAM_RETRY_REASONS = Object.keys(RETRY_REASON_SET);
@@ -387,7 +387,7 @@ Primary result fields:
387
387
  | Field | Description |
388
388
  | --- | --- |
389
389
  | `status` | Overall health verdict: `pass`, `warn`, or `fail` |
390
- | `hardChecks` | Deterministic checks such as `state-db-schema`, `state-db-round-trip`, `state-db-migrations`, `task-log-backing`, `active-runs`, `default-engine`, `model-map-files`, `default-llm-engine`, `configured-engines`, and `active-improve-strategy` |
390
+ | `hardChecks` | Deterministic checks such as `state-db-schema`, `state-db-round-trip`, `state-db-integrity`, `state-db-migrations`, `task-log-backing`, `active-runs`, `default-engine`, `model-map-files`, `default-llm-engine`, `configured-engines`, and `active-improve-strategy` |
391
391
  | `advisories` | Non-fatal warnings including `semantic-search-runtime`, `session-extraction` (akmExtract pipeline health), `cli-version` (installed vs latest release), `thinking-control` (an `enableThinking: false` engine whose recorded usage still shows reasoning tokens), and `engine-last-used` (an engine bound to an enabled improve process with no recorded use in 30 days) |
392
392
  | `metrics` | Aggregate task/runtime metrics: `taskFailRate`, `agentFailureRate`, `stuckActiveRuns`, `logBackingRate`, `probeRoundTripMs` |
393
393
  | `improve` | Recent improve-loop counts derived from `improve_invoked`, `improve_skipped`, and `improve_completed` events |
@@ -1673,16 +1673,25 @@ in order:
1673
1673
 
1674
1674
  1. legacy config `extraParams` keys lifted onto first-class engine fields
1675
1675
  (`configExtraParams`);
1676
- 2. pending `state.db` migrations, historical-destructive ones included, with
1676
+ 2. retired `experimental.*` config keys removed, today `workflowEngine`
1677
+ (`configRetiredExperimentalKeys`) — config loading already ignores them
1678
+ with a one-time warning, so this only cleans the file;
1679
+ 3. scheduler grants bound to the configured source installation that was
1680
+ approved, with stale grants for removed bundles dropped
1681
+ (`configSchedulerSourceIds`);
1682
+ 4. pending `state.db` migrations, historical-destructive ones included, with
1677
1683
  a verified sibling safety copy (`stateMigrations`) — the only path besides
1678
1684
  `akm upgrade` that admits released migration 018, which an ordinary
1679
1685
  command refuses;
1680
- 3. task-v2 files to task v3, then task-v3 files to task source v4
1686
+ 5. source-owned schedule enablement converted to host-local scheduler grants
1687
+ (`schedulerActivation`);
1688
+ 6. task-v2 files to task v3, then task-v3 files to task source v4
1681
1689
  (`taskV3Migration`, `taskV4Migration`), each keeping its own lock, backup,
1682
1690
  prevalidation, and rollback, so a file blocked in the first generation does
1683
1691
  not stop the second from converting files already at `version: 3`;
1684
- 4. superseded pre-0.9.0 `.akm` residue and stale filesystem transactions
1685
- (`deadResidue`, `staleTxns`).
1692
+ 7. superseded pre-0.9.0 `.akm` residue and stale filesystem transactions
1693
+ (`deadResidue`, `staleTxns`), then live `.akm` writers relocated to
1694
+ `$STATE`/`$CACHE` (`writerRelocation`).
1686
1695
 
1687
1696
  ```sh
1688
1697
  akm migrate status
@@ -2459,8 +2468,9 @@ configures a judgment engine. Each row carries `enabled`, the resolved
2459
2468
  `engine`/`model` (llm-backed processes only) and `engineKind`, this process's
2460
2469
  own lowering `notices`, and — for reflect/distill/consolidate only —
2461
2470
  `eligibleRefs`, the count of this run's `effectiveRefs` the process would act
2462
- on (`shouldSkipRef`'s allowedTypes/process-disabled check; a count, not a
2463
- per-ref matrix, to keep the envelope bounded). A row that could not resolve an
2471
+ on (`shouldSkipRef`'s allowedTypes/excludeRefPrefixes (reflect only)/
2472
+ process-disabled check; a count, not a per-ref matrix, to keep the envelope
2473
+ bounded). A row that could not resolve an
2464
2474
  engine or credential carries `unavailable: {configKey, reason}` — the same
2465
2475
  data behind `skippedProcesses` above, reshaped per process. When the process
2466
2476
  resolved a real engine whose credential just isn't reachable here, the row
@@ -208,6 +208,7 @@ the set of types the code actually emits at HEAD (verified against every
208
208
  | `events_purged` | Old events deleted by improve maintenance (90-day default retention) | `purgedCount`, `retentionDays` |
209
209
  | `improve_runs_purged` | Old `improve_runs` rows deleted by improve maintenance (same retention window as events) | `purgedCount`, `retentionDays` |
210
210
  | `improve_cycle_metrics_purged` | Old `improve_cycle_metrics` rows (365-day retention) deleted by improve maintenance | `purgedCount`, `retentionDays` |
211
+ | `state_db_vacuumed` | state.db was VACUUMed after the retention purge because more than half its pages were free | `pagesBefore`, `pagesAfter`, `freelistRatioBefore` |
211
212
  | `task_logs_purged` | Old scheduled-task log files purged by improve maintenance | |
212
213
 
213
214
  *Workflows*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.16",
3
+ "version": "0.9.17-alpha.2",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
6
6
  "keywords": [
@@ -796,6 +796,13 @@
796
796
  "minLength": 1
797
797
  }
798
798
  },
799
+ "excludeRefPrefixes": {
800
+ "type": "array",
801
+ "items": {
802
+ "type": "string",
803
+ "minLength": 1
804
+ }
805
+ },
799
806
  "limit": {
800
807
  "type": "integer",
801
808
  "exclusiveMinimum": 0
@@ -1207,6 +1214,10 @@
1207
1214
  },
1208
1215
  "fullScan": {
1209
1216
  "type": "boolean"
1217
+ },
1218
+ "maxChunksPerAsset": {
1219
+ "type": "integer",
1220
+ "exclusiveMinimum": 0
1210
1221
  }
1211
1222
  },
1212
1223
  "additionalProperties": true
@@ -2578,6 +2589,13 @@
2578
2589
  "minLength": 1
2579
2590
  }
2580
2591
  },
2592
+ "excludeRefPrefixes": {
2593
+ "type": "array",
2594
+ "items": {
2595
+ "type": "string",
2596
+ "minLength": 1
2597
+ }
2598
+ },
2581
2599
  "limit": {
2582
2600
  "type": "integer",
2583
2601
  "exclusiveMinimum": 0
@@ -2989,6 +3007,10 @@
2989
3007
  },
2990
3008
  "fullScan": {
2991
3009
  "type": "boolean"
3010
+ },
3011
+ "maxChunksPerAsset": {
3012
+ "type": "integer",
3013
+ "exclusiveMinimum": 0
2992
3014
  }
2993
3015
  },
2994
3016
  "additionalProperties": true
@@ -3755,6 +3777,13 @@
3755
3777
  "minLength": 1
3756
3778
  }
3757
3779
  },
3780
+ "excludeRefPrefixes": {
3781
+ "type": "array",
3782
+ "items": {
3783
+ "type": "string",
3784
+ "minLength": 1
3785
+ }
3786
+ },
3758
3787
  "limit": {
3759
3788
  "type": "integer",
3760
3789
  "exclusiveMinimum": 0
@@ -3883,6 +3912,10 @@
3883
3912
  "fullScan": {
3884
3913
  "type": "boolean"
3885
3914
  },
3915
+ "maxChunksPerAsset": {
3916
+ "type": "integer",
3917
+ "exclusiveMinimum": 0
3918
+ },
3886
3919
  "defaultSince": {
3887
3920
  "type": "string",
3888
3921
  "minLength": 1
@@ -4148,6 +4181,13 @@
4148
4181
  "minLength": 1
4149
4182
  }
4150
4183
  },
4184
+ "excludeRefPrefixes": {
4185
+ "type": "array",
4186
+ "items": {
4187
+ "type": "string",
4188
+ "minLength": 1
4189
+ }
4190
+ },
4151
4191
  "limit": {
4152
4192
  "type": "integer",
4153
4193
  "exclusiveMinimum": 0
@@ -4559,6 +4599,10 @@
4559
4599
  },
4560
4600
  "fullScan": {
4561
4601
  "type": "boolean"
4602
+ },
4603
+ "maxChunksPerAsset": {
4604
+ "type": "integer",
4605
+ "exclusiveMinimum": 0
4562
4606
  }
4563
4607
  },
4564
4608
  "additionalProperties": true