akm-cli 0.9.0-beta.5 → 0.9.0-beta.51

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 (221) hide show
  1. package/CHANGELOG.md +711 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -0,0 +1,161 @@
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
+ // Reliability computation
6
+ // ---------------------------------------------------------------------------
7
+ /** Number of fixed-width reliability buckets over [0, 1]. */
8
+ export const CALIBRATION_BUCKET_COUNT = 10;
9
+ function roundRate(value) {
10
+ return Number(value.toFixed(4));
11
+ }
12
+ /**
13
+ * Assign a confidence in [0, 1] to one of {@link CALIBRATION_BUCKET_COUNT}
14
+ * fixed-width buckets. The final bucket is closed on the right so confidence
15
+ * === 1 lands in the top bucket rather than overflowing.
16
+ */
17
+ function bucketIndex(confidence) {
18
+ const clamped = Math.min(1, Math.max(0, confidence));
19
+ const idx = Math.floor(clamped * CALIBRATION_BUCKET_COUNT);
20
+ return Math.min(CALIBRATION_BUCKET_COUNT - 1, idx);
21
+ }
22
+ /**
23
+ * Compute a deterministic calibration summary from a list of acted-on gate
24
+ * decisions. Pure: identical input always yields identical output, with no
25
+ * dependency on wall-clock time or randomness.
26
+ */
27
+ export function summarizeCalibration(samples) {
28
+ const buckets = [];
29
+ const bucketCounts = new Array(CALIBRATION_BUCKET_COUNT).fill(0);
30
+ const bucketAccepted = new Array(CALIBRATION_BUCKET_COUNT).fill(0);
31
+ const bucketConfSum = new Array(CALIBRATION_BUCKET_COUNT).fill(0);
32
+ let accepted = 0;
33
+ let confSum = 0;
34
+ for (const sample of samples) {
35
+ const idx = bucketIndex(sample.confidence);
36
+ bucketCounts[idx] = (bucketCounts[idx] ?? 0) + 1;
37
+ bucketConfSum[idx] = (bucketConfSum[idx] ?? 0) + sample.confidence;
38
+ confSum += sample.confidence;
39
+ if (sample.outcome === "auto-accepted") {
40
+ accepted += 1;
41
+ bucketAccepted[idx] = (bucketAccepted[idx] ?? 0) + 1;
42
+ }
43
+ }
44
+ const total = samples.length;
45
+ const width = 1 / CALIBRATION_BUCKET_COUNT;
46
+ for (let i = 0; i < CALIBRATION_BUCKET_COUNT; i += 1) {
47
+ const count = bucketCounts[i] ?? 0;
48
+ const acc = bucketAccepted[i] ?? 0;
49
+ buckets.push({
50
+ lower: roundRate(i * width),
51
+ upper: roundRate((i + 1) * width),
52
+ count,
53
+ accepted: acc,
54
+ acceptRate: count > 0 ? roundRate(acc / count) : 0,
55
+ meanConfidence: count > 0 ? roundRate((bucketConfSum[i] ?? 0) / count) : 0,
56
+ });
57
+ }
58
+ const overallAcceptRate = total > 0 ? roundRate(accepted / total) : 0;
59
+ const meanConfidence = total > 0 ? roundRate(confSum / total) : 0;
60
+ return {
61
+ samples: total,
62
+ accepted,
63
+ rejected: total - accepted,
64
+ overallAcceptRate,
65
+ meanConfidence,
66
+ calibrationGap: total > 0 ? roundRate(meanConfidence - overallAcceptRate) : 0,
67
+ buckets,
68
+ };
69
+ }
70
+ /**
71
+ * Project a list of `gateDecision` records (read from the proposal store) into
72
+ * the acted-on calibration samples within an optional `[since, until)` window.
73
+ *
74
+ * Only `auto-accepted` / `auto-rejected` decisions with a finite confidence in
75
+ * [0, 1] contribute. `deferred` decisions and decisions missing a confidence
76
+ * are excluded (no realized accept/reject signal). The window filter uses each
77
+ * decision's `decidedAt` timestamp; decisions with an unparseable timestamp are
78
+ * kept only when no window is supplied.
79
+ *
80
+ * Exploration-budget promotions (`reason === "exploration-budget"`) are EXCLUDED:
81
+ * they are accepted regardless of confidence, so they carry no reliability signal
82
+ * about the gate threshold. Counting them would inflate the apparent accept-rate
83
+ * and bias the auto-tuner downward — they are exempt from auto-tune by design
84
+ * (WS-4 exploration budget).
85
+ */
86
+ export function gateDecisionsToSamples(decisions, window) {
87
+ const sinceMs = window?.since ? new Date(window.since).getTime() : undefined;
88
+ const untilMs = window?.until ? new Date(window.until).getTime() : undefined;
89
+ const samples = [];
90
+ for (const decision of decisions) {
91
+ if (!decision)
92
+ continue;
93
+ if (decision.outcome !== "auto-accepted" && decision.outcome !== "auto-rejected")
94
+ continue;
95
+ if (decision.reason === "exploration-budget")
96
+ continue;
97
+ const confidence = decision.confidence;
98
+ if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1)
99
+ continue;
100
+ if (sinceMs !== undefined || untilMs !== undefined) {
101
+ const ts = new Date(decision.decidedAt).getTime();
102
+ if (!Number.isFinite(ts))
103
+ continue;
104
+ if (sinceMs !== undefined && ts < sinceMs)
105
+ continue;
106
+ if (untilMs !== undefined && ts >= untilMs)
107
+ continue;
108
+ }
109
+ samples.push({ confidence, outcome: decision.outcome });
110
+ }
111
+ return samples;
112
+ }
113
+ /**
114
+ * Compute a bounded, opt-in threshold adjustment from a calibration summary.
115
+ * PURE and deterministic — does not mutate config or read the clock. The
116
+ * caller is responsible for persisting `newThreshold` and logging the result.
117
+ *
118
+ * Algorithm (deliberately simple and bounded):
119
+ * - When `autoTune` is false → no-op (`disabled`).
120
+ * - When samples < `minSamples` → no-op (`insufficient-samples`).
121
+ * - Otherwise nudge by at most `maxStep` toward `targetAcceptRate`, then
122
+ * clamp into `[minThreshold, maxThreshold]`. The step size scales with the
123
+ * gap from target but is capped, so a single run can never make a large
124
+ * swing.
125
+ */
126
+ export function computeThresholdAutoTune(currentThreshold, summary, config) {
127
+ const previousThreshold = Math.round(currentThreshold);
128
+ const noop = (reason) => ({
129
+ adjusted: false,
130
+ previousThreshold,
131
+ newThreshold: previousThreshold,
132
+ delta: 0,
133
+ reason,
134
+ });
135
+ if (!config.autoTune)
136
+ return noop("disabled");
137
+ if (summary.samples < config.minSamples)
138
+ return noop("insufficient-samples");
139
+ const gap = config.targetAcceptRate - summary.overallAcceptRate;
140
+ // A small dead-band so tiny noise doesn't churn the threshold every run.
141
+ const DEAD_BAND = 0.01;
142
+ if (Math.abs(gap) <= DEAD_BAND)
143
+ return noop("within-target");
144
+ // gap > 0 ⇒ realized below target ⇒ raise threshold (be stricter).
145
+ // gap < 0 ⇒ realized above target ⇒ lower threshold (be more permissive).
146
+ const direction = gap > 0 ? 1 : -1;
147
+ // Scale the step with the gap magnitude (in points) but cap at maxStep.
148
+ const desiredMagnitude = Math.min(config.maxStep, Math.max(1, Math.round(Math.abs(gap) * 100)));
149
+ const proposed = previousThreshold + direction * desiredMagnitude;
150
+ const clamped = Math.min(config.maxThreshold, Math.max(config.minThreshold, proposed));
151
+ const delta = clamped - previousThreshold;
152
+ if (delta === 0)
153
+ return noop("clamped-at-bound");
154
+ return {
155
+ adjusted: true,
156
+ previousThreshold,
157
+ newThreshold: clamped,
158
+ delta,
159
+ reason: direction > 0 ? "below-target-raise" : "above-target-lower",
160
+ };
161
+ }
@@ -0,0 +1,141 @@
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
+ // Chunk sizing + per-chunk prompt assembly for consolidate. Pure token math and
5
+ // prompt-string construction over MemoryEntry inputs — no LLM call, no embedder,
6
+ // no orchestrator coupling.
7
+ import fs from "node:fs";
8
+ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
9
+ import { cacheHash } from "../dedup.js";
10
+ /**
11
+ * Conservative chars-per-token estimate used when computing prompt budgets.
12
+ * English text averages roughly 4 chars/token for most LLM tokenizers. We use
13
+ * 3 to stay conservative (shorter tokens = more tokens per char).
14
+ */
15
+ const CHARS_PER_TOKEN = 3;
16
+ /**
17
+ * Overhead budget reserved for the system prompt, chunk header lines, and per-
18
+ * memory metadata lines (name, description, tags, separator). Measured at
19
+ * roughly 600 chars for the system prompt + ~100 chars of header + ~50 chars
20
+ * per memory × chunk size. We round up to 2 000 tokens to leave room for the
21
+ * model's own output.
22
+ */
23
+ const PROMPT_OVERHEAD_TOKENS = 2_000;
24
+ /**
25
+ * Default effective token budget used when the default LLM profile's
26
+ * `contextLength` is not set. This is intentionally conservative (4 096)
27
+ * rather than being set to the model's actual context window, because:
28
+ *
29
+ * - When the agent path is used, the agent CLI (e.g. opencode)
30
+ * prepends its own large system prompt + conversation history before
31
+ * forwarding to the model. That overhead easily consumes 30K+ tokens on
32
+ * a model with a 16K context window, leaving very little room for
33
+ * chunk content.
34
+ * - When the HTTP path is used (an LLM profile is selected), only the akm
35
+ * system prompt and user prompt are sent, so the budget can be set to the
36
+ * model's actual context length via profiles.llm[defaults.llm].contextLength.
37
+ *
38
+ * Set profiles.llm[defaults.llm].contextLength in your config file to the
39
+ * model's actual context window to allow larger chunks on the HTTP path.
40
+ */
41
+ export const DEFAULT_CONTEXT_LENGTH_TOKENS = 4_096;
42
+ /**
43
+ * Given the model's context window and the per-memory body truncation limit,
44
+ * return the maximum number of memories that can safely fit in one chunk
45
+ * without the prompt overflowing the context window.
46
+ *
47
+ * The formula is:
48
+ * usableTokens = contextLength - PROMPT_OVERHEAD_TOKENS
49
+ * tokensPerMemory = ceil(bodyTruncation / CHARS_PER_TOKEN)
50
+ * chunkSize = floor(usableTokens / tokensPerMemory)
51
+ *
52
+ * Result is clamped between 1 and 50 to avoid degenerate values.
53
+ *
54
+ * @param contextLength - Model context window in tokens.
55
+ * @param bodyTruncation - Max chars per memory body included in the prompt.
56
+ * @param maxChunkSize - Optional override for the hardcoded cap of 50 (1–50).
57
+ */
58
+ export function computeSafeChunkSize(contextLength, bodyTruncation, maxChunkSize) {
59
+ const usableTokens = Math.max(contextLength - PROMPT_OVERHEAD_TOKENS, 0);
60
+ const tokensPerMemory = Math.max(Math.ceil(bodyTruncation / CHARS_PER_TOKEN), 1);
61
+ const raw = Math.floor(usableTokens / tokensPerMemory);
62
+ return Math.max(1, Math.min(maxChunkSize ?? 50, raw));
63
+ }
64
+ /**
65
+ * Build the per-chunk user prompt fed to the consolidate LLM.
66
+ *
67
+ * Each memory is annotated with two flags that drive the system-prompt
68
+ * rules at lines 181-186:
69
+ * - `(captureMode: hot)` — user-explicit memory; system prompt rule 2
70
+ * forbids proposing delete. ~60 wasted LLM verdicts/4h on this user's
71
+ * stack before this annotation.
72
+ * - `(already queued)` — the memory's body hash matches a pending
73
+ * consolidate proposal; system prompt rule 3 forbids proposing
74
+ * promote/merge/contradict. ~107/4h before this annotation.
75
+ *
76
+ * Both annotations are visible to the LLM. `pendingProposalBodyHashes`
77
+ * is precomputed once per run by `loadPendingConsolidateProposalHashes`
78
+ * so the cost stays O(memories) inside the chunk loop.
79
+ */
80
+ export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set(), standardsContext = "") {
81
+ const start = memories[0] ? `memory:${memories[0].name}` : "";
82
+ const end = memories[memories.length - 1] ? `memory:${memories[memories.length - 1].name}` : "";
83
+ const annotationsByIndex = [];
84
+ const hotRefs = [];
85
+ for (const m of memories) {
86
+ let body = "";
87
+ try {
88
+ body = fs.readFileSync(m.filePath, "utf8");
89
+ }
90
+ catch {
91
+ body = "(unreadable)";
92
+ }
93
+ const parsed = parseFrontmatter(body);
94
+ const isHot = parsed.data.captureMode === "hot";
95
+ // Use cacheHash (case-preserving stripped body) to match the domain used
96
+ // by loadPendingConsolidateProposalHashes and the body-embedding cache.
97
+ const bodyHash = cacheHash(body);
98
+ const isAlreadyQueued = pendingProposalBodyHashes.has(bodyHash);
99
+ annotationsByIndex.push({ isHot, isAlreadyQueued, body });
100
+ if (isHot)
101
+ hotRefs.push(`memory:${m.name}`);
102
+ }
103
+ const lines = [
104
+ `Source: ${sourceName}`,
105
+ `Chunk ${chunkIndex + 1} of ${totalChunks}, memories ${start}–${end}:`,
106
+ "",
107
+ ];
108
+ if (standardsContext.trim()) {
109
+ lines.push("Standards to follow (the rulebook for this target):");
110
+ lines.push(standardsContext.trim());
111
+ lines.push("");
112
+ }
113
+ // Top-of-prompt protection block for hot refs. Neutral phrasing — avoid
114
+ // op-words like "promote", "merge", "contradict" so the model doesn't
115
+ // accidentally treat the warning as a hint to use that op elsewhere
116
+ // (variant B leaked the word "contradict" into the control sample
117
+ // during the diagnostic).
118
+ if (hotRefs.length > 0) {
119
+ lines.push("⛔ DO NOT propose any `delete` operation for these refs — they are user-explicit (captureMode: hot) and the downstream guard refuses them regardless. Proposing delete for any of these only wastes tokens.");
120
+ for (const ref of hotRefs)
121
+ lines.push(` - ${ref}`);
122
+ lines.push("");
123
+ }
124
+ for (let i = 0; i < memories.length; i++) {
125
+ const m = memories[i];
126
+ const { isHot, isAlreadyQueued, body } = annotationsByIndex[i];
127
+ const annotations = [];
128
+ if (isHot)
129
+ annotations.push("captureMode: hot");
130
+ if (isAlreadyQueued)
131
+ annotations.push("already queued");
132
+ const annotationSuffix = annotations.length > 0 ? ` (${annotations.join("; ")})` : "";
133
+ lines.push(`[${i + 1}] memory:${m.name}${annotationSuffix}`);
134
+ lines.push(`Description: ${m.description || "(none)"}`);
135
+ lines.push(`Tags: ${m.tags.length > 0 ? m.tags.join(", ") : "(none)"}`);
136
+ lines.push("---");
137
+ lines.push(body.slice(0, bodyTruncation));
138
+ lines.push("");
139
+ }
140
+ return lines.join("\n");
141
+ }
@@ -0,0 +1,81 @@
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
+ // Eligibility / safety predicates for consolidate: "may we touch this memory?"
5
+ // One reason to change — the policy for what consolidate is allowed to act on.
6
+ import fs from "node:fs";
7
+ import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
8
+ import { hasHotCaptureMode } from "../../proposal/validators/proposal-quality-validators.js";
9
+ export function isConsolidationEligibleMemoryName(name) {
10
+ return !name.endsWith(".derived");
11
+ }
12
+ /**
13
+ * #632 — AKM session-capture telemetry memories: auto-generated session-end
14
+ * checkpoints named `<harness>-session-<YYYYMMDD>-<id>` or
15
+ * `<harness>-checkpoint-<YYYYMMDD…>-<id>`, carrying an embedded
16
+ * `akm_memory_kind: session_checkpoint` metadata block. Their bodies are
17
+ * pipeline bookkeeping, not durable knowledge, so the improve passes exclude
18
+ * them from their pools (recombine, consolidate). The `\d{8}` datestamp anchor
19
+ * is what distinguishes a capture name from a durable memory that merely
20
+ * MENTIONS session/checkpoint (e.g. `akm-plugins-session-end-extract-hook`,
21
+ * `session-checkpoint-lint-skips`), which stay in the pool.
22
+ *
23
+ * Lives here (the eligibility module) rather than in recombine so both
24
+ * recombine and consolidate can reuse it without a circular import.
25
+ */
26
+ export function isSessionCaptureMemoryName(name) {
27
+ return /-(session|checkpoint)-\d{8}/.test(name);
28
+ }
29
+ /**
30
+ * Returns true when the memory file has `captureMode: hot` in its frontmatter.
31
+ *
32
+ * Hot memories are USER-EXPLICIT (written via `akm remember` on the hot path).
33
+ * The consolidate LLM is forbidden from deleting or auto-merging them — the
34
+ * user wrote them on purpose and only the user can decide to retire them.
35
+ *
36
+ * Reads the file once per check; consolidate runs against ~10 memories per
37
+ * chunk so the IO cost is trivial. Returns false on any read/parse error
38
+ * (fail-safe: an unparseable file is treated as not-hot, but the broader
39
+ * consolidate flow already guards against unparseable memories elsewhere).
40
+ *
41
+ * Defends against four observed defect classes (see
42
+ * `memory:akm-improve-critical-review-2026-05-20`):
43
+ * - LLM marks a memory contradicted then deletes (dangling contradictedBy)
44
+ * - LLM merges two unrelated memories sharing a topic keyword
45
+ * - LLM judges a recent durable design memo as "redundant"
46
+ * - Cascade deletes (LLM uses ref:X as `contradictedBy` for ref:Y then deletes both)
47
+ */
48
+ export function isHotCapturedMemory(filePath) {
49
+ try {
50
+ if (!fs.existsSync(filePath))
51
+ return false;
52
+ const content = fs.readFileSync(filePath, "utf8");
53
+ const parsed = parseFrontmatter(content);
54
+ return hasHotCaptureMode(parsed.data);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ export function consolidateGuardStatus(filePath) {
61
+ if (!fs.existsSync(filePath))
62
+ return "missing";
63
+ let content;
64
+ try {
65
+ content = fs.readFileSync(filePath, "utf8");
66
+ }
67
+ catch {
68
+ return "unparseable";
69
+ }
70
+ let parsed;
71
+ try {
72
+ parsed = parseFrontmatter(content);
73
+ }
74
+ catch {
75
+ return "unparseable";
76
+ }
77
+ const data = parsed.data;
78
+ if (!data || Object.keys(data).length === 0)
79
+ return "unparseable";
80
+ return hasHotCaptureMode(data) ? "hot" : "safe";
81
+ }
@@ -0,0 +1,145 @@
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
+ export function isValidOp(op) {
5
+ if (typeof op !== "object" || op === null)
6
+ return false;
7
+ const o = op;
8
+ if (o.op === "merge") {
9
+ return typeof o.primary === "string" && Array.isArray(o.secondaries);
10
+ }
11
+ if (o.op === "delete") {
12
+ return typeof o.ref === "string";
13
+ }
14
+ if (o.op === "promote") {
15
+ return typeof o.ref === "string" && typeof o.knowledgeRef === "string";
16
+ }
17
+ if (o.op === "contradict") {
18
+ return typeof o.ref === "string" && typeof o.contradictedByRef === "string";
19
+ }
20
+ return false;
21
+ }
22
+ export function mergePlans(chunks, knownRefs) {
23
+ const mergeOps = new Map();
24
+ const deleteOps = new Map();
25
+ const promoteOps = new Map();
26
+ // C-3 / #382: contradict ops keyed by `ref|contradictedByRef` to deduplicate.
27
+ const contradictOps = new Map();
28
+ const warnings = [];
29
+ for (const chunk of chunks) {
30
+ for (const op of chunk) {
31
+ if (op.op === "merge") {
32
+ // Drop ops whose primary the LLM hallucinated (not in the loaded memory
33
+ // pool). Without this guard, a hallucinated primary flows all the way to
34
+ // Phase B where !memoryByRef.has(primary) fires and charges every real
35
+ // secondary with merge_primary_missing — masking LLM hallucinations as
36
+ // filter regressions in health metrics.
37
+ if (knownRefs && !knownRefs.has(op.primary)) {
38
+ warnings.push(`mergePlans: primary ${op.primary} not in loaded memory pool (LLM hallucination) — dropping op before execution.`);
39
+ // Use a dedicated skip reason so dashboards can distinguish
40
+ // hallucinated primaries from stale-DB regressions.
41
+ // Secondaries are real refs; they are NOT charged here — they remain
42
+ // available for other ops to claim.
43
+ continue;
44
+ }
45
+ // Filter hallucinated secondaries while preserving real ones.
46
+ let mergeOp = op;
47
+ if (knownRefs) {
48
+ const filteredSecondaries = op.secondaries.filter((sec) => {
49
+ if (!knownRefs.has(sec)) {
50
+ warnings.push(`mergePlans: secondary ${sec} not in loaded memory pool (LLM hallucination) — dropping from op.`);
51
+ return false;
52
+ }
53
+ return true;
54
+ });
55
+ if (filteredSecondaries.length !== op.secondaries.length) {
56
+ mergeOp = { ...op, secondaries: filteredSecondaries };
57
+ }
58
+ }
59
+ // merge wins over delete
60
+ if (deleteOps.has(mergeOp.primary)) {
61
+ deleteOps.delete(mergeOp.primary);
62
+ }
63
+ for (const sec of mergeOp.secondaries) {
64
+ if (deleteOps.has(sec))
65
+ deleteOps.delete(sec);
66
+ }
67
+ mergeOps.set(mergeOp.primary, mergeOp);
68
+ }
69
+ else if (op.op === "delete") {
70
+ // merge and promote both win over delete. A promote is non-destructive
71
+ // (creates a proposal) but the source memory is counted in `promoted`;
72
+ // if a delete also fires, the ref lands in both `promoted` and
73
+ // `skipReasons`, breaking the invariant by +1.
74
+ if (!mergeOps.has(op.ref) && !promoteOps.has(op.ref)) {
75
+ deleteOps.set(op.ref, op);
76
+ }
77
+ }
78
+ else if (op.op === "promote") {
79
+ // C-2 / #381: when both a promote and a merge target the same ref,
80
+ // queue the promote FIRST rather than discarding it. The promote op
81
+ // routes through createProposal (the human-gated proposal queue), so
82
+ // it is non-destructive. The merge follows after the proposal is
83
+ // created. This preserves the human reviewer's ability to inspect the
84
+ // promotion before the source memory is merged/deleted.
85
+ // AGM K*8 — retain the maximally informative consistent subset.
86
+ promoteOps.set(op.ref, op);
87
+ }
88
+ else if (op.op === "contradict") {
89
+ // Deduplicate by ref+contradictedByRef pair.
90
+ const key = `${op.ref}|${op.contradictedByRef}`;
91
+ if (!contradictOps.has(key)) {
92
+ contradictOps.set(key, op);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ // Second pass: enforce merge-wins-over-delete and deduplicate secondaries.
98
+ //
99
+ // 1. Delete/secondary ordering bug: the per-chunk loop removes delete ops
100
+ // for secondaries that were already in deleteOps, but misses the case
101
+ // where the delete chunk came first. A full sweep here fixes both orders.
102
+ //
103
+ // 2. Cross-merge secondary dedup: if ref A is a secondary in two merge ops,
104
+ // only the first (insertion-order) retains it. Without this, a successful
105
+ // merge credits A to mergedSecondaries and a later merge's emitMerge-
106
+ // FailureSkips also charges A to skipReasons — double-counting A while
107
+ // processed has it only once.
108
+ //
109
+ // 3. Primary-as-secondary dedup: if ref A is a primary in one merge op and
110
+ // a secondary in another, remove A from the secondary list. Both merges
111
+ // would otherwise claim A (merged++ for A, then mergedSecondaries++ for A)
112
+ // breaking the invariant the same way.
113
+ // Also remove delete ops for any ref claimed by a promote op (handles the
114
+ // case where the delete chunk appeared before the promote chunk).
115
+ for (const ref of promoteOps.keys()) {
116
+ deleteOps.delete(ref);
117
+ }
118
+ const claimedSecondaries = new Set();
119
+ for (const mergeOp of mergeOps.values()) {
120
+ deleteOps.delete(mergeOp.primary);
121
+ mergeOp.secondaries = mergeOp.secondaries.filter((sec) => {
122
+ if (mergeOps.has(sec)) {
123
+ warnings.push(`Merge: secondary ${sec} is also a merge primary — removing from secondary list to avoid double-count.`);
124
+ return false;
125
+ }
126
+ if (claimedSecondaries.has(sec)) {
127
+ warnings.push(`Merge: secondary ${sec} appears in multiple merge ops — retaining in first op only.`);
128
+ return false;
129
+ }
130
+ claimedSecondaries.add(sec);
131
+ deleteOps.delete(sec);
132
+ return true;
133
+ });
134
+ }
135
+ // C-2 / #381: promote ops are ordered BEFORE merge ops so that the
136
+ // human-gated proposal queue entry is created before any destructive merge.
137
+ // Phase B processes ops in array order, so promote executes first.
138
+ const ops = [
139
+ ...promoteOps.values(),
140
+ ...mergeOps.values(),
141
+ ...deleteOps.values(),
142
+ ...contradictOps.values(),
143
+ ];
144
+ return { ops, warnings };
145
+ }