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.
- package/dist/cli.js +5 -3
- package/dist/commands/agent/contribute-cli.js +2 -3
- package/dist/commands/env/env-cli.js +187 -202
- package/dist/commands/env/secret-cli.js +109 -121
- package/dist/commands/feedback-cli.js +152 -155
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/improve-metrics.js +754 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +18 -0
- package/dist/commands/health/windows.js +196 -0
- package/dist/commands/health.js +14 -1624
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/collapse-detector.js +3 -2
- package/dist/commands/improve/consolidate.js +636 -633
- package/dist/commands/improve/dedup.js +1 -1
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +228 -0
- package/dist/commands/improve/distill/quality-gate.js +233 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill.js +49 -575
- package/dist/commands/improve/extract-cli.js +74 -76
- package/dist/commands/improve/extract.js +6 -4
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +3 -2
- package/dist/commands/improve/improve-cli.js +14 -13
- package/dist/commands/improve/improve-result-file.js +2 -1
- package/dist/commands/improve/improve.js +6 -5
- package/dist/commands/improve/loop-stages.js +19 -21
- package/dist/commands/improve/preparation.js +4 -2
- package/dist/commands/improve/procedural.js +10 -31
- package/dist/commands/improve/recombine.js +19 -43
- package/dist/commands/improve/reflect.js +1 -1
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +48 -0
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +2 -2
- package/dist/commands/proposal/drain.js +1 -1
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +3 -3
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +1 -1
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposals.js +5 -920
- package/dist/commands/read/remember-cli.js +132 -137
- package/dist/commands/read/search-cli.js +1 -1
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/sources/add-cli.js +90 -94
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/schema-repair.js +1 -1
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +1 -1
- package/dist/commands/tasks/tasks-cli.js +1 -2
- package/dist/commands/wiki-cli.js +2 -3
- package/dist/core/common.js +3 -3
- package/dist/core/config/config-schema.js +6 -0
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/events.js +2 -1
- package/dist/core/logs-db.js +8 -13
- package/dist/core/paths.js +14 -14
- package/dist/core/state-db.js +13 -1140
- package/dist/indexer/db/db.js +66 -709
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/feedback/utility-policy.js +85 -0
- package/dist/indexer/graph/graph-extraction.js +2 -1
- package/dist/indexer/index-writer-lock.js +9 -0
- package/dist/indexer/indexer.js +78 -23
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/integrations/agent/spawn.js +15 -66
- package/dist/output/text/helpers.js +13 -0
- package/dist/scripts/migrate-storage.js +6891 -7436
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +44 -43
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +24 -1607
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +130 -0
- package/dist/storage/repositories/index-db.js +4 -7
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +3 -3
- package/dist/tasks/runner.js +2 -1
- package/package.json +1 -1
- 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 {
|
|
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 {
|
|
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 ??
|
|
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 {
|
|
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 {
|
|
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 {
|
|
53
|
-
import {
|
|
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 ??
|
|
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
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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 {
|
|
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(
|
|
94
|
-
const maxDurationMs = parsePositiveIntFlag(
|
|
95
|
-
const maxEvents = parsePositiveIntFlag(
|
|
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 "./
|
|
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/
|
|
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 "./
|
|
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("./
|
|
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("./
|
|
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("./
|
|
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,
|
|
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 "./
|
|
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),
|