akm-cli 0.9.0-beta.1 → 0.9.0-beta.11
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 +492 -0
- 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 +201 -14
- 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 +73 -8
- package/dist/commands/improve/improve-auto-accept.js +63 -3
- package/dist/commands/improve/improve-cli.js +7 -0
- package/dist/commands/improve/improve-profiles.js +4 -0
- package/dist/commands/improve/improve.js +874 -447
- 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 +33 -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 +154 -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,26 @@ 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;
|
|
54
|
+
/**
|
|
55
|
+
* Default cap on NEW sessions the extract pass will LLM-process in a single run
|
|
56
|
+
* (`processes.extract.maxSessionsPerRun` overrides; `0` disables). Bounds per-run
|
|
57
|
+
* wall time + token spend so a backlog of accumulated sessions can't run a single
|
|
58
|
+
* pass past its scheduled-task timeout. Overflow sessions stay unseen and are
|
|
59
|
+
* processed by subsequent runs, so coverage is preserved — just spread out.
|
|
60
|
+
*/
|
|
61
|
+
const DEFAULT_MAX_SESSIONS_PER_RUN = 25;
|
|
45
62
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
46
63
|
/**
|
|
47
64
|
* Parse a since-string into an absolute ms-epoch cutoff. Accepts:
|
|
@@ -115,7 +132,7 @@ function buildCandidateProposal(candidate, sourceRef) {
|
|
|
115
132
|
* proposal validation failure) the session result records a warning and
|
|
116
133
|
* keeps going — one session's bad luck never aborts a multi-session run.
|
|
117
134
|
*/
|
|
118
|
-
async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing) {
|
|
135
|
+
async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, sessionIndexing) {
|
|
119
136
|
const warnings = [];
|
|
120
137
|
let data;
|
|
121
138
|
try {
|
|
@@ -136,6 +153,31 @@ async function processSession(harness, sessionRef, stashDir, config, llmConfig,
|
|
|
136
153
|
const filtered = preFilterSession(data, {
|
|
137
154
|
...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
|
|
138
155
|
});
|
|
156
|
+
// #595/#596 — minContentChars gate: skip the LLM call for sessions whose RAW
|
|
157
|
+
// size is below threshold. Measured on the raw event text BEFORE the noise
|
|
158
|
+
// pre-filter, NOT on post-filter output — the pre-filter strips boilerplate
|
|
159
|
+
// so aggressively that even signal-bearing sessions can have tiny output
|
|
160
|
+
// (#596: gating post-filter filtered out 100% of sessions). Note: the 0.8.x
|
|
161
|
+
// fix gated on `filtered.stats.inputCount`, which is an EVENT count, not a
|
|
162
|
+
// char count — this port measures actual raw chars so the threshold matches
|
|
163
|
+
// the config key's documented unit.
|
|
164
|
+
const rawContentChars = data.events.reduce((sum, event) => sum + event.text.length, 0);
|
|
165
|
+
if (minContentChars > 0 && rawContentChars < minContentChars) {
|
|
166
|
+
return {
|
|
167
|
+
sessionId: sessionRef.sessionId,
|
|
168
|
+
harness: harness.name,
|
|
169
|
+
candidateCount: 0,
|
|
170
|
+
proposalIds: [],
|
|
171
|
+
preFilter: {
|
|
172
|
+
inputCount: filtered.stats.inputCount,
|
|
173
|
+
outputCount: filtered.stats.outputCount,
|
|
174
|
+
truncatedCount: filtered.stats.truncatedCount,
|
|
175
|
+
},
|
|
176
|
+
warnings: [],
|
|
177
|
+
skipped: true,
|
|
178
|
+
skipReason: "too_short",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
139
181
|
const prompt = buildExtractPrompt({ data, events: filtered.events, inlineRefs: data.inlineRefs });
|
|
140
182
|
// #561 — ADDITIVE session indexing. Generate + write the session asset
|
|
141
183
|
// (`sessions/<harness>/<id>.md`). FAIL-OPEN: any failure only records a
|
|
@@ -290,13 +332,20 @@ export async function akmExtract(options) {
|
|
|
290
332
|
const stashDir = options.stashDir ?? resolveStashDir();
|
|
291
333
|
const dryRun = options.dryRun ?? false;
|
|
292
334
|
const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
|
|
293
|
-
// Read the per-process extract config from the
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
335
|
+
// Read the per-process extract config + enabled-state from the ACTIVE improve
|
|
336
|
+
// profile when `akmImprove` threaded it; otherwise fall back to the `default`
|
|
337
|
+
// profile (the explicit `akm extract` path, which has no active profile). This
|
|
338
|
+
// is what stops a non-default profile's `extract.enabled` from being silently
|
|
339
|
+
// overridden by the default profile and vice-versa.
|
|
340
|
+
const activeProfile = options.improveProfile;
|
|
341
|
+
const extractProcess = activeProfile
|
|
342
|
+
? activeProfile.processes?.extract
|
|
343
|
+
: config.profiles?.improve?.default?.processes?.extract;
|
|
344
|
+
const extractEnabled = activeProfile
|
|
345
|
+
? resolveProcessEnabled("extract", activeProfile)
|
|
346
|
+
: isLlmFeatureEnabled(config, "session_extraction");
|
|
298
347
|
// Feature-gate early so we get a clean "skipped because disabled" envelope.
|
|
299
|
-
if (!
|
|
348
|
+
if (!extractEnabled) {
|
|
300
349
|
return {
|
|
301
350
|
schemaVersion: 1,
|
|
302
351
|
ok: true,
|
|
@@ -339,6 +388,15 @@ export async function akmExtract(options) {
|
|
|
339
388
|
60_000;
|
|
340
389
|
// Pre-filter budget — process config can raise it for large-context models.
|
|
341
390
|
const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
|
|
391
|
+
// #595/#596 — minimum raw session size; sessions below it skip the LLM call
|
|
392
|
+
// entirely. Set `processes.extract.minContentChars: 0` to disable the gate.
|
|
393
|
+
const minContentChars = typeof extractProcess?.minContentChars === "number" ? extractProcess.minContentChars : DEFAULT_MIN_CONTENT_CHARS;
|
|
394
|
+
// Cap on NEW sessions LLM-processed per run; 0 disables. Absent = default.
|
|
395
|
+
// Bounds per-run wall time / LLM cost so a backlog can't push a run past its
|
|
396
|
+
// task timeout — the overflow stays unseen and is picked up by later runs.
|
|
397
|
+
const maxSessionsPerRun = typeof extractProcess?.maxSessionsPerRun === "number"
|
|
398
|
+
? extractProcess.maxSessionsPerRun
|
|
399
|
+
: DEFAULT_MAX_SESSIONS_PER_RUN;
|
|
342
400
|
// Default discovery window — process config can override the built-in 24h.
|
|
343
401
|
const effectiveSince = options.since ?? extractProcess?.defaultSince;
|
|
344
402
|
// #561 — resolve session-indexing config. Default ON: we only reach this code
|
|
@@ -482,8 +540,15 @@ export async function akmExtract(options) {
|
|
|
482
540
|
skippedCount += 1;
|
|
483
541
|
continue;
|
|
484
542
|
}
|
|
543
|
+
// Per-run cap on LLM-processed sessions (skip-tracked seen sessions above
|
|
544
|
+
// don't count). Single-session / --force modes bypass the cap (explicit
|
|
545
|
+
// intent). Overflow sessions are left unseen for the next run.
|
|
546
|
+
if (!options.sessionId && !options.force && maxSessionsPerRun > 0 && processedCount >= maxSessionsPerRun) {
|
|
547
|
+
topLevelWarnings.push(`Reached maxSessionsPerRun=${maxSessionsPerRun}; ${candidates.length - processedCount - skippedCount} session(s) deferred to a later run.`);
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
485
550
|
try {
|
|
486
|
-
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, sessionIndexing);
|
|
551
|
+
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars, minContentChars, sessionIndexing);
|
|
487
552
|
sessions.push(result);
|
|
488
553
|
if (result.skipped)
|
|
489
554
|
skippedCount += 1;
|
|
@@ -4,7 +4,24 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Derive a stable, low-cardinality reason bucket from an auto-accept promotion
|
|
10
|
+
* error. `promoteProposal` throws a `validateProposal` report formatted as
|
|
11
|
+
* `[kind] message` lines; we extract the first finding kind. Non-validation
|
|
12
|
+
* throws collapse to `promote-error`.
|
|
13
|
+
*/
|
|
14
|
+
function classifyPromoteFailure(err) {
|
|
15
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
16
|
+
const finding = /\[([a-z][a-z0-9-]*)\]/i.exec(message);
|
|
17
|
+
if (finding)
|
|
18
|
+
return `validation:${finding[1]}`;
|
|
19
|
+
if (/not pending/i.test(message))
|
|
20
|
+
return "not-pending";
|
|
21
|
+
if (/unknown asset type/i.test(message))
|
|
22
|
+
return "unknown-type";
|
|
23
|
+
return "promote-error";
|
|
24
|
+
}
|
|
8
25
|
// ---------------------------------------------------------------------------
|
|
9
26
|
// Gate implementation
|
|
10
27
|
// ---------------------------------------------------------------------------
|
|
@@ -18,7 +35,7 @@ import { promoteProposal } from "../proposal/validators/proposals.js";
|
|
|
18
35
|
* @param promoteFn Injectable override for `promoteProposal` (test seam).
|
|
19
36
|
*/
|
|
20
37
|
export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProposal) {
|
|
21
|
-
const result = { promoted: [], skipped: [], failed: [] };
|
|
38
|
+
const result = { promoted: [], skipped: [], failed: [], failedByReason: {} };
|
|
22
39
|
// --- Guard: gate is disabled or context is incomplete ---
|
|
23
40
|
if (cfg.dryRun || cfg.globalThreshold === undefined || !cfg.stashDir) {
|
|
24
41
|
result.skipped = candidates.map((c) => c.proposalId);
|
|
@@ -26,14 +43,40 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
26
43
|
}
|
|
27
44
|
const effectiveThreshold = Math.max(cfg.globalThreshold, cfg.minimumThreshold ?? 0) / 100;
|
|
28
45
|
const resolvedConfig = typeof cfg.config === "function" ? cfg.config() : cfg.config;
|
|
46
|
+
const gateLabel = `improve:${cfg.phase}`;
|
|
47
|
+
// #577: stamp the gate's verdict onto each proposal so `akm proposal show`
|
|
48
|
+
// can explain why a proposal is pending (e.g. "deferred: below-threshold,
|
|
49
|
+
// 0.72 < 0.90"). Best-effort — a recording failure must never abort the gate.
|
|
50
|
+
const stamp = (proposalId, decision) => {
|
|
51
|
+
try {
|
|
52
|
+
recordGateDecision(cfg.stashDir, proposalId, decision);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
warn(`[improve] ${cfg.phase} failed to record gate decision for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
29
58
|
for (const candidate of candidates) {
|
|
30
59
|
const { proposalId, confidence } = candidate;
|
|
31
60
|
if (confidence === undefined || confidence < effectiveThreshold) {
|
|
61
|
+
stamp(proposalId, {
|
|
62
|
+
outcome: "deferred",
|
|
63
|
+
reason: confidence === undefined ? "no-confidence" : "below-threshold",
|
|
64
|
+
...(confidence !== undefined ? { confidence } : {}),
|
|
65
|
+
thresholds: { autoAccept: effectiveThreshold },
|
|
66
|
+
gate: gateLabel,
|
|
67
|
+
});
|
|
32
68
|
result.skipped.push(proposalId);
|
|
33
69
|
continue;
|
|
34
70
|
}
|
|
35
71
|
try {
|
|
36
72
|
const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
|
|
73
|
+
stamp(promotion.proposal.id, {
|
|
74
|
+
outcome: "auto-accepted",
|
|
75
|
+
reason: "above-threshold",
|
|
76
|
+
confidence,
|
|
77
|
+
thresholds: { autoAccept: effectiveThreshold },
|
|
78
|
+
gate: gateLabel,
|
|
79
|
+
});
|
|
37
80
|
appendEvent({
|
|
38
81
|
eventType: "promoted",
|
|
39
82
|
ref: promotion.ref,
|
|
@@ -46,14 +89,31 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
|
|
|
46
89
|
confidence,
|
|
47
90
|
threshold: effectiveThreshold,
|
|
48
91
|
phase: cfg.phase,
|
|
92
|
+
// Attribution tagging: carry the eligibility lane from the proposal
|
|
93
|
+
// record onto the auto-accept promoted event so the lane survives to
|
|
94
|
+
// accept time even when promotion happens in a later run.
|
|
95
|
+
...(promotion.proposal.eligibilitySource !== undefined
|
|
96
|
+
? { eligibilitySource: promotion.proposal.eligibilitySource }
|
|
97
|
+
: {}),
|
|
49
98
|
},
|
|
50
99
|
}, cfg.eventsCtx ?? {});
|
|
51
100
|
info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
|
|
52
101
|
result.promoted.push(proposalId);
|
|
53
102
|
}
|
|
54
103
|
catch (err) {
|
|
55
|
-
|
|
104
|
+
const reason = classifyPromoteFailure(err);
|
|
105
|
+
warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId} (${reason}): ${err instanceof Error ? err.message : String(err)}`);
|
|
56
106
|
result.failed.push(proposalId);
|
|
107
|
+
result.failedByReason[reason] = (result.failedByReason[reason] ?? 0) + 1;
|
|
108
|
+
// Record WHY on the proposal so `akm proposal show` explains the rejection
|
|
109
|
+
// and the leak is no longer blind. Best-effort.
|
|
110
|
+
stamp(proposalId, {
|
|
111
|
+
outcome: "auto-rejected",
|
|
112
|
+
reason,
|
|
113
|
+
confidence,
|
|
114
|
+
thresholds: { autoAccept: effectiveThreshold },
|
|
115
|
+
gate: gateLabel,
|
|
116
|
+
});
|
|
57
117
|
}
|
|
58
118
|
}
|
|
59
119
|
return result;
|
|
@@ -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.
|