akm-cli 0.9.0-beta.0 → 0.9.0-beta.10
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/CHANGELOG.md +511 -0
- package/dist/assets/profiles/quick.json +2 -1
- package/dist/assets/templates/html/default.html +78 -0
- package/dist/assets/templates/html/health.html +732 -0
- package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
- package/dist/cli/shared.js +21 -5
- package/dist/cli.js +47 -5
- package/dist/commands/config-cli.js +0 -10
- package/dist/commands/feedback-cli.js +42 -37
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +666 -0
- package/dist/commands/health.js +186 -13
- package/dist/commands/improve/consolidate.js +39 -6
- package/dist/commands/improve/distill.js +26 -5
- package/dist/commands/improve/extract-prompt.js +1 -1
- package/dist/commands/improve/extract.js +52 -8
- package/dist/commands/improve/improve-auto-accept.js +33 -1
- package/dist/commands/improve/improve-cli.js +7 -0
- package/dist/commands/improve/improve-profiles.js +4 -0
- package/dist/commands/improve/improve.js +877 -433
- package/dist/commands/improve/proactive-maintenance.js +113 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +31 -0
- package/dist/commands/proposal/drain.js +73 -6
- package/dist/commands/proposal/proposal-cli.js +22 -10
- package/dist/commands/proposal/proposal.js +17 -1
- package/dist/commands/proposal/validators/proposals.js +365 -329
- package/dist/commands/read/curate.js +17 -0
- package/dist/commands/remember.js +6 -2
- package/dist/commands/sources/stash-cli.js +10 -2
- package/dist/commands/tasks/tasks.js +32 -8
- package/dist/core/config/config-schema.js +30 -0
- package/dist/core/file-lock.js +22 -0
- package/dist/core/logs-db.js +304 -0
- package/dist/core/paths.js +3 -0
- package/dist/core/state-db.js +152 -14
- package/dist/indexer/db/db.js +99 -13
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +61 -22
- package/dist/integrations/harnesses/claude/session-log.js +17 -5
- package/dist/llm/client.js +38 -4
- package/dist/llm/usage-persist.js +77 -0
- package/dist/llm/usage-telemetry.js +103 -0
- package/dist/output/context.js +3 -2
- package/dist/output/html-render.js +73 -0
- package/dist/output/shapes/helpers.js +17 -1
- package/dist/output/text/helpers.js +69 -1
- package/dist/scripts/migrate-storage.js +153 -25
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +21 -2
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/tasks/backends/cron.js +46 -9
- package/dist/tasks/runner.js +99 -16
- package/dist/workflows/db.js +4 -0
- package/package.json +3 -2
- package/dist/commands/config-edit.js +0 -344
|
@@ -39,9 +39,18 @@ import { chatCompletion } from "../../llm/client.js";
|
|
|
39
39
|
import { isLlmFeatureEnabled, tryLlmFeature } from "../../llm/feature-gate.js";
|
|
40
40
|
import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
|
|
41
41
|
import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt.js";
|
|
42
|
+
import { resolveProcessEnabled } from "./improve-profiles.js";
|
|
42
43
|
import { buildSessionSummaryPrompt, parseSessionSummary, SESSION_SUMMARY_JSON_SCHEMA, sessionMeetsDurationGate, writeSessionAsset, } from "./session-asset.js";
|
|
43
44
|
/** Default minimum session duration (minutes) for session indexing (#561). */
|
|
44
45
|
const DEFAULT_MIN_SESSION_DURATION_MINUTES = 5;
|
|
46
|
+
/**
|
|
47
|
+
* Default minimum raw session size (chars) below which the extract LLM call is
|
|
48
|
+
* skipped (#595/#596). Deliberately tiny: analysis of 218 candidate-producing
|
|
49
|
+
* sessions showed sessions of 22–368 raw chars regularly yield 1–5 candidates,
|
|
50
|
+
* so size is not a reliable proxy for value — only truly empty sessions
|
|
51
|
+
* (0 chars, journal files) are safe to skip.
|
|
52
|
+
*/
|
|
53
|
+
const DEFAULT_MIN_CONTENT_CHARS = 10;
|
|
45
54
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
46
55
|
/**
|
|
47
56
|
* Parse a since-string into an absolute ms-epoch cutoff. Accepts:
|
|
@@ -115,7 +124,7 @@ function buildCandidateProposal(candidate, sourceRef) {
|
|
|
115
124
|
* proposal validation failure) the session result records a warning and
|
|
116
125
|
* keeps going — one session's bad luck never aborts a multi-session run.
|
|
117
126
|
*/
|
|
118
|
-
async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing) {
|
|
127
|
+
async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, sessionIndexing) {
|
|
119
128
|
const warnings = [];
|
|
120
129
|
let data;
|
|
121
130
|
try {
|
|
@@ -136,6 +145,31 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
|
|
|
136
145
|
const filtered = preFilterSession(data, {
|
|
137
146
|
...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
|
|
138
147
|
});
|
|
148
|
+
// #595/#596 — minContentChars gate: skip the LLM call for sessions whose RAW
|
|
149
|
+
// size is below threshold. Measured on the raw event text BEFORE the noise
|
|
150
|
+
// pre-filter, NOT on post-filter output — the pre-filter strips boilerplate
|
|
151
|
+
// so aggressively that even signal-bearing sessions can have tiny output
|
|
152
|
+
// (#596: gating post-filter filtered out 100% of sessions). Note: the 0.8.x
|
|
153
|
+
// fix gated on `filtered.stats.inputCount`, which is an EVENT count, not a
|
|
154
|
+
// char count — this port measures actual raw chars so the threshold matches
|
|
155
|
+
// the config key's documented unit.
|
|
156
|
+
const rawContentChars = data.events.reduce((sum, event) => sum + event.text.length, 0);
|
|
157
|
+
if (minContentChars > 0 && rawContentChars < minContentChars) {
|
|
158
|
+
return {
|
|
159
|
+
sessionId: sessionRef.sessionId,
|
|
160
|
+
harness: harness.name,
|
|
161
|
+
candidateCount: 0,
|
|
162
|
+
proposalIds: [],
|
|
163
|
+
preFilter: {
|
|
164
|
+
inputCount: filtered.stats.inputCount,
|
|
165
|
+
outputCount: filtered.stats.outputCount,
|
|
166
|
+
truncatedCount: filtered.stats.truncatedCount,
|
|
167
|
+
},
|
|
168
|
+
warnings: [],
|
|
169
|
+
skipped: true,
|
|
170
|
+
skipReason: "too_short",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
139
173
|
const prompt = buildExtractPrompt({ data, events: filtered.events, inlineRefs: data.inlineRefs });
|
|
140
174
|
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
141
175
|
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
|
|
@@ -290,13 +324,20 @@ export async function akmExtract(options) {
|
|
|
290
324
|
const stashDir = options.stashDir ?? resolveStashDir();
|
|
291
325
|
const dryRun = options.dryRun ?? false;
|
|
292
326
|
const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
|
|
293
|
-
// Read the per-process extract config from the
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
327
|
+
// Read the per-process extract config + enabled-state from the ACTIVE improve
|
|
328
|
+
// profile when `akmImprove` threaded it; otherwise fall back to the `default`
|
|
329
|
+
// profile (the explicit `akm extract` path, which has no active profile). This
|
|
330
|
+
// is what stops a non-default profile's `extract.enabled` from being silently
|
|
331
|
+
// overridden by the default profile and vice-versa.
|
|
332
|
+
const activeProfile = options.improveProfile;
|
|
333
|
+
const extractProcess = activeProfile
|
|
334
|
+
? activeProfile.processes?.extract
|
|
335
|
+
: config.profiles?.improve?.default?.processes?.extract;
|
|
336
|
+
const extractEnabled = activeProfile
|
|
337
|
+
? resolveProcessEnabled("extract", activeProfile)
|
|
338
|
+
: isLlmFeatureEnabled(config, "session_extraction");
|
|
298
339
|
// Feature-gate early so we get a clean "skipped because disabled" envelope.
|
|
299
|
-
if (!
|
|
340
|
+
if (!extractEnabled) {
|
|
300
341
|
return {
|
|
301
342
|
schemaVersion: 1,
|
|
302
343
|
ok: true,
|
|
@@ -339,6 +380,9 @@ export async function akmExtract(options) {
|
|
|
339
380
|
60_000;
|
|
340
381
|
// Pre-filter budget — process config can raise it for large-context models.
|
|
341
382
|
const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
|
|
383
|
+
// #595/#596 — minimum raw session size; sessions below it skip the LLM call
|
|
384
|
+
// entirely. Set `processes.extract.minContentChars: 0` to disable the gate.
|
|
385
|
+
const minContentChars = typeof extractProcess?.minContentChars === "number" ? extractProcess.minContentChars : DEFAULT_MIN_CONTENT_CHARS;
|
|
342
386
|
// Default discovery window — process config can override the built-in 24h.
|
|
343
387
|
const effectiveSince = options.since ?? extractProcess?.defaultSince;
|
|
344
388
|
// #561 — resolve session-indexing config. Default ON: we only reach this code
|
|
@@ -483,7 +527,7 @@ export async function akmExtract(options) {
|
|
|
483
527
|
continue;
|
|
484
528
|
}
|
|
485
529
|
try {
|
|
486
|
-
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing);
|
|
530
|
+
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, sessionIndexing);
|
|
487
531
|
sessions.push(result);
|
|
488
532
|
if (result.skipped)
|
|
489
533
|
skippedCount += 1;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { loadConfig } from "../../core/config/config.js";
|
|
5
5
|
import { appendEvent } from "../../core/events.js";
|
|
6
6
|
import { info, warn } from "../../core/warn.js";
|
|
7
|
-
import { promoteProposal } from "../proposal/validators/proposals.js";
|
|
7
|
+
import { promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Gate implementation
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
@@ -26,14 +26,40 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
26
26
|
}
|
|
27
27
|
const effectiveThreshold = Math.max(cfg.globalThreshold, cfg.minimumThreshold ?? 0) / 100;
|
|
28
28
|
const resolvedConfig = typeof cfg.config === "function" ? cfg.config() : cfg.config;
|
|
29
|
+
const gateLabel = `improve:${cfg.phase}`;
|
|
30
|
+
// #577: stamp the gate's verdict onto each proposal so `akm proposal show`
|
|
31
|
+
// can explain why a proposal is pending (e.g. "deferred: below-threshold,
|
|
32
|
+
// 0.72 < 0.90"). Best-effort — a recording failure must never abort the gate.
|
|
33
|
+
const stamp = (proposalId, decision) => {
|
|
34
|
+
try {
|
|
35
|
+
recordGateDecision(cfg.stashDir, proposalId, decision);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
warn(`[improve] ${cfg.phase} failed to record gate decision for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
29
41
|
for (const candidate of candidates) {
|
|
30
42
|
const { proposalId, confidence } = candidate;
|
|
31
43
|
if (confidence === undefined || confidence < effectiveThreshold) {
|
|
44
|
+
stamp(proposalId, {
|
|
45
|
+
outcome: "deferred",
|
|
46
|
+
reason: confidence === undefined ? "no-confidence" : "below-threshold",
|
|
47
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
48
|
+
thresholds: { autoAccept: effectiveThreshold },
|
|
49
|
+
gate: gateLabel,
|
|
50
|
+
});
|
|
32
51
|
result.skipped.push(proposalId);
|
|
33
52
|
continue;
|
|
34
53
|
}
|
|
35
54
|
try {
|
|
36
55
|
const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
|
|
56
|
+
stamp(promotion.proposal.id, {
|
|
57
|
+
outcome: "auto-accepted",
|
|
58
|
+
reason: "above-threshold",
|
|
59
|
+
confidence,
|
|
60
|
+
thresholds: { autoAccept: effectiveThreshold },
|
|
61
|
+
gate: gateLabel,
|
|
62
|
+
});
|
|
37
63
|
appendEvent({
|
|
38
64
|
eventType: "promoted",
|
|
39
65
|
ref: promotion.ref,
|
|
@@ -46,6 +72,12 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
46
72
|
confidence,
|
|
47
73
|
threshold: effectiveThreshold,
|
|
48
74
|
phase: cfg.phase,
|
|
75
|
+
// Attribution tagging: carry the eligibility lane from the proposal
|
|
76
|
+
// record onto the auto-accept promoted event so the lane survives to
|
|
77
|
+
// accept time even when promotion happens in a later run.
|
|
78
|
+
...(promotion.proposal.eligibilitySource !== undefined
|
|
79
|
+
? { eligibilitySource: promotion.proposal.eligibilitySource }
|
|
80
|
+
: {}),
|
|
49
81
|
},
|
|
50
82
|
}, cfg.eventsCtx ?? {});
|
|
51
83
|
info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
|
|
@@ -54,6 +54,11 @@ export const improveCommand = defineCommand({
|
|
|
54
54
|
description: "Emit the full JSON result on stdout (legacy behaviour). (0.8.0+: full result is recorded in the improve_runs table of state.db and stdout is empty; use this flag for the prior behaviour, e.g. `akm improve --json-to-stdout | jq`.)",
|
|
55
55
|
default: false,
|
|
56
56
|
},
|
|
57
|
+
"skip-if-locked": {
|
|
58
|
+
type: "boolean",
|
|
59
|
+
description: "If another improve run already holds the lock, skip gracefully (exit 0) instead of failing with 'already running' (exit 78). Use for high-frequency scheduled runs so they don't pile up failures while a longer run is in progress.",
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
57
62
|
profile: {
|
|
58
63
|
type: "string",
|
|
59
64
|
description: "Named improve profile from profiles.improve or built-in profiles (default, quick, thorough, memory-focus, graph-refresh). Controls which sub-processes run and which asset types are processed.",
|
|
@@ -92,6 +97,7 @@ export const improveCommand = defineCommand({
|
|
|
92
97
|
const minRetrievalCountRaw = getHyphenatedArg(args, "min-retrieval-count");
|
|
93
98
|
const minRetrievalCount = parseNonNegativeIntFlag(minRetrievalCountRaw, "--min-retrieval-count");
|
|
94
99
|
const requireFeedbackSignal = getHyphenatedBoolean(args, "require-feedback-signal");
|
|
100
|
+
const skipIfLocked = getHyphenatedBoolean(args, "skip-if-locked");
|
|
95
101
|
const profileArg = getStringArg(args, "profile");
|
|
96
102
|
// Only set the keys the user actually passed (citty leaves the flag
|
|
97
103
|
// undefined unless `--sync`/`--no-sync` / `--push`/`--no-push` appears),
|
|
@@ -189,6 +195,7 @@ export const improveCommand = defineCommand({
|
|
|
189
195
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
190
196
|
...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
|
|
191
197
|
...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
|
|
198
|
+
...(skipIfLocked ? { skipIfLocked } : {}),
|
|
192
199
|
...(profileArg !== undefined ? { profile: profileArg } : {}),
|
|
193
200
|
...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
|
|
194
201
|
consolidateOptions: {
|
|
@@ -54,6 +54,10 @@ const IMPROVE_PROCESS_DEFAULTS = {
|
|
|
54
54
|
// proposal-queue triage drains the standing backlog. Opt-in (default off),
|
|
55
55
|
// like `validation` — needs an explicit `enabled: true`.
|
|
56
56
|
triage: false,
|
|
57
|
+
// Layer 2 proactive-maintenance selector. Opt-in (default off) — surfaces
|
|
58
|
+
// stale high-value assets on a schedule. Enable per-profile with an explicit
|
|
59
|
+
// `processes.proactiveMaintenance.enabled: true`.
|
|
60
|
+
proactiveMaintenance: false,
|
|
57
61
|
};
|
|
58
62
|
/**
|
|
59
63
|
* Compute the effective enabled-state for a named improve process.
|