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

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 (66) hide show
  1. package/dist/assets/hints/cli-hints-full.md +6 -5
  2. package/dist/cli/clack.js +56 -0
  3. package/dist/cli/confirm.js +1 -1
  4. package/dist/cli.js +0 -7
  5. package/dist/commands/env/env-cli.js +3 -2
  6. package/dist/commands/env/env.js +14 -67
  7. package/dist/commands/health/checks.js +28 -15
  8. package/dist/commands/health/html-report.js +33 -10
  9. package/dist/commands/health.js +222 -22
  10. package/dist/commands/improve/collapse-detector.js +419 -0
  11. package/dist/commands/improve/consolidate.js +72 -54
  12. package/dist/commands/improve/distill.js +79 -13
  13. package/dist/commands/improve/extract.js +13 -6
  14. package/dist/commands/improve/homeostatic.js +109 -79
  15. package/dist/commands/improve/improve-cli.js +67 -1
  16. package/dist/commands/improve/improve.js +10 -0
  17. package/dist/commands/improve/loop-stages.js +39 -1
  18. package/dist/commands/improve/outcome-loop.js +33 -19
  19. package/dist/commands/improve/preparation.js +36 -11
  20. package/dist/commands/improve/salience.js +49 -32
  21. package/dist/commands/read/curate.js +9 -13
  22. package/dist/commands/read/knowledge.js +4 -0
  23. package/dist/commands/read/search-cli.js +6 -4
  24. package/dist/commands/read/search.js +12 -5
  25. package/dist/commands/read/show.js +6 -8
  26. package/dist/commands/sources/add-cli.js +1 -1
  27. package/dist/commands/sources/init.js +12 -0
  28. package/dist/commands/sources/stash-cli.js +1 -1
  29. package/dist/commands/tasks/default-tasks.js +12 -0
  30. package/dist/core/asset/asset-spec.js +3 -2
  31. package/dist/core/config/config-schema.js +39 -17
  32. package/dist/core/config/config.js +12 -0
  33. package/dist/core/eval/rank-metrics.js +113 -0
  34. package/dist/core/state/migrations.js +56 -0
  35. package/dist/core/state-db.js +146 -19
  36. package/dist/core/warn.js +21 -0
  37. package/dist/indexer/db/db.js +6 -0
  38. package/dist/indexer/ensure-index.js +36 -92
  39. package/dist/indexer/index-writer-lock.js +9 -11
  40. package/dist/indexer/index-written-assets.js +105 -0
  41. package/dist/indexer/indexer.js +16 -4
  42. package/dist/indexer/passes/metadata.js +20 -0
  43. package/dist/indexer/read-preflight.js +23 -0
  44. package/dist/indexer/search/db-search.js +29 -1
  45. package/dist/indexer/search/ranking-contributors.js +33 -1
  46. package/dist/indexer/search/ranking.js +66 -0
  47. package/dist/indexer/search/search-fields.js +6 -0
  48. package/dist/indexer/walk/walker.js +21 -13
  49. package/dist/integrations/agent/detect.js +9 -0
  50. package/dist/integrations/agent/index.js +1 -1
  51. package/dist/llm/client.js +12 -0
  52. package/dist/llm/embedder.js +26 -2
  53. package/dist/llm/embedders/local.js +7 -1
  54. package/dist/llm/feature-gate.js +6 -2
  55. package/dist/output/renderers.js +8 -13
  56. package/dist/output/shapes/helpers.js +0 -3
  57. package/dist/output/shapes/passthrough.js +1 -0
  58. package/dist/scripts/migrate-storage.js +178 -35
  59. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +46 -19
  60. package/dist/setup/detect.js +9 -0
  61. package/dist/setup/registry-stash-loader.js +12 -0
  62. package/dist/setup/setup.js +1 -1
  63. package/dist/storage/repositories/index-db.js +10 -1
  64. package/dist/tasks/backends/index.js +9 -0
  65. package/dist/tasks/runner.js +9 -0
  66. 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)
@@ -0,0 +1,56 @@
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 { cancel as realCancel, confirm as realConfirm, intro as realIntro, isCancel as realIsCancel, log as realLog, multiselect as realMultiselect, note as realNote, outro as realOutro, select as realSelect, spinner as realSpinner, text as realText, } from "@clack/prompts";
5
+ // ── Test seam ────────────────────────────────────────────────────────────────
6
+ // Swap-and-restore override. Inert in production; only tests call the setter
7
+ // (via tests/_helpers/seams.ts `overrideSeam`, never directly).
8
+ let clackFake;
9
+ /** TEST-ONLY. Swap the clack prompt surface; pass undefined to restore. */
10
+ export function _setClackForTests(fake) {
11
+ clackFake = fake;
12
+ }
13
+ const realFns = {
14
+ intro: realIntro,
15
+ outro: realOutro,
16
+ cancel: realCancel,
17
+ confirm: realConfirm,
18
+ select: realSelect,
19
+ multiselect: realMultiselect,
20
+ text: realText,
21
+ spinner: realSpinner,
22
+ note: realNote,
23
+ isCancel: realIsCancel,
24
+ };
25
+ /** Delegator with the real export's exact type; reads the fake at call time. */
26
+ function bind(name) {
27
+ return ((...args) => {
28
+ const impl = (clackFake?.[name] ?? realFns[name]);
29
+ return impl(...args);
30
+ });
31
+ }
32
+ function bindLog(name) {
33
+ return ((...args) => {
34
+ const impl = (clackFake?.log?.[name] ?? realLog[name]);
35
+ return impl(...args);
36
+ });
37
+ }
38
+ export const intro = bind("intro");
39
+ export const outro = bind("outro");
40
+ export const cancel = bind("cancel");
41
+ export const confirm = bind("confirm");
42
+ export const select = bind("select");
43
+ export const multiselect = bind("multiselect");
44
+ export const text = bind("text");
45
+ export const spinner = bind("spinner");
46
+ export const note = bind("note");
47
+ export const isCancel = bind("isCancel");
48
+ export const log = {
49
+ message: bindLog("message"),
50
+ info: bindLog("info"),
51
+ success: bindLog("success"),
52
+ step: bindLog("step"),
53
+ warn: bindLog("warn"),
54
+ warning: bindLog("warning"),
55
+ error: bindLog("error"),
56
+ };
@@ -35,8 +35,8 @@
35
35
  * `--quiet` NEVER suppresses the confirmation prompt — it is safety-critical
36
36
  * output. The auto-migration banner is similarly exempt from `--quiet`.
37
37
  */
38
- import * as p from "@clack/prompts";
39
38
  import { UsageError } from "../core/errors.js";
39
+ import * as p from "./clack.js";
40
40
  /**
41
41
  * Prompt the user to confirm a destructive action.
42
42
  *
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
@@ -282,6 +282,7 @@ export function buildHealthHtmlReplacements(result, opts) {
282
282
  };
283
283
  const coverage = improve.coverage;
284
284
  const degradation = improve.degradation;
285
+ const minting = improve.enrichmentMinting;
285
286
  // #576: real per-stage LLM token/time accounting (replaces the GPU-time
286
287
  // proxy). Optional-guarded so reports built from older health JSON without
287
288
  // the aggregate still render.
@@ -557,7 +558,7 @@ export function buildHealthHtmlReplacements(result, opts) {
557
558
  "Coverage rate",
558
559
  pct(coverage.rate, 1),
559
560
  "flat",
560
- "Accepted proposals / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
561
+ "Distinct accepted refs / total stash assets (denominator-fixed). Shows what fraction of the corpus has been touched.",
561
562
  ], [
562
563
  "Eligible fraction",
563
564
  pct(coverage.eligibleFraction, 1),
@@ -566,8 +567,22 @@ export function buildHealthHtmlReplacements(result, opts) {
566
567
  ], [
567
568
  "Coverage accepted",
568
569
  num(coverage.acceptedProposals),
569
- "up",
570
- "Total accepted proposals used for the denominator-fixed coverage rate.",
570
+ "flat",
571
+ "Total accepted proposals in the window (raw volume — includes repeated rewrites of the same asset).",
572
+ ], [
573
+ "Churn ratio",
574
+ Number.isFinite(coverage.churnRatio) ? num(coverage.churnRatio) : "—",
575
+ Number.isFinite(coverage.churnRatio) && coverage.churnRatio > 1.5 ? "down" : "flat",
576
+ "Accepted proposals / distinct refs touched. >1.5 = the loop is repeatedly rewriting the same assets (churn, not coverage).",
577
+ ]);
578
+ }
579
+ // Enrichment-vs-minting policy rollup (reporting-only).
580
+ if (minting && Number.isFinite(minting.share)) {
581
+ summaryRows.push([
582
+ "Enrichment-lane minted share",
583
+ pct(minting.share, 1),
584
+ minting.share > 0.05 ? "down" : "flat",
585
+ `New assets minted by enrichment lanes / their accepted total (${minting.minted} minted vs ${minting.updated} updated). Enrichment lanes are ratified to edit existing assets only; WARN >5%, FAIL >15%.`,
571
586
  ]);
572
587
  }
573
588
  // WS-5: perf telemetry rows (only when at least one run reported telemetry).
@@ -606,18 +621,13 @@ export function buildHealthHtmlReplacements(result, opts) {
606
621
  summaryRows.push([
607
622
  "Corpus diversity (Gini)",
608
623
  num(degradation.corpusCentroidDistance),
609
- degradation.entrenchmentFlagged ? "down" : "flat",
610
- "Gini coefficient of retrieval_salience for top-100 ranked assets. High Gini (>0.35) = entrenchment risk.",
624
+ degradation.entrenchmentFlagged || degradation.salienceUniformityFlagged ? "down" : "flat",
625
+ "Gini coefficient of retrieval_salience for top-100 ranked assets. Two-tailed: >0.35 = entrenchment risk; <0.08 = collapsed toward uniform (ranking no longer discriminates).",
611
626
  ], [
612
627
  "Merge fidelity contradiction rate",
613
628
  pct(degradation.mergeFidelityContradictionRate, 1),
614
629
  "flat",
615
630
  "Fraction of consolidated proposals that involved a contradiction, from consolidation result envelopes.",
616
- ], [
617
- "High-generation fraction",
618
- pct(degradation.highGenerationFraction, 1),
619
- "flat",
620
- "Fraction of assets with consecutive_no_ops >= 2 (proxy for high-generation assets in the salience table).",
621
631
  ]);
622
632
  }
623
633
  const summaryRowsHtml = summaryRows
@@ -726,6 +736,19 @@ export function buildHealthHtmlReplacements(result, opts) {
726
736
  remedy: "akm health --format json | jq '.improve.degradation'",
727
737
  });
728
738
  }
739
+ // Low-tail companion: salience distribution collapsed toward uniform.
740
+ if (degradation?.salienceUniformityFlagged) {
741
+ pushItem({
742
+ key: "salience-uniformity-collapse",
743
+ prio: "P2",
744
+ cls: "warn",
745
+ title: "Salience distribution collapsed: retrieval_salience Gini < 0.08",
746
+ descHtml: "The top-100 salience scores are near-uniform (uniform baseline ≈ 0.1) — " +
747
+ "ranking currently carries little to no discrimination between assets. " +
748
+ `Corpus diversity proxy: ${esc(String(degradation.corpusCentroidDistance))}.`,
749
+ remedy: "akm health --format json | jq '.improve.degradation'",
750
+ });
751
+ }
729
752
  // WS-5: over-budget consolidation advisory.
730
753
  if (perf.overBudgetRuns > 0) {
731
754
  pushItem({