akm-cli 0.9.0-beta.1 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +469 -0
  2. package/dist/assets/templates/html/default.html +78 -0
  3. package/dist/assets/templates/html/health.html +732 -0
  4. package/dist/assets/templates/html/vendor/echarts.min.js +45 -0
  5. package/dist/cli/shared.js +21 -5
  6. package/dist/cli.js +47 -5
  7. package/dist/commands/config-cli.js +0 -10
  8. package/dist/commands/feedback-cli.js +42 -37
  9. package/dist/commands/graph/graph.js +75 -71
  10. package/dist/commands/health/checks.js +48 -0
  11. package/dist/commands/health/html-report.js +666 -0
  12. package/dist/commands/health.js +186 -13
  13. package/dist/commands/improve/consolidate.js +39 -6
  14. package/dist/commands/improve/distill.js +26 -5
  15. package/dist/commands/improve/extract-prompt.js +1 -1
  16. package/dist/commands/improve/extract.js +52 -8
  17. package/dist/commands/improve/improve-auto-accept.js +33 -1
  18. package/dist/commands/improve/improve-cli.js +7 -0
  19. package/dist/commands/improve/improve-profiles.js +4 -0
  20. package/dist/commands/improve/improve.js +874 -447
  21. package/dist/commands/improve/proactive-maintenance.js +113 -0
  22. package/dist/commands/improve/reflect-noise.js +0 -0
  23. package/dist/commands/improve/reflect.js +31 -0
  24. package/dist/commands/proposal/drain.js +73 -6
  25. package/dist/commands/proposal/proposal-cli.js +22 -10
  26. package/dist/commands/proposal/proposal.js +17 -1
  27. package/dist/commands/proposal/validators/proposals.js +365 -329
  28. package/dist/commands/read/curate.js +17 -0
  29. package/dist/commands/remember.js +6 -2
  30. package/dist/commands/sources/stash-cli.js +10 -2
  31. package/dist/commands/tasks/tasks.js +32 -8
  32. package/dist/core/config/config-schema.js +30 -0
  33. package/dist/core/logs-db.js +304 -0
  34. package/dist/core/paths.js +3 -0
  35. package/dist/core/state-db.js +152 -14
  36. package/dist/indexer/db/db.js +99 -13
  37. package/dist/indexer/ensure-index.js +152 -17
  38. package/dist/indexer/index-writer-lock.js +99 -0
  39. package/dist/indexer/indexer.js +114 -111
  40. package/dist/indexer/passes/memory-inference.js +61 -22
  41. package/dist/integrations/harnesses/claude/session-log.js +17 -5
  42. package/dist/llm/client.js +38 -4
  43. package/dist/llm/usage-persist.js +77 -0
  44. package/dist/llm/usage-telemetry.js +103 -0
  45. package/dist/output/context.js +3 -2
  46. package/dist/output/html-render.js +73 -0
  47. package/dist/output/shapes/helpers.js +17 -1
  48. package/dist/output/text/helpers.js +69 -1
  49. package/dist/scripts/migrate-storage.js +153 -25
  50. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +21 -2
  51. package/dist/sources/providers/tar-utils.js +16 -8
  52. package/dist/tasks/backends/cron.js +46 -9
  53. package/dist/tasks/runner.js +99 -16
  54. package/dist/workflows/db.js +4 -0
  55. package/package.json +3 -2
  56. 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 active improve profile. Matches
294
- // the pattern reflect/distill/consolidate use: `profiles.improve.<active>.processes.extract`.
295
- // Only the `default` improve profile is consulted here extract isn't invoked
296
- // with a profile flag yet (parity item for a future change).
297
- const extractProcess = config.profiles?.improve?.default?.processes?.extract;
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 (!isLlmFeatureEnabled(config, "session_extraction")) {
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.