akm-cli 0.9.0-beta.52 → 0.9.0-beta.53

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 (42) hide show
  1. package/dist/assets/hints/cli-hints-full.md +6 -5
  2. package/dist/cli.js +0 -7
  3. package/dist/commands/env/env-cli.js +3 -2
  4. package/dist/commands/env/env.js +14 -67
  5. package/dist/commands/health/checks.js +28 -15
  6. package/dist/commands/health.js +68 -1
  7. package/dist/commands/improve/collapse-detector.js +419 -0
  8. package/dist/commands/improve/consolidate.js +72 -54
  9. package/dist/commands/improve/distill.js +79 -13
  10. package/dist/commands/improve/extract.js +13 -6
  11. package/dist/commands/improve/homeostatic.js +109 -79
  12. package/dist/commands/improve/improve-cli.js +67 -1
  13. package/dist/commands/improve/improve.js +10 -0
  14. package/dist/commands/improve/loop-stages.js +39 -1
  15. package/dist/commands/improve/outcome-loop.js +15 -3
  16. package/dist/commands/improve/preparation.js +17 -8
  17. package/dist/commands/improve/salience.js +49 -32
  18. package/dist/commands/read/curate.js +5 -9
  19. package/dist/commands/read/knowledge.js +4 -0
  20. package/dist/commands/read/search.js +5 -2
  21. package/dist/commands/read/show.js +3 -3
  22. package/dist/core/asset/asset-spec.js +3 -2
  23. package/dist/core/config/config-schema.js +39 -17
  24. package/dist/core/eval/rank-metrics.js +113 -0
  25. package/dist/core/state/migrations.js +56 -0
  26. package/dist/core/state-db.js +146 -19
  27. package/dist/indexer/ensure-index.js +33 -90
  28. package/dist/indexer/index-writer-lock.js +0 -11
  29. package/dist/indexer/index-written-assets.js +105 -0
  30. package/dist/indexer/passes/metadata.js +20 -0
  31. package/dist/indexer/search/db-search.js +29 -1
  32. package/dist/indexer/search/ranking-contributors.js +33 -1
  33. package/dist/indexer/search/ranking.js +66 -0
  34. package/dist/indexer/search/search-fields.js +6 -0
  35. package/dist/llm/feature-gate.js +6 -2
  36. package/dist/output/renderers.js +8 -13
  37. package/dist/output/shapes/helpers.js +0 -3
  38. package/dist/output/shapes/passthrough.js +1 -0
  39. package/dist/scripts/migrate-storage.js +152 -33
  40. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +41 -18
  41. package/dist/storage/repositories/index-db.js +10 -1
  42. package/package.json +2 -4
@@ -58,7 +58,7 @@ akm show knowledge:my-doc # Show content (local or remote)
58
58
  | knowledge | `content` (with view modes: `full`, `toc`, `frontmatter`, `section`, `lines`) |
59
59
  | workflow | `workflowTitle`, `workflowParameters`, `steps` |
60
60
  | memory | `content` (recalled context) |
61
- | env | `keys`, `comments` (key names + comments only — values never returned) |
61
+ | env | `keys` (key names only — values and comment text never returned) |
62
62
  | secret | `name` only (the whole file is the value — never returned) |
63
63
  | wiki | `content` (same view modes as knowledge). For any wiki task, run `akm wiki list`. To ingest sources, `akm wiki ingest <name>` dispatches the configured agent (defaults.agent or `--profile`) to execute the ingest workflow. |
64
64
 
@@ -122,15 +122,16 @@ search results. No `--llm` anywhere — akm never reasons about page content.
122
122
  ## Env files
123
123
 
124
124
  A group of related CONFIGURATION for an app/service in one `.env` file at
125
- `<stashDir>/env/<name>.env`, sourced/injected wholesale. Key names + comments
126
- are discoverable; values stay on disk and never reach stdout or the index. akm
127
- does not edit entries — you edit the file with your own editor and akm loads it.
125
+ `<stashDir>/env/<name>.env`, sourced/injected wholesale. Key names are
126
+ discoverable; values and comment text stay on disk and never reach stdout or
127
+ the index (comments can contain commented-out credentials). akm does not edit
128
+ entries — you edit the file with your own editor and akm loads it.
128
129
 
129
130
  ```sh
130
131
  akm env create prod # Create an empty env file
131
132
  akm env create prod --from-file ./.env # Ingest an existing .env
132
133
  akm env list # List all env files across stashes with key names
133
- akm show env:prod # Inspect key names + comments (never values)
134
+ akm show env:prod # Inspect key names (never values or comments)
134
135
  akm env run env:prod -- ./deploy.sh # Run a command with the whole .env injected (the safe path)
135
136
  akm env run env:prod -- $SHELL # Open an interactive shell with values injected
136
137
  akm env export env:prod --out ./env.sh # Write a sourceable script to a file (mode 0600)
package/dist/cli.js CHANGED
@@ -544,13 +544,6 @@ const EXIT_HEALTH_WARN = EXIT_CODES.HEALTH_WARN;
544
544
  // The wrapper sets `AKM_NODE_ENTRY=1` to opt into the startup block. The test
545
545
  // harness never sets it, so importing cli.ts under Bun stays inert as before.
546
546
  if (import.meta.main || process.env.AKM_NODE_ENTRY === "1") {
547
- // Mark that this process is the real akm CLI: its `process.argv[1]` is the
548
- // akm entrypoint, so the background auto-reindex may safely re-invoke it as a
549
- // detached child. Hosts that merely import this module (the in-process test
550
- // harness, library embeddings) never reach this block, so they fall back to
551
- // an inline reindex instead of spawning the wrong program. See
552
- // `ensureIndex` in src/indexer/ensure-index.ts.
553
- process.env.AKM_CLI_ENTRY = "1";
554
547
  // citty reads process.argv directly and does not accept a custom argv array,
555
548
  // so we must replace process.argv with the normalized version before runMain.
556
549
  process.argv = normalizeShowArgv(process.argv);
@@ -10,8 +10,9 @@
10
10
  * copy.
11
11
  *
12
12
  * `akm env` manages whole `.env` files under each stash's env/ directory.
13
- * Values are NEVER written to stdout or structured output — only key NAMES and
14
- * start-of-line comments are surfaced. akm does not manage individual entries;
13
+ * Values and comment text are NEVER written to stdout or structured output —
14
+ * only key NAMES are surfaced (comments routinely contain commented-out
15
+ * credentials). akm does not manage individual entries;
15
16
  * you edit the `.env` file yourself and akm loads it. Replaced the deprecated
16
17
  * `vault` type (removed in 0.9.0).
17
18
  */
@@ -18,10 +18,13 @@
18
18
  * round-trip through `dotenv`; the shell-load safety guarantee still lives on
19
19
  * the READ path (see `buildShellExportScript` + `akm env export`).
20
20
  *
21
- * Invariant: env values must never be written to stdout, returned through the
22
- * indexer, the `akm show` renderer, or any structured output channel. Key
23
- * NAMES and start-of-line comments ARE surfaced by design (discoverability) —
24
- * only values are secret. The supported value-load paths are:
21
+ * Invariant: nothing from an env file except key NAMES may be written to
22
+ * stdout, returned through the indexer, the `akm show` renderer, or any
23
+ * structured output channel. Key NAMES are surfaced for discoverability;
24
+ * comment text is NOT — real .env files routinely carry commented-out
25
+ * `KEY=value` lines and free-text notes containing live credentials, so
26
+ * comments are treated exactly like values. The supported value-load paths
27
+ * are:
25
28
  *
26
29
  * - `akm env run <ref> -- <command>` — values injected into the child
27
30
  * process env (never via a shell), see `injectIntoEnv` / `loadEnv`. This is
@@ -71,75 +74,19 @@ function scanKeys(text) {
71
74
  return keys;
72
75
  }
73
76
  /**
74
- * Scan lines and return start-of-line `#` comments (with the leading `#` and
75
- * any leading whitespace stripped). Inline/trailing `#` after an assignment is
76
- * never extracted.
77
- */
78
- function scanComments(text) {
79
- const comments = [];
80
- for (const line of text.split(/\r?\n/)) {
81
- const trimmed = line.trimStart();
82
- if (trimmed.startsWith("#")) {
83
- comments.push(trimmed.slice(1).trimStart());
84
- }
85
- }
86
- return comments;
87
- }
88
- /**
89
- * Read and return ONLY non-secret metadata (keys + start-of-line comments).
77
+ * Read and return ONLY non-secret metadata: key names.
90
78
  *
91
79
  * The function reads the whole file into memory (same as any dotenv parser)
92
- * but deliberately does not parse values — the LHS-only regex scanners above
93
- * ensure no value content is retained or returned. The guarantee is that
94
- * values never leave this function.
80
+ * but deliberately does not parse values — the LHS-only regex scanner above
81
+ * ensures no value content is retained or returned. Comment text is never
82
+ * returned either: comments routinely contain commented-out `KEY=value`
83
+ * credentials and free-text secrets, so they never leave this function.
95
84
  */
96
85
  export function listKeys(envPath) {
97
86
  if (!fs.existsSync(envPath))
98
- return { keys: [], comments: [] };
99
- const text = fs.readFileSync(envPath, "utf8");
100
- return { keys: scanKeys(text), comments: scanComments(text) };
101
- }
102
- /**
103
- * Return structured `entries` pairing each key with the nearest preceding
104
- * comment line (if any). This is an easier-to-consume shape than the parallel
105
- * `keys[]` + `comments[]` of `listKeys` (QA #35).
106
- *
107
- * Values are never included — the same privacy guarantee as `listKeys`.
108
- */
109
- export function listEntries(envPath) {
110
- if (!fs.existsSync(envPath))
111
- return [];
87
+ return { keys: [] };
112
88
  const text = fs.readFileSync(envPath, "utf8");
113
- const lines = text.split(/\r?\n/);
114
- const seen = new Set();
115
- const entries = [];
116
- let pendingComment;
117
- for (const line of lines) {
118
- const trimmed = line.trimStart();
119
- if (trimmed.startsWith("#")) {
120
- // Capture the most recent comment before a key
121
- pendingComment = trimmed.slice(1).trimStart() || undefined;
122
- continue;
123
- }
124
- const m = line.match(ASSIGN_RE);
125
- if (m) {
126
- const key = m[1];
127
- if (!seen.has(key)) {
128
- seen.add(key);
129
- const entry = { key };
130
- if (pendingComment)
131
- entry.comment = pendingComment;
132
- entries.push(entry);
133
- }
134
- pendingComment = undefined;
135
- }
136
- else {
137
- // Any non-comment, non-assignment line (including blank lines)
138
- // breaks "nearest preceding comment line" association.
139
- pendingComment = undefined;
140
- }
141
- }
142
- return entries;
89
+ return { keys: scanKeys(text) };
143
90
  }
144
91
  /**
145
92
  * Read all KEY=value pairs from an env file. Intended for programmatic callers
@@ -191,21 +191,34 @@ export const HEALTH_CHECKS = [
191
191
  {
192
192
  name: "semantic-search-runtime",
193
193
  channel: "advisory",
194
- run: (ctx) => ({
195
- name: "semantic-search-runtime",
196
- kind: "deterministic",
197
- status: !ctx.semanticStatus ||
198
- ctx.semanticStatus.status === "pending" ||
199
- ctx.semanticStatus.status === "ready-js" ||
200
- ctx.semanticStatus.status === "ready-vec"
201
- ? "pass"
202
- : "warn",
203
- confidence: "medium",
204
- message: ctx.semanticStatus
205
- ? `Semantic search status: ${ctx.semanticStatus.status}`
206
- : "No semantic-search runtime status recorded yet.",
207
- evidence: ctx.semanticStatus ? { ...ctx.semanticStatus } : undefined,
208
- }),
194
+ run: (ctx) => {
195
+ const blocked = ctx.semanticStatus?.status === "blocked";
196
+ // The generic "status: blocked" line is not actionable when the real
197
+ // problem is a configured remote embedding endpoint that is down while
198
+ // semanticSearchMode leaves semantic search enabled — every index run
199
+ // burns time failing against it and searches silently degrade to
200
+ // keyword-only. Name the endpoint and the two ways out.
201
+ const remoteReason = ctx.semanticStatus?.reason?.startsWith("remote-") === true;
202
+ const endpointAdvisory = blocked && remoteReason && ctx.embeddingEndpoint
203
+ ? `Configured embedding endpoint ${ctx.embeddingEndpoint} is failing ` +
204
+ `(${ctx.semanticStatus?.reason}${ctx.semanticStatus?.message ? `: ${ctx.semanticStatus.message}` : ""}) ` +
205
+ `while semanticSearchMode is "${ctx.semanticSearchMode ?? "auto"}". Searches fall back to keyword-only. ` +
206
+ `Restore the endpoint, or set semanticSearchMode to "off" (or remove embedding.endpoint to use the local model).`
207
+ : undefined;
208
+ return {
209
+ name: "semantic-search-runtime",
210
+ kind: "deterministic",
211
+ status: !ctx.semanticStatus || !blocked ? "pass" : "warn",
212
+ confidence: "medium",
213
+ message: endpointAdvisory ??
214
+ (ctx.semanticStatus
215
+ ? `Semantic search status: ${ctx.semanticStatus.status}`
216
+ : "No semantic-search runtime status recorded yet."),
217
+ evidence: ctx.semanticStatus
218
+ ? { ...ctx.semanticStatus, ...(ctx.embeddingEndpoint ? { embeddingEndpoint: ctx.embeddingEndpoint } : {}) }
219
+ : undefined,
220
+ };
221
+ },
209
222
  },
210
223
  {
211
224
  // session-log-failures: demoted to informational — the ERROR_PATTERNS regex
@@ -2,11 +2,12 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
+ import { loadConfig } from "../core/config/config.js";
5
6
  import { ConfigError, UsageError } from "../core/errors.js";
6
7
  import { appendEvent, readEvents } from "../core/events.js";
7
8
  import { buildTaskRunId, getLoggedRunIds, openLogsDatabase } from "../core/logs-db.js";
8
9
  import { getStateDbPathInDataDir } from "../core/paths.js";
9
- import { listExistingTableNames, listProposalGateDecisions, listStateProposals, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
10
+ import { getLatestCycleMetrics, listExistingTableNames, listProposalGateDecisions, listStateProposals, openStateDatabase, queryCompletedTaskIntervals, queryImproveRuns, queryTaskHistory, } from "../core/state-db.js";
10
11
  import { parseSinceToIso } from "../core/time.js";
11
12
  import { readSemanticStatus } from "../indexer/search/semantic-status.js";
12
13
  import { getExecutionLogCandidates } from "../integrations/session-logs/index.js";
@@ -1411,6 +1412,18 @@ export function akmHealth(options = {}) {
1411
1412
  const taskFailRate = taskRows.length === 0 ? 0 : failedTaskRows.length / taskRows.length;
1412
1413
  const agentFailureRate = promptRows.length === 0 ? 0 : promptFailures.length / promptRows.length;
1413
1414
  const semanticStatus = readSemanticStatus();
1415
+ // For the embedding-endpoint advisory. Best-effort: an unloadable config
1416
+ // leaves both undefined and the check falls back to its generic message.
1417
+ let semanticSearchMode;
1418
+ let embeddingEndpoint;
1419
+ try {
1420
+ const config = loadConfig();
1421
+ semanticSearchMode = config.semanticSearchMode;
1422
+ embeddingEndpoint = config.embedding?.endpoint;
1423
+ }
1424
+ catch {
1425
+ // fall through with undefined
1426
+ }
1414
1427
  const improveInvoked = readEvents({ since, type: "improve_invoked" }, { dbPath: stateDbPath }).events.length;
1415
1428
  const improveCompletedEvents = readEvents({ since, type: IMPROVE_COMPLETED_EVENT }, { dbPath: stateDbPath }).events;
1416
1429
  const improveSkippedEvents = readEvents({ since, type: "improve_skipped" }, { dbPath: stateDbPath }).events;
@@ -1453,6 +1466,58 @@ export function akmHealth(options = {}) {
1453
1466
  "The 0.10+ rich in-session outcome signal is no longer deferrable. See plan §WS-2.",
1454
1467
  });
1455
1468
  }
1469
+ // R5 collapse/churn detector: surface any collapse_detector_alert events
1470
+ // in the health window, plus the latest cycle row's headline numbers so
1471
+ // the operator can act without opening the DB. `unknown` when the detector
1472
+ // has never produced a cycle row (no consolidate/recombine work yet).
1473
+ try {
1474
+ // Reuse the already-open state.db handle (readEvents supports a
1475
+ // borrowed connection) — no extra open/migrate/close per health call.
1476
+ const collapseAlertEvents = readEvents({ since, type: "collapse_detector_alert" }, { dbPath: stateDbPath, db }).events;
1477
+ const latestCycle = getLatestCycleMetrics(db);
1478
+ const cycleSummary = latestCycle
1479
+ ? `Latest cycle (${latestCycle.ts}, ${latestCycle.pass}): mean canary recall ${latestCycle.mean_recall.toFixed(3)}, ` +
1480
+ `distinct-content ratio ${latestCycle.distinct_content_ratio.toFixed(3)}, ` +
1481
+ `${latestCycle.accepted_actions} accepted action(s).`
1482
+ : "";
1483
+ if (collapseAlertEvents.length > 0) {
1484
+ const kinds = [...new Set(collapseAlertEvents.map((e) => String(e.metadata?.kind ?? "unknown")))];
1485
+ const collapseKinds = kinds.filter((k) => k.startsWith("collapse"));
1486
+ advisories.push({
1487
+ name: "collapse-churn-detector",
1488
+ status: "warn",
1489
+ kind: "deterministic",
1490
+ // Collapse kinds are measured, not inferred; churn/merge-floor
1491
+ // volume thresholds are still being tuned (design doc §7).
1492
+ confidence: collapseKinds.length > 0 ? "high" : "medium",
1493
+ message: `R5 detector fired ${collapseAlertEvents.length} alert(s) in window (kinds: ${kinds.join(", ")}). ` +
1494
+ `${cycleSummary} See docs/design/improve-collapse-churn-detector-design.md §6.3 runbook queries.`,
1495
+ });
1496
+ }
1497
+ else if (latestCycle) {
1498
+ advisories.push({
1499
+ name: "collapse-churn-detector",
1500
+ status: "pass",
1501
+ kind: "deterministic",
1502
+ confidence: "high",
1503
+ message: `No collapse/churn alerts in window. ${cycleSummary}`,
1504
+ });
1505
+ }
1506
+ else {
1507
+ advisories.push({
1508
+ name: "collapse-churn-detector",
1509
+ status: "unknown",
1510
+ kind: "deterministic",
1511
+ confidence: "high",
1512
+ message: "No detector cycle rows yet — the collapse/churn detector runs only on improve cycles " +
1513
+ "where consolidate/recombine did work (synthesis lanes may be idle).",
1514
+ });
1515
+ }
1516
+ }
1517
+ catch {
1518
+ // Table may predate migration 016 in odd mixed-version setups — advisory
1519
+ // is best-effort and must never fail the health command.
1520
+ }
1456
1521
  let sessionLogEntries = [];
1457
1522
  try {
1458
1523
  const sinceDays = Math.max(0, Math.ceil((now() - new Date(since).getTime()) / (24 * 60 * 60 * 1000)));
@@ -1482,6 +1547,8 @@ export function akmHealth(options = {}) {
1482
1547
  logBackingRate,
1483
1548
  stuckActiveRuns,
1484
1549
  semanticStatus,
1550
+ semanticSearchMode,
1551
+ embeddingEndpoint,
1485
1552
  sessionLogEntries,
1486
1553
  sessionExtraction: improveSummary.sessionExtraction,
1487
1554
  autoAccept: improveSummary.autoAccept,