akm-cli 0.9.0-beta.54 → 0.9.0-beta.55

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 (101) hide show
  1. package/dist/cli.js +5 -3
  2. package/dist/commands/agent/contribute-cli.js +2 -3
  3. package/dist/commands/env/env-cli.js +187 -202
  4. package/dist/commands/env/secret-cli.js +109 -121
  5. package/dist/commands/feedback-cli.js +152 -155
  6. package/dist/commands/health/advisories.js +151 -0
  7. package/dist/commands/health/improve-metrics.js +754 -0
  8. package/dist/commands/health/llm-usage.js +65 -0
  9. package/dist/commands/health/md-report.js +103 -0
  10. package/dist/commands/health/metrics.js +278 -0
  11. package/dist/commands/health/task-runs.js +135 -0
  12. package/dist/commands/health/types.js +18 -0
  13. package/dist/commands/health/windows.js +196 -0
  14. package/dist/commands/health.js +14 -1624
  15. package/dist/commands/improve/anti-collapse.js +170 -0
  16. package/dist/commands/improve/collapse-detector.js +3 -2
  17. package/dist/commands/improve/consolidate.js +636 -633
  18. package/dist/commands/improve/dedup.js +1 -1
  19. package/dist/commands/improve/distill/content-repair.js +202 -0
  20. package/dist/commands/improve/distill/promote-memory.js +228 -0
  21. package/dist/commands/improve/distill/quality-gate.js +233 -0
  22. package/dist/commands/improve/distill-guards.js +127 -0
  23. package/dist/commands/improve/distill.js +49 -575
  24. package/dist/commands/improve/extract-cli.js +74 -76
  25. package/dist/commands/improve/extract.js +6 -4
  26. package/dist/commands/improve/hot-probation.js +45 -0
  27. package/dist/commands/improve/improve-auto-accept.js +3 -2
  28. package/dist/commands/improve/improve-cli.js +14 -13
  29. package/dist/commands/improve/improve-result-file.js +2 -1
  30. package/dist/commands/improve/improve.js +6 -5
  31. package/dist/commands/improve/loop-stages.js +19 -21
  32. package/dist/commands/improve/preparation.js +4 -2
  33. package/dist/commands/improve/procedural.js +10 -31
  34. package/dist/commands/improve/recombine.js +19 -43
  35. package/dist/commands/improve/reflect.js +1 -1
  36. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  37. package/dist/commands/improve/shared.js +48 -0
  38. package/dist/commands/observability-cli.js +4 -4
  39. package/dist/commands/proposal/drain-policies.js +2 -2
  40. package/dist/commands/proposal/drain.js +1 -1
  41. package/dist/commands/proposal/legacy-import.js +115 -0
  42. package/dist/commands/proposal/proposal-cli.js +3 -3
  43. package/dist/commands/proposal/proposal.js +2 -1
  44. package/dist/commands/proposal/propose.js +1 -1
  45. package/dist/commands/proposal/repository.js +829 -0
  46. package/dist/commands/proposal/validators/proposals.js +5 -920
  47. package/dist/commands/read/remember-cli.js +132 -137
  48. package/dist/commands/read/search-cli.js +1 -1
  49. package/dist/commands/registry-cli.js +76 -87
  50. package/dist/commands/sources/add-cli.js +90 -94
  51. package/dist/commands/sources/history.js +1 -1
  52. package/dist/commands/sources/schema-repair.js +1 -1
  53. package/dist/commands/sources/sources-cli.js +3 -3
  54. package/dist/commands/sources/stash-cli.js +1 -1
  55. package/dist/commands/tasks/tasks-cli.js +1 -2
  56. package/dist/commands/wiki-cli.js +2 -3
  57. package/dist/core/common.js +3 -3
  58. package/dist/core/config/config-schema.js +6 -0
  59. package/dist/core/deep-merge.js +38 -0
  60. package/dist/core/events.js +2 -1
  61. package/dist/core/logs-db.js +8 -13
  62. package/dist/core/paths.js +14 -14
  63. package/dist/core/state-db.js +13 -1140
  64. package/dist/indexer/db/db.js +66 -709
  65. package/dist/indexer/db/entry-mapper.js +41 -0
  66. package/dist/indexer/db/schema.js +516 -0
  67. package/dist/indexer/feedback/utility-policy.js +85 -0
  68. package/dist/indexer/graph/graph-extraction.js +2 -1
  69. package/dist/indexer/index-writer-lock.js +9 -0
  70. package/dist/indexer/indexer.js +78 -23
  71. package/dist/indexer/search/fts-query.js +51 -0
  72. package/dist/integrations/agent/spawn.js +15 -66
  73. package/dist/output/text/helpers.js +13 -0
  74. package/dist/scripts/migrate-storage.js +6891 -7436
  75. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
  76. package/dist/setup/legacy-config.js +106 -0
  77. package/dist/setup/prompt.js +57 -0
  78. package/dist/setup/providers.js +14 -0
  79. package/dist/setup/semantic-assets.js +124 -0
  80. package/dist/setup/setup.js +24 -1607
  81. package/dist/setup/steps/connection.js +734 -0
  82. package/dist/setup/steps/output.js +31 -0
  83. package/dist/setup/steps/platforms.js +124 -0
  84. package/dist/setup/steps/semantic.js +27 -0
  85. package/dist/setup/steps/sources.js +222 -0
  86. package/dist/setup/steps/stashdir.js +42 -0
  87. package/dist/setup/steps/tasks.js +152 -0
  88. package/dist/storage/repositories/canaries-repository.js +107 -0
  89. package/dist/storage/repositories/consolidation-repository.js +38 -0
  90. package/dist/storage/repositories/embeddings-repository.js +72 -0
  91. package/dist/storage/repositories/events-repository.js +187 -0
  92. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  93. package/dist/storage/repositories/improve-runs-repository.js +130 -0
  94. package/dist/storage/repositories/index-db.js +4 -7
  95. package/dist/storage/repositories/proposals-repository.js +220 -0
  96. package/dist/storage/repositories/recombine-repository.js +213 -0
  97. package/dist/storage/repositories/task-history-repository.js +93 -0
  98. package/dist/storage/sqlite-pragmas.js +3 -3
  99. package/dist/tasks/runner.js +2 -1
  100. package/package.json +1 -1
  101. package/dist/commands/improve/homeostatic.js +0 -497
@@ -24,17 +24,15 @@ import fs from "node:fs";
24
24
  import proceduralSystemPrompt from "../../assets/prompts/procedural-system.md" with { type: "text" };
25
25
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
26
26
  import { resolveStashDir } from "../../core/common.js";
27
- import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
27
+ import { loadConfig } from "../../core/config/config.js";
28
28
  import { appendEvent } from "../../core/events.js";
29
29
  import { parseEmbeddedJsonResponse } from "../../core/parse.js";
30
30
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
31
- import { warn } from "../../core/warn.js";
32
31
  import { closeDatabase, getAllEntries, openExistingDatabase } from "../../indexer/db/db.js";
33
- import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
34
- import { chatCompletion } from "../../llm/client.js";
35
32
  import { parseWorkflow } from "../../workflows/parser.js";
33
+ import { createProposal, isProposalSkipped } from "../proposal/repository.js";
36
34
  import { validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
37
- import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
35
+ import { resolveImproveLlmFn } from "./shared.js";
38
36
  const PROCEDURAL_SYSTEM_PROMPT = proceduralSystemPrompt;
39
37
  const DEFAULT_MIN_RECURRENCE = 3;
40
38
  const DEFAULT_MAX_PROPOSALS_PER_RUN = 3;
@@ -240,31 +238,6 @@ export function assembleWorkflowMarkdown(doc) {
240
238
  return lines.join("\n");
241
239
  }
242
240
  // ── Production LLM seam ───────────────────────────────────────────────────────
243
- /**
244
- * Resolve the production LLM seam from the active improve profile. Returns a
245
- * `ProceduralLlmFn` that issues one bounded chatCompletion per call, or
246
- * `undefined` when no LLM is configured (the pass then makes no calls).
247
- */
248
- function resolveProductionLlmFn(config, signal) {
249
- const proceduralProcess = config.profiles?.improve?.default?.processes?.procedural;
250
- const runnerSpec = resolveImproveProcessRunnerFromProfile(proceduralProcess, config);
251
- const llmConfig = runnerSpec && runnerIsLlm(runnerSpec) ? runnerSpec.connection : getDefaultLlmConfig(config);
252
- if (!llmConfig)
253
- return undefined;
254
- return async (prompt) => {
255
- const messages = [
256
- { role: "system", content: PROCEDURAL_SYSTEM_PROMPT },
257
- { role: "user", content: prompt },
258
- ];
259
- try {
260
- return await chatCompletion(llmConfig, messages, { signal, enableThinking: false });
261
- }
262
- catch (e) {
263
- warn(`[procedural] LLM call failed: ${String(e)}`);
264
- return null;
265
- }
266
- };
267
- }
268
241
  // ── Main entry point ───────────────────────────────────────────────────────────
269
242
  export async function akmProcedural(opts) {
270
243
  const startMs = Date.now();
@@ -313,7 +286,13 @@ export async function akmProcedural(opts) {
313
286
  if (clusters.length === 0) {
314
287
  return finish({ sequencesScanned, clustersFormed: 0 });
315
288
  }
316
- const llmFn = opts.proceduralLlmFn ?? resolveProductionLlmFn(config, opts.signal);
289
+ const llmFn = opts.proceduralLlmFn ??
290
+ resolveImproveLlmFn(config, {
291
+ processKey: "procedural",
292
+ systemPrompt: PROCEDURAL_SYSTEM_PROMPT,
293
+ tag: "[procedural]",
294
+ signal: opts.signal,
295
+ });
317
296
  if (!llmFn) {
318
297
  warnings.push("procedural: no LLM configured — skipping");
319
298
  return finish({ sequencesScanned, clustersFormed: 0 });
@@ -40,20 +40,20 @@
40
40
  import { createHash } from "node:crypto";
41
41
  import fs from "node:fs";
42
42
  import recombineSystemPrompt from "../../assets/prompts/recombine-system.md" with { type: "text" };
43
+ import { assembleAssetFromString } from "../../core/asset/asset-serialize.js";
43
44
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
44
45
  import { resolveStashDir } from "../../core/common.js";
45
- import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
46
+ import { loadConfig } from "../../core/config/config.js";
46
47
  import { appendEvent } from "../../core/events.js";
47
48
  import { parseEmbeddedJsonResponse } from "../../core/parse.js";
48
49
  import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
49
- import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis, markRecombineHypothesisPromoted, recordRecombineInduction, withStateDbAsync, } from "../../core/state-db.js";
50
- import { warn } from "../../core/warn.js";
50
+ import { withStateDbAsync } from "../../core/state-db.js";
51
51
  import { closeDatabase, getAllEntries, getEntitiesByEntryIds, openExistingDatabase, } from "../../indexer/db/db.js";
52
- import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
53
- import { chatCompletion } from "../../llm/client.js";
52
+ import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis, markRecombineHypothesisPromoted, recordRecombineInduction, } from "../../storage/repositories/recombine-repository.js";
53
+ import { archiveProposal, createProposal, isProposalSkipped, listProposals } from "../proposal/repository.js";
54
54
  import { isValidDescription, isValidWhenToUse, validateProposalFrontmatter, } from "../proposal/validators/proposal-quality-validators.js";
55
- import { archiveProposal, createProposal, isProposalSkipped, listProposals } from "../proposal/validators/proposals.js";
56
55
  import { isConsolidationEligibleMemoryName, isSessionCaptureMemoryName } from "./consolidate.js";
56
+ import { resolveImproveLlmFn } from "./shared.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;
@@ -487,31 +487,6 @@ function parseGeneralization(raw) {
487
487
  return null;
488
488
  return { description, body, ...(when_to_use ? { when_to_use } : {}) };
489
489
  }
490
- /**
491
- * Resolve the production LLM seam from the active improve profile. Returns a
492
- * `RecombineLlmFn` that issues one bounded chatCompletion per call, or
493
- * `undefined` when no LLM is configured (the pass then makes no calls).
494
- */
495
- function resolveProductionLlmFn(config, signal) {
496
- const recombineProcess = config.profiles?.improve?.default?.processes?.recombine;
497
- const runnerSpec = resolveImproveProcessRunnerFromProfile(recombineProcess, config);
498
- const llmConfig = runnerSpec && runnerIsLlm(runnerSpec) ? runnerSpec.connection : getDefaultLlmConfig(config);
499
- if (!llmConfig)
500
- return undefined;
501
- return async (clusterPrompt) => {
502
- const messages = [
503
- { role: "system", content: RECOMBINE_SYSTEM_PROMPT },
504
- { role: "user", content: clusterPrompt },
505
- ];
506
- try {
507
- return await chatCompletion(llmConfig, messages, { signal, enableThinking: false });
508
- }
509
- catch (e) {
510
- warn(`[recombine] LLM call failed: ${String(e)}`);
511
- return null;
512
- }
513
- };
514
- }
515
490
  // ── Main entry point ───────────────────────────────────────────────────────────
516
491
  export async function akmRecombine(opts) {
517
492
  const startMs = Date.now();
@@ -581,7 +556,13 @@ export async function akmRecombine(opts) {
581
556
  let proposalsEmitted = 0;
582
557
  let lessonsPromoted = 0;
583
558
  let nullsReturned = 0;
584
- const llmFn = opts.recombineLlmFn ?? resolveProductionLlmFn(config, opts.signal);
559
+ const llmFn = opts.recombineLlmFn ??
560
+ resolveImproveLlmFn(config, {
561
+ processKey: "recombine",
562
+ systemPrompt: RECOMBINE_SYSTEM_PROMPT,
563
+ tag: "[recombine]",
564
+ signal: opts.signal,
565
+ });
585
566
  if (!llmFn) {
586
567
  warnings.push("recombine: no LLM configured — skipping");
587
568
  return finish({ clustersFormed: 0 });
@@ -836,15 +817,10 @@ export async function akmRecombine(opts) {
836
817
  }
837
818
  /** Serialize frontmatter + body into a markdown asset string. */
838
819
  function assembleContent(frontmatter, body) {
839
- const lines = ["---"];
840
- for (const [key, value] of Object.entries(frontmatter)) {
841
- if (Array.isArray(value)) {
842
- lines.push(`${key}: [${value.map((v) => JSON.stringify(v)).join(", ")}]`);
843
- }
844
- else {
845
- lines.push(`${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
846
- }
847
- }
848
- lines.push("---", "", body, "");
849
- return lines.join("\n");
820
+ const fmLines = Object.entries(frontmatter)
821
+ .map(([key, value]) => Array.isArray(value)
822
+ ? `${key}: [${value.map((v) => JSON.stringify(v)).join(", ")}]`
823
+ : `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`)
824
+ .join("\n");
825
+ return assembleAssetFromString(fmLines, body);
850
826
  }
@@ -45,8 +45,8 @@ import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.
45
45
  import { chatCompletion } from "../../llm/client.js";
46
46
  import { isLlmFeatureEnabled } from "../../llm/feature-gate.js";
47
47
  import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "../agent/agent-support.js";
48
+ import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
48
49
  import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
49
- import { createProposal, isProposalSkipped, listProposals, } from "../proposal/validators/proposals.js";
50
50
  import { deriveLessonRef, runLessonQualityJudge } from "./distill.js";
51
51
  import { classifyReflectChange } from "./reflect-noise.js";
52
52
  const MAX_FEEDBACK_LINES = 10;
@@ -0,0 +1,168 @@
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
+ * WS-3b Step 0b — Schema-similarity intake gate.
6
+ *
7
+ * At intake, if a new candidate's body embedding is within ε of an existing
8
+ * derived-layer lesson/knowledge node, mark `schema-consistent` and lower its
9
+ * priority; only schema-inconsistent/contradicting candidates get full
10
+ * `encodingSalience`. One embedding lookup via body_embeddings cache; relieves
11
+ * dedup pressure before it accumulates.
12
+ *
13
+ * @module schema-similarity-gate
14
+ */
15
+ import { warn } from "../../core/warn.js";
16
+ import { closeDatabase, openExistingDatabase } from "../../indexer/db/db.js";
17
+ /** Default epsilon for schema-similarity gate (looser than dedup's 0.97). */
18
+ export const DEFAULT_SCHEMA_SIMILARITY_EPSILON = 0.85;
19
+ /** Default multiplicative confidence penalty applied to schema-consistent candidates. */
20
+ export const DEFAULT_SCHEMA_CONFIDENCE_PENALTY = 0.5;
21
+ /**
22
+ * Check whether a candidate body embedding is schema-consistent with an existing
23
+ * derived-layer lesson/knowledge node. Returns `true` when the candidate is
24
+ * within ε of ANY existing derived node (i.e. it's likely covering ground the
25
+ * derived layer already knows about, so give it lower priority).
26
+ *
27
+ * One embedding lookup via the body_embeddings cache; no LLM call.
28
+ * Fails open: returns `false` (not schema-consistent) on any error so the
29
+ * candidate is not silently dropped.
30
+ *
31
+ * @param candidateEmbedding - Float32 embedding vector for the candidate body.
32
+ * @param existingDerivedEmbeddings - Pre-loaded embeddings for existing derived assets.
33
+ * @param config - Schema-similarity gate config.
34
+ */
35
+ export function isSchemaConsistent(candidateEmbedding, existingDerivedEmbeddings, config) {
36
+ if (!config.enabled || existingDerivedEmbeddings.length === 0) {
37
+ return { consistent: false };
38
+ }
39
+ const epsilon = config.epsilon ?? DEFAULT_SCHEMA_SIMILARITY_EPSILON;
40
+ let bestSim = -Infinity;
41
+ let bestRef;
42
+ for (const { ref, embedding } of existingDerivedEmbeddings) {
43
+ // cosine similarity: dot(a,b) / (|a| * |b|)
44
+ let dot = 0;
45
+ let magA = 0;
46
+ let magB = 0;
47
+ for (let i = 0; i < candidateEmbedding.length; i++) {
48
+ const a = candidateEmbedding[i] ?? 0;
49
+ const b = embedding[i] ?? 0;
50
+ dot += a * b;
51
+ magA += a * a;
52
+ magB += b * b;
53
+ }
54
+ const sim = magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB));
55
+ if (sim > bestSim) {
56
+ bestSim = sim;
57
+ bestRef = ref;
58
+ }
59
+ }
60
+ if (bestSim >= epsilon) {
61
+ return { consistent: true, matchedRef: bestRef, similarity: bestSim };
62
+ }
63
+ return { consistent: false };
64
+ }
65
+ /**
66
+ * WS-3b Step-0b: apply the schema-similarity intake gate to one extract
67
+ * candidate. Pure/deterministic given `embedText`, so it is directly unit
68
+ * testable without the full extract→LLM harness.
69
+ *
70
+ * Returns the (possibly penalised) effective confidence plus a `penalised` flag
71
+ * and an optional human-readable `warning`. Parity guarantees:
72
+ * - `ctx === null` (gate disabled / default-off) → no change, never embeds.
73
+ * - empty `derivedEmbeddings` → no change, never embeds.
74
+ * - candidate type not lesson/knowledge → no change, never embeds.
75
+ * - embed throws → fail open (no change), warns.
76
+ */
77
+ export async function applySchemaSimilarityPenalty(candidate, ctx, embedText) {
78
+ const baseConfidence = typeof candidate.confidence === "number" ? candidate.confidence : undefined;
79
+ if (ctx === null || ctx.derivedEmbeddings.length === 0) {
80
+ return { effectiveConfidence: baseConfidence, penalised: false };
81
+ }
82
+ if (candidate.type !== "lesson" && candidate.type !== "knowledge") {
83
+ return { effectiveConfidence: baseConfidence, penalised: false };
84
+ }
85
+ try {
86
+ const candidateVec = await embedText(candidate.body);
87
+ const check = isSchemaConsistent(candidateVec, ctx.derivedEmbeddings, ctx.config);
88
+ if (check.consistent) {
89
+ const penalty = ctx.config.confidencePenalty ?? DEFAULT_SCHEMA_CONFIDENCE_PENALTY;
90
+ return {
91
+ effectiveConfidence: (baseConfidence ?? 1.0) * penalty,
92
+ penalised: true,
93
+ warning: `[extract] schema-consistent candidate ${candidate.type}:${candidate.name} ` +
94
+ `(sim=${check.similarity?.toFixed(3)} vs ${check.matchedRef}) — confidence penalised ×${penalty}`,
95
+ };
96
+ }
97
+ return { effectiveConfidence: baseConfidence, penalised: false };
98
+ }
99
+ catch (embedErr) {
100
+ // Fail open: embed errors must never abort extraction.
101
+ return {
102
+ effectiveConfidence: baseConfidence,
103
+ penalised: false,
104
+ warning: `[extract] schema-similarity embed failed for ${candidate.type}:${candidate.name} — skipping gate: ` +
105
+ (embedErr instanceof Error ? embedErr.message : String(embedErr)),
106
+ };
107
+ }
108
+ }
109
+ /**
110
+ * Load persisted body embeddings for all indexed **derived-layer**
111
+ * (lesson + knowledge) entries from index.db. Returns an empty array when
112
+ * the DB is unavailable, empty, or the embeddings table has no entries for
113
+ * those types — the caller treats an empty array as "gate inactive".
114
+ *
115
+ * FAIL-OPEN: any error emits a debug warning and returns an empty array.
116
+ * This ensures the extract pass never fails because of a missing index.
117
+ *
118
+ * The returned entries are keyed by `entry_key` (e.g. "lesson:foo",
119
+ * "knowledge:bar"). Only entries whose embedding dimension matches the first
120
+ * observed dimension are included (mixed-dim BLOBs are silently skipped).
121
+ *
122
+ * @param dbPath - Optional path override for index.db (for testing).
123
+ */
124
+ export function loadDerivedLayerEmbeddings(dbPath) {
125
+ let db;
126
+ try {
127
+ db = openExistingDatabase(dbPath);
128
+ const rows = db
129
+ .prepare(`SELECT e.entry_key, emb.embedding
130
+ FROM entries e
131
+ JOIN embeddings emb ON emb.id = e.id
132
+ WHERE e.entry_type IN ('lesson', 'knowledge')`)
133
+ .all();
134
+ if (rows.length === 0)
135
+ return [];
136
+ let expectedDim;
137
+ const result = [];
138
+ for (const row of rows) {
139
+ const buf = row.embedding;
140
+ if (!buf || buf.byteLength === 0 || buf.byteLength % 4 !== 0)
141
+ continue;
142
+ const dim = buf.byteLength / 4;
143
+ if (expectedDim === undefined)
144
+ expectedDim = dim;
145
+ if (dim !== expectedDim)
146
+ continue;
147
+ const aligned = new ArrayBuffer(buf.byteLength);
148
+ new Uint8Array(aligned).set(buf);
149
+ const f32 = new Float32Array(aligned);
150
+ result.push({ ref: row.entry_key, embedding: Array.from(f32) });
151
+ }
152
+ return result;
153
+ }
154
+ catch (err) {
155
+ warn("[schema-similarity-gate] loadDerivedLayerEmbeddings: failed to load from index.db — gate inactive:", err instanceof Error ? err.message : String(err));
156
+ return [];
157
+ }
158
+ finally {
159
+ if (db) {
160
+ try {
161
+ closeDatabase(db);
162
+ }
163
+ catch {
164
+ // ignore close errors
165
+ }
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,48 @@
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
+ import { getDefaultLlmConfig } from "../../core/config/config.js";
5
+ import { warn } from "../../core/warn.js";
6
+ import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
7
+ import { chatCompletion } from "../../llm/client.js";
8
+ /** Normalize an unknown thrown value to a human-readable message string. */
9
+ export function errMessage(e) {
10
+ return e instanceof Error ? e.message : String(e);
11
+ }
12
+ /**
13
+ * Slugify an asset ref for use in eval-case / rejection filenames: lowercase,
14
+ * non-alphanumerics collapsed to `-`, capped at 60 characters.
15
+ */
16
+ export function refSlug(ref) {
17
+ return ref
18
+ .replace(/[^a-z0-9]/gi, "-")
19
+ .toLowerCase()
20
+ .slice(0, 60);
21
+ }
22
+ /**
23
+ * Resolve the production LLM seam for an improve process (`recombine` /
24
+ * `procedural`) from the active default improve profile. Returns a function
25
+ * that issues one bounded chatCompletion per call, or `undefined` when no LLM
26
+ * is configured (the pass then makes no calls). Previously copied verbatim in
27
+ * recombine.ts and procedural.ts.
28
+ */
29
+ export function resolveImproveLlmFn(config, opts) {
30
+ const processConfig = config.profiles?.improve?.default?.processes?.[opts.processKey];
31
+ const runnerSpec = resolveImproveProcessRunnerFromProfile(processConfig, config);
32
+ const llmConfig = runnerSpec && runnerIsLlm(runnerSpec) ? runnerSpec.connection : getDefaultLlmConfig(config);
33
+ if (!llmConfig)
34
+ return undefined;
35
+ return async (prompt) => {
36
+ const messages = [
37
+ { role: "system", content: opts.systemPrompt },
38
+ { role: "user", content: prompt },
39
+ ];
40
+ try {
41
+ return await chatCompletion(llmConfig, messages, { signal: opts.signal, enableThinking: false });
42
+ }
43
+ catch (e) {
44
+ warn(`${opts.tag} LLM call failed: ${String(e)}`);
45
+ return null;
46
+ }
47
+ };
48
+ }
@@ -28,7 +28,7 @@ import { parsePositiveIntFlag } from "../cli/parse-args.js";
28
28
  import { defineJsonCommand, output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared.js";
29
29
  import { closeDatabase, collectTagSetFromEntries, openExistingDatabase } from "../indexer/db/db.js";
30
30
  import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "../output/cli-hints.js";
31
- import { getHyphenatedArg, getOutputMode, parseDetailLevel } from "../output/context.js";
31
+ import { getOutputMode, parseDetailLevel } from "../output/context.js";
32
32
  import { formatEventLine } from "../output/text.js";
33
33
  import { getDirname } from "../runtime.js";
34
34
  import { akmEventsList, akmEventsTail } from "./events.js";
@@ -90,9 +90,9 @@ const eventsTailCommand = defineCommand({
90
90
  },
91
91
  async run({ args }) {
92
92
  await runWithJsonErrors(async () => {
93
- const intervalMs = parsePositiveIntFlag(getHyphenatedArg(args, "interval-ms"), "--interval-ms");
94
- const maxDurationMs = parsePositiveIntFlag(getHyphenatedArg(args, "max-duration-ms"), "--max-duration-ms");
95
- const maxEvents = parsePositiveIntFlag(getHyphenatedArg(args, "max-events"), "--max-events");
93
+ const intervalMs = parsePositiveIntFlag(args["interval-ms"], "--interval-ms");
94
+ const maxDurationMs = parsePositiveIntFlag(args["max-duration-ms"], "--max-duration-ms");
95
+ const maxEvents = parsePositiveIntFlag(args["max-events"], "--max-events");
96
96
  const mode = getOutputMode();
97
97
  // In streaming text mode we want each event to print as soon as it
98
98
  // arrives. The polling loop emits via `onEvent`; the final result is
@@ -17,9 +17,9 @@
17
17
  import fs from "node:fs";
18
18
  import { z } from "zod";
19
19
  import { UsageError } from "../../core/errors.js";
20
- import { PROPOSAL_SOURCES } from "./validators/proposals.js";
20
+ import { PROPOSAL_SOURCES } from "./repository.js";
21
21
  // Valid `generator` values for a drain rule are exactly the canonical proposal
22
- // `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/validators/proposals.ts). The
22
+ // `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/repository.ts). The
23
23
  // engine matches rules via `policy.accept.find(r => r.generator === proposal.source)`,
24
24
  // so a generator that is not a real source can never match — it would be a
25
25
  // silent permanent no-op. Validate against the closed set to surface typos.
@@ -44,7 +44,7 @@ import { info, warn } from "../../core/warn.js";
44
44
  import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
45
45
  import { chatCompletion, stripJsonFences } from "../../llm/client.js";
46
46
  import { akmProposalAccept, akmProposalReject } from "./proposal.js";
47
- import { listProposals, recordGateDecision } from "./validators/proposals.js";
47
+ import { listProposals, recordGateDecision } from "./repository.js";
48
48
  // ---------------------------------------------------------------------------
49
49
  // Content helpers
50
50
  // ---------------------------------------------------------------------------
@@ -0,0 +1,115 @@
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
+ * Legacy filesystem proposal import (#578).
6
+ *
7
+ * Before 0.9.0 proposals lived as per-uuid JSON directories under
8
+ * `<stashDir>/.akm/proposals/` (live) and `…/proposals/archive/` (archived).
9
+ * The first proposal operation against a stash imports any legacy
10
+ * `proposal.json` files into the `proposals` table (INSERT OR IGNORE keyed on
11
+ * the UUID, so re-runs never duplicate) and records the stash in
12
+ * `proposal_fs_imports` so later invocations skip the directory walk. The
13
+ * legacy files are left in place untouched — they are inert after import and
14
+ * may be removed by the operator at leisure.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { warn } from "../../core/warn.js";
19
+ import { hasImportedFsProposals, insertProposalIfAbsent, recordFsProposalsImport, } from "../../storage/repositories/proposals-repository.js";
20
+ /** Legacy (pre-0.9.0) proposal directory: `<stashDir>/.akm/proposals[/archive]`. */
21
+ function legacyProposalsRoot(stashDir, archive) {
22
+ const root = path.join(stashDir, ".akm", "proposals");
23
+ return archive ? path.join(root, "archive") : root;
24
+ }
25
+ /**
26
+ * One-shot import of legacy `proposal.json` files into the `proposals` table.
27
+ *
28
+ * Idempotent at two levels: the `proposal_fs_imports` ledger skips the
29
+ * directory walk after the first successful import, and INSERT OR IGNORE
30
+ * (keyed on the proposal UUID) protects against duplicates even if the walk
31
+ * re-runs. Legacy `backup.<ext>` files are inlined into `backupContent` so
32
+ * `akm proposal revert` keeps working for proposals accepted before 0.9.0.
33
+ *
34
+ * The legacy files are never modified or deleted — after import they are
35
+ * inert artifacts the operator can remove at leisure.
36
+ */
37
+ export function importLegacyProposalFiles(db, stashDir) {
38
+ if (hasImportedFsProposals(db, stashDir))
39
+ return;
40
+ const liveRoot = legacyProposalsRoot(stashDir, false);
41
+ if (!fs.existsSync(liveRoot))
42
+ return;
43
+ let imported = 0;
44
+ for (const archive of [false, true]) {
45
+ const root = legacyProposalsRoot(stashDir, archive);
46
+ let entries;
47
+ try {
48
+ entries = fs.readdirSync(root, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ for (const entry of entries) {
54
+ if (!entry.isDirectory() || entry.name === "archive")
55
+ continue;
56
+ const proposalDir = path.join(root, entry.name);
57
+ const proposal = readLegacyProposalFile(proposalDir);
58
+ if (!proposal)
59
+ continue;
60
+ if (insertProposalIfAbsent(db, proposal, stashDir))
61
+ imported += 1;
62
+ }
63
+ }
64
+ recordFsProposalsImport(db, stashDir, imported);
65
+ if (imported > 0) {
66
+ warn(`[proposals] imported ${imported} legacy proposal file(s) from ${liveRoot} into state.db`);
67
+ }
68
+ }
69
+ /**
70
+ * Parse one legacy proposal directory into a {@link Proposal}, inlining the
71
+ * backup file (when present) as `backupContent`. Returns undefined — with a
72
+ * warning — when the `proposal.json` is missing, unreadable, or malformed, so
73
+ * a single corrupt legacy entry never blocks the import of the rest.
74
+ */
75
+ function readLegacyProposalFile(proposalDir) {
76
+ const filePath = path.join(proposalDir, "proposal.json");
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
80
+ }
81
+ catch (err) {
82
+ warn(`[proposals] skipping legacy proposal at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
83
+ return undefined;
84
+ }
85
+ if (typeof parsed !== "object" ||
86
+ parsed === null ||
87
+ typeof parsed.id !== "string" ||
88
+ typeof parsed.ref !== "string") {
89
+ warn(`[proposals] skipping legacy proposal at ${filePath}: not a proposal object`);
90
+ return undefined;
91
+ }
92
+ const { backup, ...rest } = parsed;
93
+ let backupContent;
94
+ if (typeof backup === "string" && backup.length > 0) {
95
+ try {
96
+ backupContent = fs.readFileSync(path.join(proposalDir, backup), "utf8");
97
+ }
98
+ catch {
99
+ // Backup file lost — import the proposal anyway; revert for it will
100
+ // surface "no backup available", same as a new-asset proposal.
101
+ }
102
+ }
103
+ return {
104
+ ...rest,
105
+ payload: {
106
+ content: rest.payload?.content ?? "",
107
+ ...(rest.payload?.frontmatter ? { frontmatter: rest.payload.frontmatter } : {}),
108
+ },
109
+ createdAt: rest.createdAt ?? "",
110
+ updatedAt: rest.updatedAt ?? rest.createdAt ?? "",
111
+ status: rest.status ?? "pending",
112
+ source: rest.source ?? "import",
113
+ ...(backupContent !== undefined ? { backupContent } : {}),
114
+ };
115
+ }
@@ -97,7 +97,7 @@ const proposalAcceptCommand = defineJsonCommand({
97
97
  process.stderr.write("Aborted.\n");
98
98
  return;
99
99
  }
100
- const { listProposals } = await import("./validators/proposals.js");
100
+ const { listProposals } = await import("./repository.js");
101
101
  const stashDir = resolveStashDir();
102
102
  const rawMaxDiff = args["max-diff-lines"] ? Number.parseInt(String(args["max-diff-lines"]), 10) : undefined;
103
103
  if (rawMaxDiff !== undefined && (Number.isNaN(rawMaxDiff) || rawMaxDiff < 0)) {
@@ -191,7 +191,7 @@ const proposalRejectCommand = defineJsonCommand({
191
191
  process.stderr.write("Aborted.\n");
192
192
  return;
193
193
  }
194
- const { listProposals } = await import("./validators/proposals.js");
194
+ const { listProposals } = await import("./repository.js");
195
195
  const stashDir = resolveStashDir();
196
196
  const rawMaxDiff = args["max-diff-lines"] ? Number.parseInt(String(args["max-diff-lines"]), 10) : undefined;
197
197
  if (rawMaxDiff !== undefined && (Number.isNaN(rawMaxDiff) || rawMaxDiff < 0)) {
@@ -390,7 +390,7 @@ const proposalDrainCommand = defineJsonCommand({
390
390
  // second read (engine API owned by another agent — not changed here).
391
391
  let excludeIds;
392
392
  if (olderThanMs !== undefined) {
393
- const { listProposals } = await import("./validators/proposals.js");
393
+ const { listProposals } = await import("./repository.js");
394
394
  const now = Date.now();
395
395
  excludeIds = new Set(listProposals(stashDir, { status: "pending" })
396
396
  // Fail SAFE: exclude a proposal when its age cannot be computed
@@ -15,7 +15,8 @@ import { resolveStashDir } from "../../core/common.js";
15
15
  import { loadConfig } from "../../core/config/config.js";
16
16
  import { UsageError } from "../../core/errors.js";
17
17
  import { appendEvent } from "../../core/events.js";
18
- import { archiveProposal, createProposal, diffProposal, getProposal, isProposalSkipped, listProposals, promoteProposal, resolveProposalId, revertProposal, validateProposal, } from "./validators/proposals.js";
18
+ import { archiveProposal, createProposal, diffProposal, getProposal, isProposalSkipped, listProposals, promoteProposal, resolveProposalId, revertProposal, } from "./repository.js";
19
+ import { validateProposal } from "./validators/proposals.js";
19
20
  // ── Shared helpers ──────────────────────────────────────────────────────────
20
21
  function resolveStash(stashDir) {
21
22
  if (stashDir)
@@ -24,7 +24,7 @@ import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
24
24
  import { buildProposePrompt, parseAgentProposalPayload } from "../../integrations/agent/prompts.js";
25
25
  import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
26
26
  import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "../agent/agent-support.js";
27
- import { createProposal, isProposalSkipped, } from "./validators/proposals.js";
27
+ import { createProposal, isProposalSkipped, } from "./repository.js";
28
28
  function failureEnvelope(result, type, name, fallbackReason = "non_zero_exit") {
29
29
  return {
30
30
  ...baseFailureFields(result, fallbackReason),