akm-cli 0.9.0-beta.49 → 0.9.0-beta.50

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.
@@ -1,15 +1,20 @@
1
1
  {
2
- "description": "Standard improve pass — all sub-processes, markdown asset types.",
2
+ "description": "Standard improve pass — all sub-processes + sustaining proactive lane.",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
6
+ "limit": 25,
6
7
  "allowedTypes": ["agent", "command", "knowledge", "lesson", "memory", "skill", "wiki", "workflow"]
7
8
  },
8
- "distill": { "enabled": true, "allowedTypes": ["memory"] },
9
- "consolidate": { "enabled": true, "allowedTypes": ["memory"] },
9
+ "distill": { "enabled": true, "allowedTypes": ["memory"], "requirePlannedRefs": true },
10
+ "consolidate": { "enabled": true, "allowedTypes": ["memory"], "minPoolSize": 500 },
10
11
  "memoryInference": { "enabled": true },
11
12
  "graphExtraction": { "enabled": true },
12
- "triage": { "enabled": false, "applyMode": "queue", "policy": "personal-stash" }
13
+ "extract": { "enabled": true, "triage": { "enabled": true, "minScore": 2 } },
14
+ "proactiveMaintenance": { "enabled": true, "dueDays": 30, "maxPerRun": 15 },
15
+ "triage": { "enabled": false, "applyMode": "queue", "policy": "personal-stash" },
16
+ "recombine": { "enabled": false },
17
+ "procedural": { "enabled": false }
13
18
  },
14
19
  "sync": { "enabled": true, "push": true }
15
20
  }
@@ -6,7 +6,7 @@
6
6
  "consolidate": { "enabled": false },
7
7
  "memoryInference": { "enabled": true },
8
8
  "graphExtraction": { "enabled": true },
9
- "extract": { "enabled": true, "minNewSessions": 3 },
9
+ "extract": { "enabled": true, "minNewSessions": 3, "triage": { "enabled": true, "minScore": 2 } },
10
10
  "triage": { "enabled": false }
11
11
  },
12
12
  "sync": { "enabled": true, "push": true }
@@ -8,5 +8,5 @@
8
8
  "graphExtraction": { "enabled": false },
9
9
  "triage": { "enabled": false }
10
10
  },
11
- "sync": { "enabled": false }
11
+ "sync": { "enabled": true, "push": true }
12
12
  }
@@ -12,5 +12,5 @@
12
12
  "graphExtraction": { "enabled": false },
13
13
  "triage": { "enabled": false }
14
14
  },
15
- "sync": { "enabled": false }
15
+ "sync": { "enabled": true, "push": true }
16
16
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "description": "Synthesis-only — cross-episodic recombination (#609). Opt-in periodic pass; all generative/extract passes off. (Procedural compilation (#615) is held OFF until cross-project scoping lands — it over-fits one-off sequences, 0% accept.)",
3
+ "processes": {
4
+ "reflect": { "enabled": false },
5
+ "distill": { "enabled": false },
6
+ "consolidate": { "enabled": false },
7
+ "memoryInference": { "enabled": false },
8
+ "graphExtraction": { "enabled": false },
9
+ "extract": { "enabled": false },
10
+ "triage": { "enabled": false },
11
+ "recombine": { "enabled": true },
12
+ "procedural": { "enabled": false }
13
+ },
14
+ "sync": { "enabled": true, "push": true }
15
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "All sub-processes enabled (currently identical to default; reserved for future divergence).",
2
+ "description": "Like default, plus the triage process (drains the pending-proposal backlog).",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
@@ -2,7 +2,7 @@ Extract entities and relations from the asset body below.
2
2
 
3
3
  Rules:
4
4
  - Output ONLY a JSON object: {"entities": ["Entity One", ...], "relations": [{"from": "A", "to": "B", "type": "uses"}, ...]}.
5
- - Entities are short, canonical noun phrases (project names, services, tools, people, file/dir names, technical concepts).
5
+ - Entities are short, canonical noun phrases (project names, services, tools, people, technical concepts). Do NOT emit file or directory paths (anything containing "/" or "\") — they are dropped downstream.
6
6
  - Relations connect two entities that both appear in the entities array.
7
7
  - "type" is a short verb phrase (e.g. "uses", "depends on", "owns", "documents"). Optional; omit when unsure.
8
8
  - Drop pleasantries, meta-commentary, and timestamps.
@@ -9,6 +9,23 @@ import { hasHotCaptureMode } from "../../proposal/validators/proposal-quality-va
9
9
  export function isConsolidationEligibleMemoryName(name) {
10
10
  return !name.endsWith(".derived");
11
11
  }
12
+ /**
13
+ * #632 — AKM session-capture telemetry memories: auto-generated session-end
14
+ * checkpoints named `<harness>-session-<YYYYMMDD>-<id>` or
15
+ * `<harness>-checkpoint-<YYYYMMDD…>-<id>`, carrying an embedded
16
+ * `akm_memory_kind: session_checkpoint` metadata block. Their bodies are
17
+ * pipeline bookkeeping, not durable knowledge, so the improve passes exclude
18
+ * them from their pools (recombine, consolidate). The `\d{8}` datestamp anchor
19
+ * is what distinguishes a capture name from a durable memory that merely
20
+ * MENTIONS session/checkpoint (e.g. `akm-plugins-session-end-extract-hook`,
21
+ * `session-checkpoint-lint-skips`), which stay in the pool.
22
+ *
23
+ * Lives here (the eligibility module) rather than in recombine so both
24
+ * recombine and consolidate can reuse it without a circular import.
25
+ */
26
+ export function isSessionCaptureMemoryName(name) {
27
+ return /-(session|checkpoint)-\d{8}/.test(name);
28
+ }
12
29
  /**
13
30
  * Returns true when the memory file has `captureMode: hot` in its frontmatter.
14
31
  *
@@ -41,8 +41,8 @@ import { normalizeUpdatedField, sanitizeMergedContent } from "./consolidate/sani
41
41
  export { normalizeUpdatedField, sanitizeMergedContent, stripOuterCodeFence } from "./consolidate/sanitize.js";
42
42
  // Eligibility / safety predicates live in ./consolidate/eligibility. Imported
43
43
  // for internal guard use; the two public predicates are re-exported.
44
- import { consolidateGuardStatus, isConsolidationEligibleMemoryName, isHotCapturedMemory, } from "./consolidate/eligibility.js";
45
- export { isConsolidationEligibleMemoryName, isHotCapturedMemory } from "./consolidate/eligibility.js";
44
+ import { consolidateGuardStatus, isConsolidationEligibleMemoryName, isHotCapturedMemory, isSessionCaptureMemoryName, } from "./consolidate/eligibility.js";
45
+ export { isConsolidationEligibleMemoryName, isHotCapturedMemory, isSessionCaptureMemoryName, } from "./consolidate/eligibility.js";
46
46
  // Plan parsing / merging (pure op-reconciliation algebra) lives in
47
47
  // ./consolidate/merge. Imported for internal use; mergePlans re-exported.
48
48
  import { isValidOp, mergePlans } from "./consolidate/merge.js";
@@ -2036,6 +2036,10 @@ function loadMemoriesForSource(source, stashDir, warnings) {
2036
2036
  return path.resolve(e.stashDir) === path.resolve(source);
2037
2037
  })
2038
2038
  .filter((e) => isConsolidationEligibleMemoryName(e.entry.name))
2039
+ // #632 — exclude session-capture telemetry (checkpoints) from the
2040
+ // consolidation pool, mirroring recombine. Their bodies are pipeline
2041
+ // bookkeeping, so consolidating them burns LLM calls on pure noise.
2042
+ .filter((e) => !isSessionCaptureMemoryName(e.entry.name))
2039
2043
  // Skip stale DB entries whose file was deleted by a prior run but not yet
2040
2044
  // re-indexed. Without this guard the deleted file's ref appears in chunks
2041
2045
  // sent to the LLM, which then proposes a second delete → delete_failed
@@ -2069,6 +2073,8 @@ function loadMemoriesForSource(source, stashDir, warnings) {
2069
2073
  const name = fname.replace(/\.md$/, "");
2070
2074
  if (!isConsolidationEligibleMemoryName(name))
2071
2075
  continue;
2076
+ if (isSessionCaptureMemoryName(name))
2077
+ continue; // #632 — skip telemetry checkpoints
2072
2078
  memories.push({ name, filePath, description: "", tags: [], stashDir: fsStashDir });
2073
2079
  }
2074
2080
  }
@@ -8,6 +8,7 @@ import profileFrequent from "../../assets/profiles/frequent.json" with { type: "
8
8
  import profileGraphRefresh from "../../assets/profiles/graph-refresh.json" with { type: "json" };
9
9
  import profileMemoryFocus from "../../assets/profiles/memory-focus.json" with { type: "json" };
10
10
  import profileQuick from "../../assets/profiles/quick.json" with { type: "json" };
11
+ import profileSynthesize from "../../assets/profiles/synthesize.json" with { type: "json" };
11
12
  import profileThorough from "../../assets/profiles/thorough.json" with { type: "json" };
12
13
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
13
14
  import { warn } from "../../core/warn.js";
@@ -31,6 +32,7 @@ const BUILTIN_PROFILES = {
31
32
  frequent: profileFrequent,
32
33
  consolidate: profileConsolidate,
33
34
  catchup: profileCatchup,
35
+ synthesize: profileSynthesize,
34
36
  };
35
37
  /**
36
38
  * Default enabled-state for known improve processes when neither the user
@@ -869,7 +869,7 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
869
869
  // Coarse audit buckets, derived from the SAME classifyImproveAction the
870
870
  // persisted metrics_json uses (state-db.ts#computeImproveRunMetrics) so the
871
871
  // emitted event and the stored row can never disagree.
872
- const classCounts = { accepted: 0, rejected: 0, error: 0, noop: 0 };
872
+ const classCounts = { accepted: 0, rejected: 0, skipped: 0, error: 0, noop: 0 };
873
873
  for (const action of result.actions ?? []) {
874
874
  classCounts[classifyImproveAction(action.mode)] += 1;
875
875
  // Per-variant counters for the event metadata. The default arm makes any
@@ -936,6 +936,7 @@ function emitImproveCompletedEvent(result, durations, eventsCtx) {
936
936
  reflectGuardRejectedActions: actionCounts.reflectGuardRejected,
937
937
  acceptedActions: classCounts.accepted,
938
938
  rejectedActions: classCounts.rejected,
939
+ skippedActions: classCounts.skipped,
939
940
  noopActions: classCounts.noop,
940
941
  reflectsWithErrorContext: result.reflectsWithErrorContext ?? 0,
941
942
  coverageGapCount: result.coverageGaps?.length ?? 0,
@@ -214,6 +214,10 @@ export async function runImproveLoopStage(args) {
214
214
  ...(reflectErrors.length > 0 ? { avoidPatterns: [...reflectErrors] } : {}),
215
215
  agentProcess: options.agentProcess ?? "reflect",
216
216
  eventSource: "improve",
217
+ // #639 — resolve the low-value filter from the ACTIVE improve profile
218
+ // (default off when unset), so the running profile decides instead of
219
+ // a hardcoded profiles.improve.default path.
220
+ lowValueFilter: improveProfile.processes?.reflect?.lowValueFilter?.enabled === true,
217
221
  ...(reflectBudgetMs > 0 ? { timeoutMs: reflectBudgetMs } : {}),
218
222
  ...(reflectProfileRunner ? { runner: reflectProfileRunner } : {}),
219
223
  // Attribution: carry the eligibility lane so reflect stamps it on
@@ -19,7 +19,7 @@ import { akmLint } from "../lint/index.js";
19
19
  import { getProposal, listProposals } from "../proposal/validators/proposals.js";
20
20
  import { runSchemaRepairPass } from "../sources/schema-repair.js";
21
21
  import { computeThresholdAutoTune, gateDecisionsToSamples, summarizeCalibration, } from "./calibration.js";
22
- import { akmConsolidate } from "./consolidate.js";
22
+ import { akmConsolidate, isSessionCaptureMemoryName } from "./consolidate.js";
23
23
  // Eligibility / candidate-selection predicates live in ./eligibility.
24
24
  import { buildLatestFeedbackTsMap, buildLatestProposalTsMap, buildUtilityMap, dedupeRefs, findAssetFilePath, isDistillCandidateRef, isLessonCandidate, isSignalDeltaEligible, } from "./eligibility.js";
25
25
  import { akmExtract, countNewExtractCandidates } from "./extract.js";
@@ -1139,7 +1139,10 @@ export async function runImprovePreparationStage(args) {
1139
1139
  withStateDb((dbForHighSalience) => {
1140
1140
  const effectiveLimit = options.limit ?? 10;
1141
1141
  const highSalienceCap = Math.max(1, Math.floor(effectiveLimit * 0.1));
1142
- const candidates = noFeedbackCandidates.filter((r) => !proactiveAndRetrievalSet.has(r.ref));
1142
+ // #632/#4 session-capture telemetry (checkpoints) must never consume
1143
+ // the scarce high-salience budget. Even with a content-scored row, these
1144
+ // are pipeline bookkeeping, not assets worth reflecting/rewriting.
1145
+ const candidates = noFeedbackCandidates.filter((r) => !proactiveAndRetrievalSet.has(r.ref) && !isSessionCaptureMemoryName(parseAssetRef(r.ref).name));
1143
1146
  for (const r of candidates) {
1144
1147
  if (highSalienceRefs.length >= highSalienceCap)
1145
1148
  break;
@@ -1465,7 +1468,7 @@ export async function runImprovePreparationStage(args) {
1465
1468
  // stash-wide), but it is the most faithful comparison possible at cutover and
1466
1469
  // allows the top-200→below-500 forgetting guard to fire if the formula change
1467
1470
  // dramatically reorders the candidate pool.
1468
- // See docs/design/improve-reconciliation-plan.md §WS-1 step 7 — the stash-wide
1471
+ // See docs/archive/improve-reconciliation-plan.md §WS-1 step 7 — the stash-wide
1469
1472
  // ordering was unreconstructable (no prior state.db snapshot), so this candidate-
1470
1473
  // pool partial reconstruction is the documented resolution for the first-run case.
1471
1474
  // Emit `improve_salience_first_run` to mark the cutover moment and include the
@@ -1507,7 +1510,7 @@ export async function runImprovePreparationStage(args) {
1507
1510
  // Limitation: this covers only the current-run candidate pool, not the full
1508
1511
  // stash. The stash-wide ordering was never persisted (asset_salience is a new
1509
1512
  // WS-1 table), so this is the most faithful comparison possible at cutover.
1510
- // See docs/design/improve-reconciliation-plan.md §WS-1 step 7.
1513
+ // See docs/archive/improve-reconciliation-plan.md §WS-1 step 7.
1511
1514
  const reconstructedOldScores = new Map();
1512
1515
  for (const ref of salienceMap.keys()) {
1513
1516
  const utility = utilityMap.get(ref) ?? 0;
@@ -53,7 +53,7 @@ import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integ
53
53
  import { chatCompletion } from "../../llm/client.js";
54
54
  import { validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
55
55
  import { archiveProposal, createProposal, isProposalSkipped, listProposals } from "../proposal/validators/proposals.js";
56
- import { isConsolidationEligibleMemoryName } from "./consolidate.js";
56
+ import { isConsolidationEligibleMemoryName, isSessionCaptureMemoryName } from "./consolidate.js";
57
57
  const RECOMBINE_SYSTEM_PROMPT = recombineSystemPrompt;
58
58
  const DEFAULT_MIN_CLUSTER_SIZE = 3;
59
59
  const DEFAULT_MAX_CLUSTERS_PER_RUN = 5;
@@ -174,24 +174,10 @@ const JUNK_ENTITY_NORMS = new Set([
174
174
  "metadata",
175
175
  "status",
176
176
  ]);
177
- /**
178
- * #632 AKM session-capture telemetry memories: auto-generated session-end
179
- * checkpoints named `<harness>-session-<YYYYMMDD>-<id>` or
180
- * `<harness>-checkpoint-<YYYYMMDD…>-<id>`, carrying an embedded
181
- * `akm_memory_kind: session_checkpoint` metadata block. Their bodies are
182
- * pipeline bookkeeping, so the graph extracts their metadata FIELDS
183
- * (`session_checkpoint`, `harness`, `buffered observations`, `tool_*_observed`,
184
- * …) as ENTITIES — which then dominate entity clustering as bland, stash-wide
185
- * mega-buckets (the #632 symptom, just under an `entity:` signature). They are
186
- * session telemetry, not durable knowledge to generalize, so recombine excludes
187
- * them from its pool. The `\d{8}` datestamp anchor is what distinguishes a
188
- * capture name from a durable memory that merely MENTIONS session/checkpoint
189
- * (e.g. `akm-plugins-session-end-extract-hook`, `session-checkpoint-lint-skips`),
190
- * which stay in the pool.
191
- */
192
- export function isSessionCaptureMemoryName(name) {
193
- return /-(session|checkpoint)-\d{8}/.test(name);
194
- }
177
+ // #632 — `isSessionCaptureMemoryName` now lives in ./consolidate/eligibility so
178
+ // both recombine and consolidate can reuse it without a circular import. It is
179
+ // re-exported here for back-compat (existing importers + tests).
180
+ export { isSessionCaptureMemoryName };
195
181
  /**
196
182
  * #632 — an entity carries no clustering signal (and must be skipped) when it is
197
183
  * a generic extraction artefact (session / structured-log bookkeeping), a raw
@@ -602,6 +588,34 @@ export async function akmRecombine(opts) {
602
588
  break;
603
589
  }
604
590
  clustersFormed += 1;
591
+ // #9 — promotion is terminal. If this cluster Jaccard-matches a
592
+ // hypothesis that was ALREADY promoted, generating again can at best
593
+ // re-queue a redundant `type: hypothesis` for a settled ref (promotion
594
+ // gates on !alreadyPromoted below), so the LLM output is wasted. Skip the
595
+ // per-cluster call entirely. The decay sweep still spares the row — it
596
+ // matches a present cluster via `presentClusters` (independent of
597
+ // `seenThisRun`), so the promotion record is preserved.
598
+ if (stateDb) {
599
+ const promotedMatch = findMatchingRecombineHypothesis(stateDb, {
600
+ signature: cluster.signature,
601
+ memberKey: recombineMemberKey(cluster),
602
+ minOverlap: DEFAULT_RECOMBINE_OVERLAP,
603
+ });
604
+ if (promotedMatch?.hypothesis_ref &&
605
+ getRecombineHypothesis(stateDb, promotedMatch.hypothesis_ref)?.promoted_at != null) {
606
+ appendEvent({
607
+ eventType: "recombine_invoked",
608
+ ref: promotedMatch.hypothesis_ref,
609
+ metadata: {
610
+ signal: cluster.signature,
611
+ memberCount: cluster.members.length,
612
+ outcome: "skipped_promoted",
613
+ sourceRun,
614
+ },
615
+ }, opts.ctx);
616
+ continue;
617
+ }
618
+ }
605
619
  const prompt = buildClusterPrompt(cluster, standardsContext);
606
620
  const raw = await llmFn(prompt);
607
621
  const generalization = parseGeneralization(raw);
@@ -1275,11 +1275,12 @@ export async function akmReflect(options = {}) {
1275
1275
  // (new-asset proposals have nothing to diff against).
1276
1276
  if (assetContent !== undefined) {
1277
1277
  const changeKind = classifyReflectChange(assetContent, payload.content);
1278
- // 'low-value' is config-gated (#639): only defer when
1279
- // profiles.improve.default.processes.reflect.lowValueFilter.enabled is
1280
- // explicitly true. DEFAULT OFF absent flag = byte-identical pre-#639
1281
- // behaviour (low-value treated the same as substantive).
1282
- const lowValueFilterEnabled = runtimeConfig?.profiles?.improve?.default?.processes?.reflect?.lowValueFilter?.enabled === true;
1278
+ // 'low-value' is config-gated (#639). DEFAULT OFF — absent = byte-identical
1279
+ // pre-#639 behaviour (low-value treated the same as substantive). Resolved
1280
+ // by the caller from the ACTIVE improve profile's
1281
+ // `processes.reflect.lowValueFilter.enabled` and passed via options, so the
1282
+ // running profile (not a hardcoded `profiles.improve.default`) decides.
1283
+ const lowValueFilterEnabled = options.lowValueFilter === true;
1283
1284
  const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
1284
1285
  if (isDeferred) {
1285
1286
  const subreason = changeKind === "noop"
@@ -9,7 +9,7 @@
9
9
  * the engine behind `akm proposal drain` and (later) the `triage` improve
10
10
  * pre-pass.
11
11
  *
12
- * Design (see docs/technical/proposal-triage-implementation-plan.md):
12
+ * Design (see docs/archive/proposal-triage-implementation-plan.md):
13
13
  * - Reuses `listProposals` (no source filter — generator filtering is
14
14
  * in-memory) and the `akmProposalAccept` / `akmProposalReject` wrappers from
15
15
  * `proposal.ts` so the standard `promoted` / `rejected` events are emitted.
@@ -125,6 +125,10 @@ export const AgentProfileConfigSchema = z
125
125
  args: z.array(z.string()).optional(),
126
126
  workspace: z.string().min(1).optional(),
127
127
  model: z.string().min(1).optional(),
128
+ // Per-call timeout (ms) used when a command does not pass an explicit
129
+ // timeout (e.g. `akm wiki ingest` without `--timeout-ms`). null = no
130
+ // timeout. Honored by resolveAgentProfile (integrations/agent/config.ts).
131
+ timeoutMs: z.union([positiveInt, z.null()]).optional(),
128
132
  })
129
133
  .passthrough();
130
134
  // ── Improve profile / process ──────────────────────────────────────────────
@@ -201,6 +205,23 @@ export const ImproveProcessConfigSchema = z
201
205
  // high-signal-first sweep). Unset = process all eligible (current
202
206
  // behavior). Only meaningful on `graphExtraction`.
203
207
  topN: positiveInt.optional(),
208
+ // graphExtraction process: full-corpus scan. When true, graph extraction
209
+ // runs on ALL stash files instead of only files touched by actionable refs
210
+ // in the current run. Used by the `graph-refresh` built-in profile / a
211
+ // scheduled weekly task. Only meaningful on `graphExtraction`.
212
+ fullScan: z.boolean().optional(),
213
+ // #626 — extract process: pre-LLM heuristic triage gate. When enabled, a
214
+ // deterministic scorer decides BEFORE the extraction LLM call whether a
215
+ // session carries enough signal to be worth extracting; low-signal sessions
216
+ // are skipped at zero LLM cost. Default OFF. Only meaningful on `extract`.
217
+ // `minScore` is the minimum total heuristic score to PASS (default 2).
218
+ triage: z
219
+ .object({
220
+ enabled: z.boolean().optional(),
221
+ minScore: z.number().min(0).optional(),
222
+ })
223
+ .passthrough()
224
+ .optional(),
204
225
  // MemoryInference process: minimum pending memory count to run the pass.
205
226
  minPendingCount: z.number().int().min(0).optional(),
206
227
  // Extract process: minimum number of new (unseen, in-window) candidate
@@ -377,6 +398,7 @@ const ImproveProfileProcessesSchema = z
377
398
  consolidate: ImproveProcessConfigSchema.optional(),
378
399
  memoryInference: ImproveProcessConfigSchema.optional(),
379
400
  graphExtraction: ImproveProcessConfigSchema.optional(),
401
+ extract: ImproveProcessConfigSchema.optional(),
380
402
  validation: ImproveProcessConfigSchema.optional(),
381
403
  triage: ImproveProcessConfigSchema.optional(),
382
404
  proactiveMaintenance: ImproveProcessConfigSchema.optional(),
@@ -284,9 +284,14 @@ export function saveConfig(config) {
284
284
  const dir = path.dirname(configPath);
285
285
  fs.mkdirSync(dir, { recursive: true });
286
286
  const sanitized = sanitizeConfigForWrite(config);
287
- // Final validation gate before bytes hit disk. Catches schema violations
288
- // (unknown keys in registries[] / sources[] / profiles.*; out-of-range
289
- // numbers; etc. closes #462) before we corrupt the user's config.
287
+ // Final validation gate before bytes hit disk. Runs the FULL schema
288
+ // including the cross-field superRefine guards (removed `feedbackDistillation`
289
+ // process key, `defaultWriteTarget` resolution, writable npm/website sources)
290
+ // and all type/enum/range checks — so an `akm config set` (leaf OR object
291
+ // form) cannot persist a guard-violating or mistyped value. NOTE: unknown
292
+ // keys are intentionally NOT rejected here — object schemas are `.passthrough()`
293
+ // so cross-version skew round-trips (see config-schema.ts header); the lenient
294
+ // tolerance is by design, not an oversight.
290
295
  const parseResult = AkmConfigSchema.safeParse(sanitized);
291
296
  if (!parseResult.success) {
292
297
  const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
@@ -11,16 +11,18 @@ import { assertNever } from "./assert.js";
11
11
  *
12
12
  * Buckets:
13
13
  * - `accepted` — a write/content-authoring action succeeded.
14
- * - `rejected` — the action was deliberately not applied (cooldown, skip,
15
- * distill-skip, or a content-policy guard rejection). NOTE: as of the
16
- * round-2 health pass `reflect-guard-rejected` is bucketed here. Previously
17
- * the `state-db.ts` switch omitted it entirely (no case, no default), so a
18
- * guard rejection silently vanished from accepted/rejected/error totals — a
19
- * data-integrity miscount. It is a deliberate non-application of the action,
20
- * so it belongs with the other "rejected" outcomes.
14
+ * - `skipped` — the ref was GATED OUT before any content was produced: a
15
+ * cooldown, a signal-delta/eligibility skip, or a distill pool-delta skip.
16
+ * These are NOT rejections of produced content — they are the run declining
17
+ * to act on a ref it had no new reason to touch, and they scale with the
18
+ * whole indexed-ref pool (~13k/run), so folding them into `rejected` made the
19
+ * "accept rate" meaningless (deep-tuning analysis 2026-06-29, finding #1).
20
+ * - `rejected` the run PRODUCED a change and a content-policy guard then
21
+ * rejected it (`reflect-guard-rejected`). This is the genuine value-rejection
22
+ * signal; it is small and meaningful, no longer drowned by gated skips.
21
23
  * - `error` — the action failed (LLM/runtime error).
22
24
  * - `noop` — bookkeeping that is neither a write nor a rejection (memory-prune);
23
- * intentionally counted in none of the three numeric buckets.
25
+ * intentionally counted in none of the numeric buckets.
24
26
  *
25
27
  * The `default: assertNever(mode)` arm makes any future union variant a
26
28
  * compile-time error here, forcing an explicit bucket choice.
@@ -35,6 +37,7 @@ export function classifyImproveAction(mode) {
35
37
  case "reflect-cooldown":
36
38
  case "reflect-skipped":
37
39
  case "distill-skipped":
40
+ return "skipped";
38
41
  case "reflect-guard-rejected":
39
42
  return "rejected";
40
43
  case "reflect-failed":
@@ -485,7 +485,10 @@ function isRetryableBeginError(err) {
485
485
  const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
486
486
  return (msg.includes("within a transaction") ||
487
487
  msg.includes("database is locked") ||
488
- msg.includes("database table is locked"));
488
+ msg.includes("database table is locked") ||
489
+ // Phantom BEGIN (see below) — synthesized when BEGIN IMMEDIATE returns
490
+ // without opening a transaction. Safe to retry: fn() has not run.
491
+ msg.includes("did not open a transaction"));
489
492
  }
490
493
  const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
491
494
  /** Portable synchronous sleep (works under both Bun and Node). */
@@ -499,6 +502,15 @@ export function withImmediateTransaction(db, fn) {
499
502
  for (let attempt = 1; attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
500
503
  try {
501
504
  db.exec("BEGIN IMMEDIATE");
505
+ // bun:sqlite can return from BEGIN IMMEDIATE under writer contention WITHOUT
506
+ // actually opening a transaction (no throw). That phantom state otherwise
507
+ // surfaces as "cannot commit - no transaction is active" at COMMIT — AFTER
508
+ // fn() has already run in autocommit, so its writes escaped the intended
509
+ // serialization (the concurrent proposal-queue race). Detect it here, before
510
+ // fn(), and route it through the same retry path as a contended BEGIN.
511
+ if (!db.inTransaction) {
512
+ throw new Error("BEGIN IMMEDIATE did not open a transaction (phantom contention state)");
513
+ }
502
514
  }
503
515
  catch (err) {
504
516
  lastBeginErr = err;
@@ -740,15 +752,16 @@ export function computeImproveRunMetrics(result) {
740
752
  const actionsCount = actions.length;
741
753
  let acceptedCount = 0;
742
754
  let rejectedCount = 0;
755
+ let skippedCount = 0;
743
756
  let autoAcceptedCount = 0;
744
757
  let errorCount = 0;
745
758
  for (const action of actions) {
746
759
  // Bucketing delegated to the shared classifyImproveAction so this aggregate
747
760
  // and the improve_completed event in improve.ts can never disagree, and so a
748
- // new union variant is a compile error rather than a silent drop. Note:
749
- // `reflect-guard-rejected` now counts as "rejected" (previously this switch
750
- // omitted it entirely a data-integrity miscount). "noop" (memory-prune) is
751
- // intentionally counted in none of the three numeric buckets.
761
+ // new union variant is a compile error rather than a silent drop. Gated skips
762
+ // (cooldown / signal-delta / distill pool-delta) bucket to "skipped", NOT
763
+ // "rejected" only a guard-rejected produced change is a true rejection.
764
+ // "noop" (memory-prune) is intentionally counted in none of the buckets.
752
765
  switch (classifyImproveAction(action.mode)) {
753
766
  case "accepted":
754
767
  acceptedCount++;
@@ -756,6 +769,9 @@ export function computeImproveRunMetrics(result) {
756
769
  case "rejected":
757
770
  rejectedCount++;
758
771
  break;
772
+ case "skipped":
773
+ skippedCount++;
774
+ break;
759
775
  case "error":
760
776
  errorCount++;
761
777
  break;
@@ -769,7 +785,7 @@ export function computeImproveRunMetrics(result) {
769
785
  }
770
786
  // Add gate-promoted count from the unified PostPhaseAutoAcceptGate (all phases).
771
787
  autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
772
- return { plannedCount, actionsCount, acceptedCount, rejectedCount, autoAcceptedCount, errorCount };
788
+ return { plannedCount, actionsCount, acceptedCount, rejectedCount, skippedCount, autoAcceptedCount, errorCount };
773
789
  }
774
790
  /**
775
791
  * Insert a single improve-run row into `improve_runs`. Uses parameterised SQL.
@@ -837,10 +837,14 @@ export function rankCandidatesByUtility(db, candidates, _stashRoot) {
837
837
  *
838
838
  * Inferred-child memories (frontmatter `inferred: true`) are skipped — they
839
839
  * are already derived summaries, with no additional internal graph structure worth
840
- * extracting.
840
+ * extracting. Session-capture telemetry checkpoints are skipped too (see below).
841
841
  *
842
842
  * Exported for direct unit testing.
843
843
  */
844
+ // #632 — canonical session-capture name shape, mirrored from
845
+ // isSessionCaptureMemoryName (src/commands/improve/consolidate/eligibility.ts).
846
+ // Inlined to avoid an improve-layer import from the indexer layer.
847
+ const SESSION_CAPTURE_NAME_RE = /-(session|checkpoint)-\d{8}/;
844
848
  export function collectEligibleFiles(stashRoot, includeTypes = [...DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES]) {
845
849
  const out = [];
846
850
  for (const rawType of includeTypes) {
@@ -866,6 +870,14 @@ export function collectEligibleFiles(stashRoot, includeTypes = [...DEFAULT_GRAPH
866
870
  // graph to extract from a single-fact body.
867
871
  if (type === "memory" && parsed.data.inferred === true)
868
872
  continue;
873
+ // #632 — skip session-capture telemetry checkpoints (named
874
+ // `<harness>-(session|checkpoint)-<YYYYMMDD>-<id>`). Their bodies are
875
+ // pipeline bookkeeping; graph-extracting them lifts metadata fields as
876
+ // entities that then dominate clustering as bland stash-wide buckets.
877
+ // Mirrors isSessionCaptureMemoryName (consolidate/eligibility.ts); inlined
878
+ // here to keep the indexer layer free of an improve-layer import.
879
+ if (type === "memory" && SESSION_CAPTURE_NAME_RE.test(path.basename(filePath, ".md")))
880
+ continue;
869
881
  const body = parsed.content.trim();
870
882
  if (!body)
871
883
  continue;
@@ -314,7 +314,13 @@ function parseGraphExtraction(raw) {
314
314
  if (!normalized)
315
315
  continue;
316
316
  const normalizedKey = normalized.toLowerCase();
317
- if (!/[a-z0-9]/i.test(normalized) || GENERIC_ENTITIES.has(normalizedKey)) {
317
+ // Drop generic/empty entities AND raw file/dir paths (anything with a
318
+ // path separator) — the prompt no longer asks for them and isJunkEntity
319
+ // discards them downstream, so emitting them is pure waste/junk (#632).
320
+ if (!/[a-z0-9]/i.test(normalized) ||
321
+ GENERIC_ENTITIES.has(normalizedKey) ||
322
+ normalized.includes("/") ||
323
+ normalized.includes("\\")) {
318
324
  filteredGenericEntities += 1;
319
325
  continue;
320
326
  }
@@ -8986,6 +8986,7 @@ function classifyImproveAction(mode) {
8986
8986
  case "reflect-cooldown":
8987
8987
  case "reflect-skipped":
8988
8988
  case "distill-skipped":
8989
+ return "skipped";
8989
8990
  case "reflect-guard-rejected":
8990
8991
  return "rejected";
8991
8992
  case "reflect-failed":
@@ -9701,7 +9702,7 @@ function insertProposalIfAbsent(db, proposal, stashDir) {
9701
9702
  }
9702
9703
  function isRetryableBeginError(err) {
9703
9704
  const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
9704
- return msg.includes("within a transaction") || msg.includes("database is locked") || msg.includes("database table is locked");
9705
+ return msg.includes("within a transaction") || msg.includes("database is locked") || msg.includes("database table is locked") || msg.includes("did not open a transaction");
9705
9706
  }
9706
9707
  function sleepSyncMs(ms) {
9707
9708
  if (ms <= 0)
@@ -9713,6 +9714,9 @@ function withImmediateTransaction(db, fn) {
9713
9714
  for (let attempt = 1;attempt <= WITH_IMMEDIATE_TX_MAX_ATTEMPTS; attempt++) {
9714
9715
  try {
9715
9716
  db.exec("BEGIN IMMEDIATE");
9717
+ if (!db.inTransaction) {
9718
+ throw new Error("BEGIN IMMEDIATE did not open a transaction (phantom contention state)");
9719
+ }
9716
9720
  } catch (err) {
9717
9721
  lastBeginErr = err;
9718
9722
  if (isRetryableBeginError(err) && attempt < WITH_IMMEDIATE_TX_MAX_ATTEMPTS) {
@@ -9846,6 +9850,7 @@ function computeImproveRunMetrics(result) {
9846
9850
  const actionsCount = actions.length;
9847
9851
  let acceptedCount = 0;
9848
9852
  let rejectedCount = 0;
9853
+ let skippedCount = 0;
9849
9854
  let autoAcceptedCount = 0;
9850
9855
  let errorCount = 0;
9851
9856
  for (const action of actions) {
@@ -9856,6 +9861,9 @@ function computeImproveRunMetrics(result) {
9856
9861
  case "rejected":
9857
9862
  rejectedCount++;
9858
9863
  break;
9864
+ case "skipped":
9865
+ skippedCount++;
9866
+ break;
9859
9867
  case "error":
9860
9868
  errorCount++;
9861
9869
  break;
@@ -9867,7 +9875,7 @@ function computeImproveRunMetrics(result) {
9867
9875
  autoAcceptedCount++;
9868
9876
  }
9869
9877
  autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
9870
- return { plannedCount, actionsCount, acceptedCount, rejectedCount, autoAcceptedCount, errorCount };
9878
+ return { plannedCount, actionsCount, acceptedCount, rejectedCount, skippedCount, autoAcceptedCount, errorCount };
9871
9879
  }
9872
9880
  function recordImproveRun(db, input) {
9873
9881
  const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
@@ -15850,7 +15858,8 @@ var init_config_schema = __esm(() => {
15850
15858
  bin: exports_external.string().min(1).optional(),
15851
15859
  args: exports_external.array(exports_external.string()).optional(),
15852
15860
  workspace: exports_external.string().min(1).optional(),
15853
- model: exports_external.string().min(1).optional()
15861
+ model: exports_external.string().min(1).optional(),
15862
+ timeoutMs: exports_external.union([positiveInt, exports_external.null()]).optional()
15854
15863
  }).passthrough();
15855
15864
  ImproveProcessConfigSchema = exports_external.object({
15856
15865
  enabled: exports_external.boolean().optional(),
@@ -15878,6 +15887,11 @@ var init_config_schema = __esm(() => {
15878
15887
  dueDays: exports_external.number().int().min(0).optional(),
15879
15888
  maxPerRun: positiveInt.optional(),
15880
15889
  topN: positiveInt.optional(),
15890
+ fullScan: exports_external.boolean().optional(),
15891
+ triage: exports_external.object({
15892
+ enabled: exports_external.boolean().optional(),
15893
+ minScore: exports_external.number().min(0).optional()
15894
+ }).passthrough().optional(),
15881
15895
  minPendingCount: exports_external.number().int().min(0).optional(),
15882
15896
  minNewSessions: exports_external.number().int().min(0).optional(),
15883
15897
  maxSessionsPerRun: exports_external.number().int().min(0).optional(),
@@ -15939,6 +15953,7 @@ var init_config_schema = __esm(() => {
15939
15953
  consolidate: ImproveProcessConfigSchema.optional(),
15940
15954
  memoryInference: ImproveProcessConfigSchema.optional(),
15941
15955
  graphExtraction: ImproveProcessConfigSchema.optional(),
15956
+ extract: ImproveProcessConfigSchema.optional(),
15942
15957
  validation: ImproveProcessConfigSchema.optional(),
15943
15958
  triage: ImproveProcessConfigSchema.optional(),
15944
15959
  proactiveMaintenance: ImproveProcessConfigSchema.optional(),
@@ -8713,6 +8713,7 @@ function classifyImproveAction(mode) {
8713
8713
  case "reflect-cooldown":
8714
8714
  case "reflect-skipped":
8715
8715
  case "distill-skipped":
8716
+ return "skipped";
8716
8717
  case "reflect-guard-rejected":
8717
8718
  return "rejected";
8718
8719
  case "reflect-failed":
@@ -9160,6 +9161,7 @@ function computeImproveRunMetrics(result) {
9160
9161
  const actionsCount = actions.length;
9161
9162
  let acceptedCount = 0;
9162
9163
  let rejectedCount = 0;
9164
+ let skippedCount = 0;
9163
9165
  let autoAcceptedCount = 0;
9164
9166
  let errorCount = 0;
9165
9167
  for (const action of actions) {
@@ -9170,6 +9172,9 @@ function computeImproveRunMetrics(result) {
9170
9172
  case "rejected":
9171
9173
  rejectedCount++;
9172
9174
  break;
9175
+ case "skipped":
9176
+ skippedCount++;
9177
+ break;
9173
9178
  case "error":
9174
9179
  errorCount++;
9175
9180
  break;
@@ -9181,7 +9186,7 @@ function computeImproveRunMetrics(result) {
9181
9186
  autoAcceptedCount++;
9182
9187
  }
9183
9188
  autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
9184
- return { plannedCount, actionsCount, acceptedCount, rejectedCount, autoAcceptedCount, errorCount };
9189
+ return { plannedCount, actionsCount, acceptedCount, rejectedCount, skippedCount, autoAcceptedCount, errorCount };
9185
9190
  }
9186
9191
  function recordImproveRun(db, input) {
9187
9192
  const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-beta.49",
3
+ "version": "0.9.0-beta.50",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [
@@ -69,7 +69,7 @@
69
69
  "lint:devto-posts:fix": "bun scripts/lint-devto-posts.ts --fix",
70
70
  "publish:devto": "npx -y @sinedied/devto-cli push \"docs/posts/**/*.md\" --token \"$DEVTO_TOKEN\" --repo \"$GITHUB_REPOSITORY\" --branch \"${GITHUB_REF_NAME:-main}\" --reconcile",
71
71
  "release:check": "./tests/release-check.sh",
72
- "lint": "bunx biome check src/ tests/ && bun scripts/lint-tests-isolation.ts && bun scripts/lint-license-headers.ts && bun scripts/lint-runtime-boundary.ts && bun scripts/lint-repository-sql.ts",
72
+ "lint": "bunx biome check src/ tests/ && bun scripts/lint-tests-isolation.ts && bun scripts/lint-license-headers.ts && bun scripts/lint-runtime-boundary.ts && bun scripts/lint-repository-sql.ts && bun scripts/gen-config-schema.ts --check",
73
73
  "lint:runtime-boundary": "bun scripts/lint-runtime-boundary.ts",
74
74
  "lint:tests-isolation": "bun scripts/lint-tests-isolation.ts",
75
75
  "lint:fix": "bunx biome check --write src/ tests/",