akm-cli 0.9.17-alpha.7 → 0.9.17-alpha.9

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 (116) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/STABILITY.md +9 -8
  3. package/dist/akm +55 -22
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/hints/cli-hints-full.md +6 -7
  6. package/dist/assets/improve-strategies/catchup.json +0 -3
  7. package/dist/assets/improve-strategies/consolidate.json +0 -1
  8. package/dist/assets/improve-strategies/default.json +1 -2
  9. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
  10. package/dist/assets/improve-strategies/quick.json +1 -2
  11. package/dist/assets/improve-strategies/reflect-distill.json +1 -2
  12. package/dist/assets/improve-strategies/thorough.json +0 -3
  13. package/dist/assets/prompts/consolidate-pair.md +20 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
  15. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
  16. package/dist/assets/templates/html/health.html +3 -5
  17. package/dist/cli/retired-commands.js +1 -1
  18. package/dist/commands/health/archive-usage.js +98 -0
  19. package/dist/commands/health/data-dir-usage.js +25 -13
  20. package/dist/commands/health/html-report.js +1 -4
  21. package/dist/commands/health/improve-metrics.js +0 -25
  22. package/dist/commands/health/md-report.js +1 -6
  23. package/dist/commands/health/report-view-model.js +4 -14
  24. package/dist/commands/health/windows.js +0 -1
  25. package/dist/commands/health.js +13 -0
  26. package/dist/commands/improve/consolidate/continuity-check.js +137 -0
  27. package/dist/commands/improve/consolidate/pair-pass.js +791 -0
  28. package/dist/commands/improve/consolidate.js +38 -63
  29. package/dist/commands/improve/extract-prompt.js +1 -2
  30. package/dist/commands/improve/improve-cli.js +1 -1
  31. package/dist/commands/improve/improve-strategies.js +23 -5
  32. package/dist/commands/improve/improve.js +19 -30
  33. package/dist/commands/improve/ledger.js +3 -2
  34. package/dist/commands/improve/loop-stages.js +5 -84
  35. package/dist/commands/improve/memory/memory-belief.js +3 -1
  36. package/dist/commands/improve/memory/memory-improve.js +269 -11
  37. package/dist/commands/improve/planner.js +0 -5
  38. package/dist/commands/improve/preparation.js +20 -135
  39. package/dist/commands/improve/retrieval-scope.js +19 -4
  40. package/dist/commands/improve/salience.js +1 -14
  41. package/dist/commands/improve/stage.js +0 -1
  42. package/dist/commands/lint/base-linter.js +19 -11
  43. package/dist/commands/proposal/drain.js +8 -1
  44. package/dist/commands/proposal/proposal-cli.js +16 -2
  45. package/dist/commands/proposal/proposal-types.js +7 -0
  46. package/dist/commands/proposal/proposal.js +37 -6
  47. package/dist/commands/proposal/repository.js +613 -4
  48. package/dist/commands/proposal/validators/proposals.js +9 -0
  49. package/dist/commands/read/curate.js +40 -13
  50. package/dist/commands/read/knowledge.js +3 -2
  51. package/dist/commands/read/show.js +55 -16
  52. package/dist/commands/sources/info.js +3 -0
  53. package/dist/commands/sources/stash-cli.js +2 -2
  54. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  55. package/dist/core/adapter/adapters/akm-metadata.js +31 -0
  56. package/dist/core/bundle-rename.js +1 -7
  57. package/dist/core/config/config-schema.js +8 -1
  58. package/dist/core/config/config.js +23 -48
  59. package/dist/core/config/engine-semantics.js +0 -2
  60. package/dist/core/config/schema/improve-processes.js +17 -42
  61. package/dist/core/config/schema/index-config.js +5 -25
  62. package/dist/core/file-change.js +13 -5
  63. package/dist/core/improve-result.js +16 -5
  64. package/dist/core/improve-types.js +0 -1
  65. package/dist/core/loopback.js +7 -12
  66. package/dist/core/parse.js +13 -16
  67. package/dist/core/state/migrations.js +15 -0
  68. package/dist/core/time.js +0 -20
  69. package/dist/indexer/db/llm-cache.js +2 -2
  70. package/dist/indexer/ensure-index.js +2 -2
  71. package/dist/indexer/index-written-assets.js +2 -3
  72. package/dist/indexer/indexer.js +18 -418
  73. package/dist/indexer/links/declared-links.js +90 -0
  74. package/dist/indexer/passes/metadata.js +0 -19
  75. package/dist/indexer/scan/doc-to-entry.js +1 -0
  76. package/dist/indexer/walk/walker.js +3 -4
  77. package/dist/llm/client.js +8 -10
  78. package/dist/llm/embedders/remote.js +1 -2
  79. package/dist/llm/feature-gate.js +0 -5
  80. package/dist/output/shapes/helpers.js +23 -4
  81. package/dist/output/text/command-format.js +0 -8
  82. package/dist/output/text/proposal-format.js +47 -1
  83. package/dist/output/text/show-format.js +13 -17
  84. package/dist/scripts/akm-migrate-node.js +2754 -2836
  85. package/dist/scripts/akm-migrate.js +2754 -2836
  86. package/dist/setup/steps/connection.js +5 -6
  87. package/dist/setup/steps/platforms.js +2 -2
  88. package/dist/sources/providers/git-stash.js +55 -4
  89. package/dist/storage/repositories/improve-ledger-repository.js +48 -7
  90. package/dist/storage/repositories/index-entries-repository.js +16 -13
  91. package/dist/storage/repositories/index-entry-schema.js +22 -3
  92. package/dist/storage/repositories/index-links-repository.js +143 -0
  93. package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
  94. package/dist/storage/repositories/index-schema.js +82 -104
  95. package/dist/storage/repositories/proposals-repository.js +61 -0
  96. package/dist/storage/repositories/salience-repository.js +1 -19
  97. package/dist/tasks/source/task-to-v4.js +462 -74
  98. package/docs/migration/release-notes/0.9.17.md +7 -5
  99. package/docs/reference/cli.md +33 -21
  100. package/docs/reference/configuration.md +21 -12
  101. package/docs/reference/data-and-telemetry.md +0 -1
  102. package/package.json +1 -1
  103. package/schemas/akm-config.json +0 -342
  104. package/dist/assets/improve-strategies/graph-refresh.json +0 -15
  105. package/dist/assets/prompts/contradiction-judge.md +0 -33
  106. package/dist/assets/prompts/graph-extract-system.md +0 -1
  107. package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
  108. package/dist/assets/prompts/metadata-enhance-system.md +0 -1
  109. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
  110. package/dist/indexer/db/graph-db.js +0 -431
  111. package/dist/indexer/graph/graph-extraction.js +0 -807
  112. package/dist/indexer/graph/graph-related.js +0 -131
  113. package/dist/indexer/graph/graph-types.js +0 -4
  114. package/dist/llm/graph-extract.js +0 -903
  115. package/dist/llm/metadata-enhance.js +0 -95
  116. package/dist/tasks/source/task-to-v3.js +0 -453
package/dist/akm-migrate CHANGED
@@ -6,6 +6,40 @@
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
+ // #956 round 3: install the signal-forwarding scaffolding before ANYTHING
10
+ // else in this process — the Node-version check, the bun-version probe,
11
+ // spawning the child — gets a chance to run. See the matching fix and
12
+ // comment in scripts/node-runtime/akm: the three `process.once` listeners
13
+ // used to go up only after spawn() (below) returned; a scheduler preemption
14
+ // right after that syscall was enough for a signal to arrive with no
15
+ // listener yet, and the runtime's default (process-terminating) disposition
16
+ // killed the launcher outright, never reaching the child at all. `child`
17
+ // starts unset: a signal that arrives before it exists is queued and
18
+ // flushed the moment it does.
19
+ let child;
20
+ let childExited = false;
21
+ const pendingSignals = [];
22
+ const forwardSignal = (signal) => {
23
+ if (childExited) return;
24
+ if (!child) {
25
+ pendingSignals.push(signal);
26
+ return;
27
+ }
28
+ try {
29
+ child.kill(signal);
30
+ } catch {
31
+ // Child exited in the race between the check above and here.
32
+ }
33
+ };
34
+ // `.once`, not `.on`: see the matching comment in scripts/node-runtime/akm —
35
+ // a persistent `.on` listener would still be registered when the
36
+ // `process.kill(process.pid, result.signal)` re-raise below runs,
37
+ // suppressing the OS default disposition and leaving this launcher exiting
38
+ // 0 instead of reflecting the child's signal.
39
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
40
+ process.once(signal, () => forwardSignal(signal));
41
+ }
42
+
9
43
  if (!process.versions.bun) {
10
44
  const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
11
45
  if (major < 22) {
@@ -34,7 +68,7 @@ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.
34
68
  const command = process.versions.bun ? process.execPath : useBun ? "bun" : process.execPath;
35
69
  const entry = process.versions.bun || useBun ? bunEntry : nodeEntry;
36
70
  const result = await new Promise((resolve) => {
37
- const child = spawn(command, [entry, ...process.argv.slice(2)], {
71
+ child = spawn(command, [entry, ...process.argv.slice(2)], {
38
72
  stdio: "inherit",
39
73
  env,
40
74
  // #956: own process group on POSIX so the launcher's forward below is
@@ -43,27 +77,12 @@ const nodeEntry = fileURLToPath(new URL("./scripts/akm-migrate-node.js", import.
43
77
  // directly — see the matching comment in scripts/node-runtime/akm.
44
78
  detached: process.platform !== "win32",
45
79
  });
46
- let childExited = false;
47
80
  child.once("exit", () => {
48
81
  childExited = true;
49
82
  });
50
- const forwardSignal = (signal) => {
51
- if (childExited) return;
52
- try {
53
- child.kill(signal);
54
- } catch {
55
- // Child exited in the race between the check above and here.
56
- }
57
- };
58
- // `.once`, not `.on`: see the matching comment in
59
- // scripts/node-runtime/akm — a persistent `.on` listener would still be
60
- // registered when the `process.kill(process.pid, result.signal)`
61
- // re-raise below runs, suppressing the OS default disposition and
62
- // leaving this launcher exiting 0 instead of reflecting the child's
63
- // signal.
64
- for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
65
- process.once(signal, () => forwardSignal(signal));
66
- }
83
+ // Flush whatever arrived in the (now much smaller) window between the
84
+ // listeners going up and the child existing to receive them.
85
+ for (const signal of pendingSignals.splice(0)) forwardSignal(signal);
67
86
  child.once("error", (error) => resolve({ error }));
68
87
  child.once("exit", (code, signal) => resolve({ code, signal }));
69
88
  });
@@ -245,10 +245,9 @@ akm improve --no-push # commit but skip push for this ru
245
245
  akm improve --sync # force sync even on strategies that disable it
246
246
  ```
247
247
 
248
- Strategy sync defaults: `catchup`, `consolidate`, `default`,
249
- `graph-refresh`, `quick`, and `thorough` auto-commit + push;
250
- `proactive-maintenance` and `reflect-distill` skip sync entirely. Override
251
- with `--sync` / `--no-sync` flags.
248
+ Strategy sync defaults: `catchup`, `consolidate`, `default`, `quick`, and
249
+ `thorough` auto-commit + push; `proactive-maintenance` and `reflect-distill`
250
+ skip sync entirely. Override with `--sync` / `--no-sync` flags.
252
251
 
253
252
  The `--writable` flag on `akm bundle add` opts a remote git bundle into push-on-sync:
254
253
 
@@ -308,8 +307,8 @@ akm bundle create # Initialize working bund
308
307
  akm setup # Interactive wizard: bundle + LLM/embedding + agent + registry config
309
308
  akm setup --dir ~/custom-bundle # Run the wizard against a custom bundle path
310
309
  akm setup --yes # Non-interactive, accepts all defaults
311
- akm index # Rebuild search index (metadata enrichment when configured)
312
- akm index --full # Full reindex (metadata enrichment when configured)
310
+ akm index # Rebuild search index
311
+ akm index --full # Full reindex
313
312
  akm bundle list # List all sources
314
313
  akm lint # Structural lint over the bundle; exits 0 regardless of findings
315
314
  akm lint --fix # Auto-fix Tier 1 issues
@@ -399,7 +398,7 @@ akm agent --model sonnet --prompt "..." # Model override (aliases or exa
399
398
  ```sh
400
399
  akm info # Capabilities, bundle dir, index stats, semantic-search status
401
400
  akm health # Runtime diagnostics; exit 0 ok / 4 warn / 1 fail
402
- akm health --report # Adds accept-rate and graph-coverage metrics
401
+ akm health --report # Adds accept-rate metrics
403
402
  akm log # Append-only event stream (mutations, feedback, indexing)
404
403
  akm log --ref <ref> # One asset's event trail
405
404
  akm log --since @offset:<id> # Durable row-id cursor — poll this to follow the stream
@@ -16,9 +16,6 @@
16
16
  "memoryInference": {
17
17
  "enabled": false
18
18
  },
19
- "graphExtraction": {
20
- "enabled": false
21
- },
22
19
  "extract": {
23
20
  "enabled": false
24
21
  },
@@ -5,7 +5,6 @@
5
5
  "distill": { "enabled": false },
6
6
  "consolidate": { "enabled": true, "allowedTypes": ["memory"], "maxChunkSize": 25 },
7
7
  "memoryInference": { "enabled": false },
8
- "graphExtraction": { "enabled": false },
9
8
  "extract": { "enabled": false },
10
9
  "triage": { "enabled": false },
11
10
  "validation": { "enabled": false },
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Standard improve pass — reflect, distill, advisory consolidation, graph extraction, and validation. Memory inference is listed below but only runs when experimental.improveAutonomy is set; improve-stage extract and proactive maintenance off.",
2
+ "description": "Standard improve pass — reflect, distill, consolidation (promotion plus the reviewed pair-pass retire/supersede proposals), and validation. Memory inference is listed below but only runs when experimental.improveAutonomy is set; improve-stage extract and proactive maintenance off.",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
@@ -9,7 +9,6 @@
9
9
  "distill": { "enabled": true, "allowedTypes": ["memory"], "requirePlannedRefs": true },
10
10
  "consolidate": { "enabled": true, "allowedTypes": ["memory"] },
11
11
  "memoryInference": { "enabled": true },
12
- "graphExtraction": { "enabled": true },
13
12
  "extract": { "enabled": false, "triage": { "enabled": true, "minScore": 2 } },
14
13
  "validation": { "enabled": true },
15
14
  "proactiveMaintenance": { "enabled": false, "dueDays": 30, "maxPerRun": 15 },
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Opt-in proactive-maintenance pass — reflect, distill, proposal triage (promote, high budget), and the proactive-maintenance lane (maxPerRun 100); consolidate/memoryInference/graphExtraction/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
2
+ "description": "Opt-in proactive-maintenance pass — reflect, distill, proposal triage (promote, high budget), and the proactive-maintenance lane (maxPerRun 100); consolidate/memoryInference/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
@@ -8,7 +8,6 @@
8
8
  "distill": { "enabled": true, "allowedTypes": ["memory"] },
9
9
  "consolidate": { "enabled": false },
10
10
  "memoryInference": { "enabled": false },
11
- "graphExtraction": { "enabled": false },
12
11
  "extract": { "enabled": false },
13
12
  "validation": { "enabled": false },
14
13
  "triage": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Reflect-only pass — no extract, distill, consolidate, memoryInference, or graphExtraction.",
2
+ "description": "Reflect-only pass — no extract, distill, consolidate, or memoryInference.",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
@@ -9,7 +9,6 @@
9
9
  "distill": { "enabled": false },
10
10
  "consolidate": { "enabled": false },
11
11
  "memoryInference": { "enabled": false },
12
- "graphExtraction": { "enabled": false },
13
12
  "triage": { "enabled": false },
14
13
  "validation": { "enabled": false },
15
14
  "proactiveMaintenance": { "enabled": false }
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Reflect + distill pass — reflect, distill, memoryInference, and proposal triage (promote); proactiveMaintenance/consolidate/graphExtraction/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
2
+ "description": "Reflect + distill pass — reflect, distill, memoryInference, and proposal triage (promote); proactiveMaintenance/consolidate/extract off. Sync disabled: an interrupted run would otherwise leave an uncommitted backlog.",
3
3
  "processes": {
4
4
  "reflect": {
5
5
  "enabled": true,
@@ -8,7 +8,6 @@
8
8
  "distill": { "enabled": true, "allowedTypes": ["memory"], "requirePlannedRefs": false },
9
9
  "consolidate": { "enabled": false },
10
10
  "memoryInference": { "enabled": true },
11
- "graphExtraction": { "enabled": false },
12
11
  "extract": {
13
12
  "enabled": false,
14
13
  "timeoutMs": 300000,
@@ -18,9 +18,6 @@
18
18
  "memoryInference": {
19
19
  "enabled": true
20
20
  },
21
- "graphExtraction": {
22
- "enabled": true
23
- },
24
21
  "extract": {
25
22
  "enabled": false,
26
23
  "triage": {
@@ -0,0 +1,20 @@
1
+ You compare two assets from one person's agent-memory library (memories and knowledge notes an AI coding agent reads). Asset A is the OLDER one and asset B the NEWER one, by the dates shown.
2
+
3
+ Classify the relation between them as exactly one of:
4
+
5
+ - "duplicate": they state the same durable facts. Wording, title or formatting may differ, but neither adds a claim a reader would need that the other lacks.
6
+ - "subsumed": one of them contains every durable claim of the other, plus more. The smaller one is redundant.
7
+ - "supersedes": B updates, corrects, reverses or replaces a claim in A (a newer version, a changed decision, a fixed bug, a new value). A is now stale or wrong on that point.
8
+ - "contradicts": they make logically exclusive claims about the same thing and nothing shows which one is current.
9
+ - "overlap": same subject, but each has durable claims the other lacks. Keeping both loses nothing; merging them would keep both sets of claims.
10
+ - "unrelated": different subjects or different facts that happen to share words.
11
+
12
+ Rules:
13
+ - A durable claim is a fact, decision, value, command, path, number or rule someone would act on. Ignore dates, headings, tags and phrasing.
14
+ - Prefer "overlap" over "duplicate" when either asset has a specific detail (a number, command, file, version or condition) that the other lacks.
15
+ - "supersedes" needs a specific claim in A that B changes. Being newer or longer is not enough.
16
+ - "contradicts" needs two claims that cannot both be true. Different scope, project or time is not a contradiction.
17
+
18
+ Set "redundant" to "A" or "B" when that asset could be removed with no loss (only for "duplicate" or "subsumed"; for "duplicate" name the less complete or older one), else null. Set "stale" to "A" when the relation is "supersedes", else null.
19
+
20
+ Answer ONLY with JSON: {"relation": "...", "redundant": "A"|"B"|null, "stale": "A"|null, "confidence": 0.0-1.0, "reason": "<at most 25 words>"}
@@ -3,25 +3,26 @@ type: fact
3
3
  category: convention
4
4
  description: How to cross-link assets so retrieval compounds — a provenance xref when derived, sparse real associative xrefs, corrections as new assets, and canonical entity naming.
5
5
  when_to_use: Surfaced to authoring agents when they create or revise any asset that derives from, corrects, or relates to another asset.
6
- updated: 2026-07-28
6
+ updated: 2026-09-28
7
7
  ---
8
8
 
9
9
  <!--
10
10
  SOFT guidance only — advice, not a contract. Back-linking here is a RETRIEVAL
11
- mechanism, not decoration: `xrefs:` frontmatter folds into the search index;
12
- the entity/relation graph is extracted from BODY prose (memory + knowledge),
13
- never from frontmatter. Over-linking degrades ranking, so these rules are
14
- deliberately conservative. (LLM-wiki bundles carry their own xref system in
15
- their pages' frontmatter — this convention is for in-stash assets.)
11
+ mechanism, not decoration: `xrefs:` frontmatter folds into the search index
12
+ and is stored as a declared link `akm show` lists on both assets.
13
+ Over-linking degrades ranking, so these rules are deliberately conservative.
14
+ (LLM-wiki bundles carry their own xref system in their pages' frontmatter —
15
+ this convention is for in-stash assets.)
16
16
  -->
17
17
 
18
18
  # Back-linking conventions
19
19
 
20
20
  Cross-references are how knowledge compounds instead of being re-derived every
21
21
  session. In AKM they are also **indexed**: the strings in an asset's `xrefs:`
22
- frontmatter fold into its search-hint text, and knowledge/memory bodies feed an
23
- LLM-extracted entity/relation graph that boosts ranking. So links are a retrieval
24
- lever — which means both too few and too many hurt.
22
+ frontmatter fold into its search-hint text and are stored as links that
23
+ `akm show` lists on both assets (with `supersededBy:`, `contradictedBy:` and a
24
+ `.derived` memory's parent). So links are a retrieval lever — which means both
25
+ too few and too many hurt.
25
26
 
26
27
  ```yaml
27
28
  ---
@@ -46,10 +47,8 @@ xrefs:
46
47
  - **Associative xrefs are discretionary — real relationships only.** Add one when
47
48
  you already know a genuine load-bearing connection. Do **not** hit a link
48
49
  quota by pointing at the topically-nearest sibling — a plausible-but-wrong
49
- xref makes this asset a false search match for the other topic, and a wrong
50
- relationship asserted in prose poisons the entity graph. A relationship you
51
- want the graph to learn must be named in the body (e.g. open with "Corrects
52
- knowledge/auth/oauth-refresh-races").
50
+ xref makes this asset a false search match for the other topic and a false
51
+ declared link on both assets' `akm show` output.
53
52
  - **Cap total xrefs at ~5 (a heuristic, not a measured threshold).** Each xref
54
53
  folds its ref tokens into THIS asset's search hints — past a handful, the
55
54
  asset matches queries about several other topics and its own ranking signal
@@ -78,19 +77,20 @@ prose is indexed in the lowest-weight `content` field, so
78
77
  `description:`/`when_to_use:` remain the primary orientation channel. Then
79
78
  open the body with a plain title plus a one-line
80
79
  orientation naming what it is, its scope/domain, and its key entities in
81
- canonical spelling (`Postgres`, `OAuth`, `TLS`, `Acme`) — the entity/relation
82
- graph is extracted from body prose, and readers land here from `akm show`.
83
- Keep the canonical-spelling list in `facts/conventions/domains` so agents don't
84
- fragment `postgres` / `postgresql` / `pg`.
80
+ canonical spelling (`Postgres`, `OAuth`, `TLS`, `Acme`) — consistent spelling
81
+ keeps search matches from fragmenting across variants, and readers land here
82
+ from `akm show`. Keep the canonical-spelling list in `facts/conventions/domains`
83
+ so agents don't fragment `postgres` / `postgresql` / `pg`.
85
84
 
86
85
  ## Hubs are optional, not per-namespace obligations
87
86
 
88
87
  A hub (a `knowledge/` overview page that xrefs the key assets in a domain) is
89
88
  worth authoring for a **few genuinely high-traffic domains**. Do **not**
90
89
  mandate a hub per namespace and do not edit a hub on every write: that is O(n)
91
- maintenance, a concurrent-write contention point, and it flattens the multi-hop
92
- graph into a namespace-wide star. Let the FTS index be the catalog; spend the
93
- effort on per-asset self-situating headers instead.
90
+ maintenance, a concurrent-write contention point, and it flattens every linked
91
+ asset's declared links into a namespace-wide star centered on the hub. Let the
92
+ FTS index be the catalog; spend the effort on per-asset self-situating headers
93
+ instead.
94
94
 
95
95
  ## Keep assets atomic
96
96
 
@@ -48,8 +48,8 @@ volume justifies it.
48
48
  ## Canonical entity spellings
49
49
 
50
50
  Pick ONE name per entity and use it everywhere in asset **bodies** — retrieval
51
- is case-insensitive but treats aliases as different entities, so alias variants
52
- fragment the entity graph. Extend as your stash grows.
51
+ is case-insensitive but treats aliases as different terms, so alias variants
52
+ fragment search matches. Extend as your stash grows.
53
53
 
54
54
  - Postgres (not postgresql / pg)
55
55
  - Kubernetes (not k8s)
@@ -325,7 +325,7 @@
325
325
 
326
326
  <div class="chart-panel full-width">
327
327
  <h3>Per-Phase Wall Time Decomposition (stacked, min)</h3>
328
- <div id="chartPhases" class="ec tall" role="img" aria-label="Stacked area chart decomposing wall time per run into consolidation, memory inference, graph extraction, and unattributed phases"></div>
328
+ <div id="chartPhases" class="ec tall" role="img" aria-label="Stacked area chart decomposing wall time per run into consolidation, memory inference, and unattributed phases"></div>
329
329
  <div class="empty-overlay" id="emptyPhases">No runs in the selected slice.</div>
330
330
  </div>
331
331
 
@@ -390,7 +390,7 @@
390
390
  <div class="table-wrap">
391
391
  <table>
392
392
  <thead>
393
- <tr><th>Started</th><th>Task</th><th>Strategy</th><th>Wall</th><th>Promoted</th><th>Merged</th><th>Contradicted</th><th>MI Written</th><th>Entities</th><th>Lint Fixed</th><th>Status</th></tr>
393
+ <tr><th>Started</th><th>Task</th><th>Strategy</th><th>Wall</th><th>Promoted</th><th>Merged</th><th>Contradicted</th><th>MI Written</th><th>Lint Fixed</th><th>Status</th></tr>
394
394
  </thead>
395
395
  <tbody id="lastRunsTable"></tbody>
396
396
  </table>
@@ -539,7 +539,7 @@ mkChart('chartWallTime', {
539
539
  // ── 2. Per-phase wall time decomposition (stacked area) ──────────────────────
540
540
  mkChart('chartPhases', {
541
541
  tooltip: baseTooltip,
542
- legend: { ...baseLegend, data: ['Consolidation', 'Memory inference', 'Graph extraction', 'Unattributed'] },
542
+ legend: { ...baseLegend, data: ['Consolidation', 'Memory inference', 'Unattributed'] },
543
543
  grid: denseGrid,
544
544
  dataZoom: denseZoom,
545
545
  xAxis: { type: 'category', data: xLabels, ...baseAxis },
@@ -547,7 +547,6 @@ mkChart('chartPhases', {
547
547
  series: [
548
548
  { name: 'Consolidation', type: 'line', stack: 'p', areaStyle: { color: 'rgba(88,166,255,0.55)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.consDurationMs)) },
549
549
  { name: 'Memory inference', type: 'line', stack: 'p', areaStyle: { color: 'rgba(63,185,80,0.55)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.miDurationMs)) },
550
- { name: 'Graph extraction', type: 'line', stack: 'p', areaStyle: { color: 'rgba(188,140,255,0.55)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.geDurationMs)) },
551
550
  { name: 'Unattributed', type: 'line', stack: 'p', areaStyle: { color: 'rgba(210,153,34,0.45)' }, lineStyle: { width: 0 }, showSymbol: false, data: rs.map(r => toMin(r.otherMs)) },
552
551
  ],
553
552
  });
@@ -695,7 +694,6 @@ tbody.innerHTML = '';
695
694
  <td>${r.merged || 0}</td>
696
695
  <td style="color:${(r.contradicted||0)>5?'var(--yellow)':'var(--text)'};">${r.contradicted || 0}</td>
697
696
  <td style="color:var(--green);">${r.miWritten || 0}</td>
698
- <td>${r.geEntities || 0}</td>
699
697
  <td>${r.lintFixed || 0}</td>
700
698
  <td>${badge}</td>
701
699
  `;
@@ -38,7 +38,7 @@ const RETIRED_COMMAND_HINTS = {
38
38
  events: "`akm events` moved in 0.9 — use `akm log`.",
39
39
  // Removed observability surfaces.
40
40
  history: "`akm history` was removed in 0.9 — use `akm log --ref <ref>` for an asset's event trail.",
41
- graph: "`akm graph` was removed in 0.9 — graph counts appear in `akm health`; refresh extraction with `akm improve --strategy graph-refresh`.",
41
+ graph: "`akm graph` was removed in 0.9. The LLM entity graph it inspected was itself retired in 0.9.17-alpha.9 — use `akm show <ref>` for an asset's declared links.",
42
42
  lessons: "`akm lessons` was removed in 0.9 — lesson strength is indexed; use `akm search --type lesson`.",
43
43
  lesson: "`akm lesson` was removed in 0.9 — lesson strength is indexed; use `akm search --type lesson`.",
44
44
  // Relocated guidance / removed asset verbs.
@@ -0,0 +1,98 @@
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
+ /**
5
+ * `memory-cleanup-archive` advisory for `akm health` (item 4, 0.9.17-alpha.9
6
+ * plan §5.4/§8 step 8).
7
+ *
8
+ * Reports the archive's size and file count for every bundle, git-backed or
9
+ * not. A bundle with no `.git` of its own keeps every retirement's archived
10
+ * bytes forever (the purge sweep never runs there at all), so that alone is
11
+ * reported. A git-backed bundle can ALSO have bytes the purge sweep will
12
+ * never remove: `.git` presence alone does not prove a retirement was ever
13
+ * committed (`proposal accept` only commits for a `kind: "git"` write
14
+ * target, and a `kind: "filesystem"` bundle that merely happens to have a
15
+ * `.git` directory never gets one) — those bytes sit there indefinitely,
16
+ * however old they get, with nothing else to say so. This checks the SAME
17
+ * git state `purgeGracedArchive` checks (tracked, clean, verifiable) and
18
+ * reports how much of the archive fails it.
19
+ *
20
+ * Silent whenever there is nothing to say: the archive does not exist or is
21
+ * empty, or (for a git-backed bundle) every byte in it is purgeable once it
22
+ * ages out.
23
+ */
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { MEMORY_ARCHIVE_REL } from "../../core/asset/memory-archive.js";
27
+ import { toPosix } from "../../core/common.js";
28
+ import { isGitBackedStash, tryListGitChangedPaths, tryListGitTrackedPaths, tryListGitUnverifiablePaths, } from "../../sources/providers/git-stash.js";
29
+ import { MAX_WALK_ENTRIES, sizeOfPath } from "./data-dir-usage.js";
30
+ /**
31
+ * Build the `memory-cleanup-archive` advisory, or `undefined` when there is
32
+ * nothing to report.
33
+ */
34
+ export function collectArchiveUsageAdvisory(stashDir) {
35
+ const archiveRoot = path.join(stashDir, MEMORY_ARCHIVE_REL);
36
+ if (!fs.existsSync(archiveRoot))
37
+ return undefined;
38
+ if (!isGitBackedStash(stashDir)) {
39
+ const usage = sizeOfPath(archiveRoot, { remaining: MAX_WALK_ENTRIES });
40
+ if (usage.files === 0)
41
+ return undefined;
42
+ const lowerBound = usage.truncated ? ` (a lower bound — the walk stopped after ${MAX_WALK_ENTRIES} entries)` : "";
43
+ return {
44
+ name: "memory-cleanup-archive",
45
+ kind: "deterministic",
46
+ status: "pass",
47
+ confidence: "high",
48
+ message: `${usage.files} archived file(s), ${usage.bytes} byte(s)${lowerBound} in .akm/memory-cleanup/archive — ` +
49
+ "this bundle has no git history, so the purge sweep leaves it untouched.",
50
+ evidence: { files: usage.files, bytes: usage.bytes, truncated: usage.truncated },
51
+ };
52
+ }
53
+ // Git-backed: the SAME three checks purgeGracedArchive runs (B1, G10) —
54
+ // computed once here, not per file, and reused via `onFile` below instead
55
+ // of a second walk of the same tree.
56
+ const dirtyQuery = tryListGitChangedPaths(stashDir);
57
+ const trackedQuery = tryListGitTrackedPaths(stashDir, MEMORY_ARCHIVE_REL);
58
+ const unverifiableQuery = tryListGitUnverifiablePaths(stashDir, MEMORY_ARCHIVE_REL);
59
+ // A failed git check here fails the same way purgeGracedArchive's own
60
+ // sweep would: nothing in the archive can be proven purgeable, so every
61
+ // byte counts as unpurgeable rather than guessing.
62
+ const gitStateKnown = dirtyQuery.ok && trackedQuery.ok && unverifiableQuery.ok;
63
+ const dirty = new Set(dirtyQuery.paths);
64
+ const tracked = new Set(trackedQuery.paths);
65
+ const unverifiable = new Set(unverifiableQuery.paths);
66
+ let unpurgeableFiles = 0;
67
+ let unpurgeableBytes = 0;
68
+ const usage = sizeOfPath(archiveRoot, { remaining: MAX_WALK_ENTRIES }, (filePath, bytes) => {
69
+ const key = toPosix(path.relative(stashDir, filePath));
70
+ const safe = gitStateKnown && tracked.has(key) && !dirty.has(key) && !unverifiable.has(key);
71
+ if (!safe) {
72
+ unpurgeableFiles++;
73
+ unpurgeableBytes += bytes;
74
+ }
75
+ });
76
+ if (usage.files === 0)
77
+ return undefined;
78
+ if (unpurgeableFiles === 0)
79
+ return undefined; // everything here is purgeable once it ages out — nothing to say
80
+ const lowerBound = usage.truncated ? ` (a lower bound — the walk stopped after ${MAX_WALK_ENTRIES} entries)` : "";
81
+ return {
82
+ name: "memory-cleanup-archive",
83
+ kind: "deterministic",
84
+ status: "warn",
85
+ confidence: "high",
86
+ message: `${usage.files} archived file(s), ${usage.bytes} byte(s)${lowerBound} in .akm/memory-cleanup/archive; ` +
87
+ `${unpurgeableFiles} file(s), ${unpurgeableBytes} byte(s) of that cannot be purged (untracked, modified, ` +
88
+ "or unverifiable in git) — commit them so the purge sweep can remove them once they age out.",
89
+ evidence: {
90
+ files: usage.files,
91
+ bytes: usage.bytes,
92
+ truncated: usage.truncated,
93
+ unpurgeableFiles,
94
+ unpurgeableBytes,
95
+ gitStateKnown,
96
+ },
97
+ };
98
+ }
@@ -41,7 +41,7 @@ const DOMINANT_SUBDIR_PERCENT_THRESHOLD = 50;
41
41
  * cap the walk stops descending further and the advisory says its size
42
42
  * figures are a lower bound.
43
43
  */
44
- const MAX_WALK_ENTRIES = 100_000;
44
+ export const MAX_WALK_ENTRIES = 100_000;
45
45
  /**
46
46
  * Below this the data dir is not worth an opinion. The advisory exists for
47
47
  * disk blowups (74 GB in the incident); on a small directory a ratio is
@@ -59,31 +59,42 @@ function liveDbBytesFor(name, sizes) {
59
59
  return ((sizes.get(name)?.bytes ?? 0) + (sizes.get(`${name}-wal`)?.bytes ?? 0) + (sizes.get(`${name}-shm`)?.bytes ?? 0));
60
60
  }
61
61
  /**
62
- * Recursively sum file sizes under `root` (stat-only, symlinks not
63
- * followed so a cyclic or huge-target symlink can't blow up the walk).
64
- * `budget` is a shared mutable counter across the whole tree so the
65
- * `MAX_WALK_ENTRIES` cap applies to the walk as a whole, not per-branch.
62
+ * Recursively sum file sizes (and count files) under `root` (stat-only,
63
+ * symlinks not followed so a cyclic or huge-target symlink can't blow up the
64
+ * walk). `budget` is a shared mutable counter across the whole tree so the
65
+ * entry cap applies to the walk as a whole, not per-branch — callers
66
+ * typically pass {@link MAX_WALK_ENTRIES}, sized for this module's own data
67
+ * dir walk, but a smaller/larger budget is fine for a different tree.
68
+ * Shared with the `archive-usage` advisory (N2) — the same "don't let a
69
+ * pathological tree hang a health check" concern applies to both.
70
+ *
71
+ * `onFile`, when given, is called once per leaf file (path, bytes) as the
72
+ * walk visits it — `archive-usage` uses this to classify each file's git
73
+ * state without a second, separate walk of the same tree.
66
74
  */
67
- function sizeOfPath(root, budget) {
75
+ export function sizeOfPath(root, budget, onFile) {
68
76
  let stat;
69
77
  try {
70
78
  stat = fs.lstatSync(root);
71
79
  }
72
80
  catch {
73
- return { bytes: 0, truncated: false };
81
+ return { bytes: 0, files: 0, truncated: false };
74
82
  }
75
83
  if (stat.isSymbolicLink())
76
- return { bytes: 0, truncated: false };
77
- if (!stat.isDirectory())
78
- return { bytes: stat.size, truncated: false };
84
+ return { bytes: 0, files: 0, truncated: false };
85
+ if (!stat.isDirectory()) {
86
+ onFile?.(root, stat.size);
87
+ return { bytes: stat.size, files: 1, truncated: false };
88
+ }
79
89
  let entries;
80
90
  try {
81
91
  entries = fs.readdirSync(root, { withFileTypes: true });
82
92
  }
83
93
  catch {
84
- return { bytes: 0, truncated: false };
94
+ return { bytes: 0, files: 0, truncated: false };
85
95
  }
86
96
  let bytes = 0;
97
+ let files = 0;
87
98
  let truncated = false;
88
99
  for (const entry of entries) {
89
100
  if (budget.remaining <= 0) {
@@ -91,12 +102,13 @@ function sizeOfPath(root, budget) {
91
102
  break;
92
103
  }
93
104
  budget.remaining--;
94
- const sub = sizeOfPath(path.join(root, entry.name), budget);
105
+ const sub = sizeOfPath(path.join(root, entry.name), budget, onFile);
95
106
  bytes += sub.bytes;
107
+ files += sub.files;
96
108
  if (sub.truncated)
97
109
  truncated = true;
98
110
  }
99
- return { bytes, truncated };
111
+ return { bytes, files, truncated };
100
112
  }
101
113
  /** `1610612736` -> `"1.5G"`. Values under 10 in a unit keep one decimal; 10+ round to an integer. */
102
114
  function formatBytes(bytes) {
@@ -131,7 +131,6 @@ function renderExecSummary(vm) {
131
131
  const trendRows = [
132
132
  trendLi("Decision quality", vm.trend.decisionQuality),
133
133
  trendLi("Output volume", vm.trend.outputVolume),
134
- trendLi("Failures", vm.trend.failures),
135
134
  trendLi("Latency", vm.trend.latency),
136
135
  ].join("");
137
136
  const deltaRows = [
@@ -151,7 +150,6 @@ function renderExecSummary(vm) {
151
150
  li("Promoted", String(vm.latest.promoted)),
152
151
  li("Judged: no action", `<abbr title="Candidates reviewed but intentionally left unchanged on this run.">${vm.latest.judgedNoAction}</abbr>`),
153
152
  li("MI written", String(vm.latest.miWritten)),
154
- li("Graph entities/relations", `${vm.latest.geEntities} / ${vm.latest.geRelations}`),
155
153
  ].join("")
156
154
  : '<li><span class="k">No runs in window</span><span class="v">—</span></li>';
157
155
  const windowRows = [
@@ -197,7 +195,7 @@ function renderExecSummary(vm) {
197
195
  </div>
198
196
  </div>
199
197
  <div class="overall">Overall trend: <b>${esc(vm.trend.overall)}</b> ${overallEmoji}
200
- &nbsp;·&nbsp; based on decision quality, output volume, failures, and latency ${vm.comparisonMode === "custom" ? "across the selected windows" : "vs the prior window"}.</div>`.trim();
198
+ &nbsp;·&nbsp; based on decision quality, output volume, and latency ${vm.comparisonMode === "custom" ? "across the selected windows" : "vs the prior window"}.</div>`.trim();
201
199
  }
202
200
  /**
203
201
  * Color is a health SIGNAL, not decoration: green/yellow/red where a card has
@@ -221,7 +219,6 @@ function renderKpiCards(vm) {
221
219
  kpiCard("neutral", "Median Duration", `${vm.medianDurMin}m`, `p95 = ${vm.p95DurMin}m`),
222
220
  kpiCard("blue", "Total Promoted", num(vm.consolidation.promoted), `avg ${vm.avgPromoted} / run`),
223
221
  kpiCard("blue", "MI Written", num(vm.miWritten), `${vm.miYieldRate} yield rate`),
224
- kpiCard("purple", "Graph Entities", num(vm.graphExtraction.entities), `+${num(vm.graphExtraction.relations)} relations`),
225
222
  kpiCard("neutral", "Stash Derived", num(vm.memorySummary.derived), `of ${num(vm.memorySummary.eligible)} eligible (whole-stash)`),
226
223
  // #576: real LLM work — duration leads, tokens compact, not a GPU proxy.
227
224
  kpiCard(vm.llm.calls > 0 ? "purple" : "neutral", "🧠 LLM Work", `${vm.llmTokensCompact} tok`, `${fmtMs(vm.llm.totalDurationMs)} · ${num(vm.llm.calls)} calls · ${compact(vm.llm.reasoningTokens)} reasoning`),