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
@@ -3,8 +3,31 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { loadConfig } from "../../core/config/config.js";
5
5
  import { appendEvent } from "../../core/events.js";
6
+ import { getPhaseThreshold, withStateDb } from "../../core/state-db.js";
6
7
  import { info, warn } from "../../core/warn.js";
7
- import { promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
8
+ import { getProposal, promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
9
+ async function sha256Hex(input) {
10
+ const data = new TextEncoder().encode(input);
11
+ const digest = await crypto.subtle.digest("SHA-256", data);
12
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
13
+ }
14
+ /**
15
+ * Derive a stable, low-cardinality reason bucket from an auto-accept promotion
16
+ * error. `promoteProposal` throws a `validateProposal` report formatted as
17
+ * `[kind] message` lines; we extract the first finding kind. Non-validation
18
+ * throws collapse to `promote-error`.
19
+ */
20
+ function classifyPromoteFailure(err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ const finding = /\[([a-z][a-z0-9-]*)\]/i.exec(message);
23
+ if (finding)
24
+ return `validation:${finding[1]}`;
25
+ if (/not pending/i.test(message))
26
+ return "not-pending";
27
+ if (/unknown asset type/i.test(message))
28
+ return "unknown-type";
29
+ return "promote-error";
30
+ }
8
31
  // ---------------------------------------------------------------------------
9
32
  // Gate implementation
10
33
  // ---------------------------------------------------------------------------
@@ -18,13 +41,32 @@ import { promoteProposal, recordGateDecision } from "../proposal/validators/prop
18
41
  * @param promoteFn Injectable override for `promoteProposal` (test seam).
19
42
  */
20
43
  export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProposal) {
21
- const result = { promoted: [], skipped: [], failed: [] };
44
+ const result = {
45
+ promoted: [],
46
+ skipped: [],
47
+ failed: [],
48
+ suppressed: [],
49
+ failedByReason: {},
50
+ failedBySource: {},
51
+ };
22
52
  // --- Guard: gate is disabled or context is incomplete ---
23
53
  if (cfg.dryRun || cfg.globalThreshold === undefined || !cfg.stashDir) {
24
54
  result.skipped = candidates.map((c) => c.proposalId);
25
55
  return result;
26
56
  }
27
- const effectiveThreshold = Math.max(cfg.globalThreshold, cfg.minimumThreshold ?? 0) / 100;
57
+ // WS-4: per-phase threshold from state.db overrides the global threshold.
58
+ // The per-phase value is populated by makeGateConfig when a stateDbPath is
59
+ // available; callers that don't pass it get the global threshold unchanged.
60
+ const resolvedThreshold = cfg.phaseThreshold ?? cfg.globalThreshold;
61
+ const effectiveThreshold = Math.max(resolvedThreshold, cfg.minimumThreshold ?? 0) / 100;
62
+ // WS-4: Exploration budget — promote at most N candidates regardless of
63
+ // confidence to prevent the gate converging to pure exploitation.
64
+ // Exploration candidates are chosen from the LOWEST-confidence eligible set
65
+ // (i.e. those that would be deferred) so the budget truly samples the low-
66
+ // confidence tail and is meaningfully distinct from normal auto-accept.
67
+ // Promoted exploration proposals are logged with eligibilitySource="exploration".
68
+ const explorationBudget = cfg.explorationBudgetCount ?? 0;
69
+ let explorationRemaining = explorationBudget;
28
70
  const resolvedConfig = typeof cfg.config === "function" ? cfg.config() : cfg.config;
29
71
  const gateLabel = `improve:${cfg.phase}`;
30
72
  // #577: stamp the gate's verdict onto each proposal so `akm proposal show`
@@ -40,7 +82,20 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
40
82
  };
41
83
  for (const candidate of candidates) {
42
84
  const { proposalId, confidence } = candidate;
43
- if (confidence === undefined || confidence < effectiveThreshold) {
85
+ let currentProposal;
86
+ try {
87
+ currentProposal = cfg.stashDir ? getProposal(cfg.stashDir, proposalId) : undefined;
88
+ }
89
+ catch {
90
+ currentProposal = undefined;
91
+ }
92
+ const currentContentHash = currentProposal ? await sha256Hex(currentProposal.payload.content) : undefined;
93
+ // Determine if this candidate is exploration-eligible: below-threshold
94
+ // (would normally be deferred) but with a valid confidence score and budget
95
+ // remaining. No-confidence candidates are never exploration-promoted.
96
+ const belowThreshold = confidence === undefined || confidence < effectiveThreshold;
97
+ const isExploration = belowThreshold && confidence !== undefined && explorationRemaining > 0;
98
+ if (belowThreshold && !isExploration) {
44
99
  stamp(proposalId, {
45
100
  outcome: "deferred",
46
101
  reason: confidence === undefined ? "no-confidence" : "below-threshold",
@@ -51,15 +106,32 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
51
106
  result.skipped.push(proposalId);
52
107
  continue;
53
108
  }
109
+ // Either above-threshold (normal auto-accept) or exploration-budget promoted.
110
+ if (isExploration)
111
+ explorationRemaining -= 1;
112
+ const promoteReason = isExploration ? "exploration-budget" : "above-threshold";
113
+ if (currentProposal?.gateDecision?.outcome === "auto-rejected" &&
114
+ currentProposal.gateDecision.contentHash !== undefined &&
115
+ currentProposal.gateDecision.contentHash === currentContentHash) {
116
+ result.suppressed.push(proposalId);
117
+ continue;
118
+ }
54
119
  try {
55
120
  const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
56
121
  stamp(promotion.proposal.id, {
57
122
  outcome: "auto-accepted",
58
- reason: "above-threshold",
123
+ reason: promoteReason,
59
124
  confidence,
60
125
  thresholds: { autoAccept: effectiveThreshold },
126
+ ...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
61
127
  gate: gateLabel,
62
128
  });
129
+ // Resolve the eligibilitySource: exploration-promoted proposals get
130
+ // eligibilitySource="exploration" (WS-4); normal auto-accepts carry
131
+ // whatever the proposal was tagged with at selection time.
132
+ const resolvedEligibilitySource = isExploration
133
+ ? "exploration"
134
+ : promotion.proposal.eligibilitySource;
63
135
  appendEvent({
64
136
  eventType: "promoted",
65
137
  ref: promotion.ref,
@@ -72,14 +144,42 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
72
144
  confidence,
73
145
  threshold: effectiveThreshold,
74
146
  phase: cfg.phase,
147
+ // Attribution tagging: carry the eligibility lane from the proposal
148
+ // record onto the auto-accept promoted event so the lane survives to
149
+ // accept time even when promotion happens in a later run.
150
+ ...(resolvedEligibilitySource !== undefined ? { eligibilitySource: resolvedEligibilitySource } : {}),
151
+ // WS-4: mark exploration promotions so health/telemetry can
152
+ // distinguish them from calibration-signal promotions.
153
+ ...(isExploration ? { explorationBudget: true } : {}),
75
154
  },
76
155
  }, cfg.eventsCtx ?? {});
77
- info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
156
+ if (isExploration) {
157
+ info(`[improve] exploration-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)}; budgetRemaining=${explorationRemaining})`);
158
+ }
159
+ else {
160
+ info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
161
+ }
78
162
  result.promoted.push(proposalId);
79
163
  }
80
164
  catch (err) {
81
- warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
165
+ const reason = classifyPromoteFailure(err);
166
+ warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId} (${reason}): ${err instanceof Error ? err.message : String(err)}`);
82
167
  result.failed.push(proposalId);
168
+ result.failedByReason[reason] = (result.failedByReason[reason] ?? 0) + 1;
169
+ // Record WHY on the proposal so `akm proposal show` explains the rejection
170
+ // and the leak is no longer blind. Best-effort.
171
+ stamp(proposalId, {
172
+ outcome: "auto-rejected",
173
+ reason,
174
+ confidence,
175
+ thresholds: { autoAccept: effectiveThreshold },
176
+ ...(currentContentHash !== undefined ? { contentHash: currentContentHash } : {}),
177
+ gate: gateLabel,
178
+ });
179
+ // If exploration budget was consumed but promotion failed, restore the slot
180
+ // so the budget isn't exhausted on errors.
181
+ if (isExploration)
182
+ explorationRemaining += 1;
83
183
  }
84
184
  }
85
185
  return result;
@@ -108,15 +208,46 @@ export function resolveExtractConfidence(proposal) {
108
208
  /**
109
209
  * Build a gate config for a phase, inheriting global settings from the
110
210
  * improve options. Callers supply only the phase-specific overrides.
211
+ *
212
+ * WS-4 additions:
213
+ * - When `shared.stateDbPath` is provided, reads the persisted per-phase
214
+ * threshold from `improve_gate_thresholds` (Migration 012). The phase
215
+ * value overrides `globalThreshold` but is still floored by
216
+ * `minimumThreshold`. Falls back to `globalThreshold` when no row exists.
217
+ * - Computes `explorationBudgetCount` from
218
+ * `config.improve.exploration.budgetFraction × candidateCount` when the
219
+ * exploration budget is enabled. Defaults to 0 (no exploration).
111
220
  */
112
221
  export function makeGateConfig(phase, shared, overrides = {}) {
222
+ // WS-4: read per-phase threshold from state.db when available.
223
+ let phaseThreshold;
224
+ if (shared.stateDbPath && shared.globalThreshold !== undefined) {
225
+ try {
226
+ phaseThreshold = withStateDb((db) => getPhaseThreshold(db, phase) ?? undefined, {
227
+ path: shared.stateDbPath,
228
+ });
229
+ }
230
+ catch {
231
+ // DB unavailable — fall back to globalThreshold silently.
232
+ }
233
+ }
234
+ // WS-4: compute exploration budget count from config fraction × candidateCount.
235
+ let explorationBudgetCount;
236
+ const resolvedConfig = typeof shared.config === "function" ? shared.config() : shared.config;
237
+ const explorationCfg = resolvedConfig.improve?.exploration;
238
+ if (explorationCfg?.enabled && shared.candidateCount !== undefined && shared.candidateCount > 0) {
239
+ const fraction = Math.min(1, Math.max(0, explorationCfg.budgetFraction ?? 0.05));
240
+ explorationBudgetCount = Math.max(0, Math.floor(fraction * shared.candidateCount));
241
+ }
113
242
  return {
114
243
  phase,
115
244
  globalThreshold: shared.globalThreshold,
245
+ ...(phaseThreshold !== undefined ? { phaseThreshold } : {}),
116
246
  dryRun: shared.dryRun,
117
247
  stashDir: shared.stashDir,
118
248
  config: shared.config,
119
249
  eventsCtx: shared.eventsCtx,
250
+ ...(explorationBudgetCount !== undefined && explorationBudgetCount > 0 ? { explorationBudgetCount } : {}),
120
251
  ...overrides,
121
252
  };
122
253
  }
@@ -13,6 +13,7 @@ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
13
13
  import { getHyphenatedArg, getHyphenatedBoolean, parseFlagValue } from "../../output/context.js";
14
14
  import { akmImprove } from "./improve.js";
15
15
  import { buildImproveRunId, recordTerminatedImproveRun, relativeImproveResultPath, writeImproveResultFile, } from "./improve-result-file.js";
16
+ import { runImproveSession } from "./improve-session.js";
16
17
  export const improveCommand = defineCommand({
17
18
  meta: {
18
19
  name: "improve",
@@ -144,67 +145,44 @@ export const improveCommand = defineCommand({
144
145
  process.stderr.write(`warning: failed to persist terminated improve run ${runId}: ${err instanceof Error ? err.message : String(err)}\n`);
145
146
  }
146
147
  };
147
- // M5 (code-health round 2): signal -> {exit code, reason, ack message}
148
- // as an explicit table instead of three near-identical handlers. The
149
- // persist of the terminated-run row MUST complete before process.exit so
150
- // a SIGTERM'd run (e.g. cron timeout) always leaves a row in
151
- // improve_runs. recordTerminatedImproveRun is fully synchronous
152
- // (bun:sqlite writes are sync), so the in-line call below blocks until
153
- // the row is flushed before we exit.
154
- const SIGNAL_TABLE = {
155
- SIGTERM: { code: 143, reason: "SIGTERM", ack: true },
156
- SIGINT: { code: 130, reason: "SIGINT", ack: true },
157
- SIGHUP: { code: 129, reason: "SIGHUP", ack: false },
158
- };
159
- const makeSignalHandler = (sig) => () => {
160
- const { code, reason, ack } = SIGNAL_TABLE[sig];
161
- // Hard-exit fallback: if the synchronous persist ever hangs (e.g. a
162
- // stuck sqlite lock under contention), the watchdog still exits with
163
- // the correct code instead of leaving a zombie process. .unref() keeps
164
- // the timer from holding the loop open on the normal (fast) path.
165
- const watchdog = setTimeout(() => process.exit(code), 2000);
166
- if (typeof watchdog.unref === "function")
167
- watchdog.unref();
168
- try {
169
- persistTerminated(reason);
170
- }
171
- finally {
172
- clearTimeout(watchdog);
173
- }
174
- if (ack) {
175
- process.stderr.write(`[improve] received ${sig}; recorded terminated run ${runId}\n`);
176
- }
177
- process.exit(code);
178
- };
179
- const sigtermHandler = makeSignalHandler("SIGTERM");
180
- const sigintHandler = makeSignalHandler("SIGINT");
181
- const sighupHandler = makeSignalHandler("SIGHUP");
182
- process.once("SIGTERM", sigtermHandler);
183
- process.once("SIGINT", sigintHandler);
184
- process.once("SIGHUP", sighupHandler);
148
+ // R8: the signal table / handlers / watchdog / persist-before-exit
149
+ // choreography lives in `runImproveSession`. It registers the
150
+ // SIGTERM/SIGINT/SIGHUP handlers (each persists the terminated-run row
151
+ // BEFORE process.exit so a SIGTERM'd run e.g. cron timeout always
152
+ // leaves a row in improve_runs), awaits the work, then removes the
153
+ // handlers on the way out. `onTerminate` persists synchronously
154
+ // (recordTerminatedImproveRun -> bun:sqlite writes are sync), and the
155
+ // 2000ms watchdog inside the session force-exits if that ever hangs.
185
156
  let improveResult;
186
157
  try {
187
- improveResult = await akmImprove({
188
- scope: scopeArg,
189
- task: taskArg,
190
- dryRun,
191
- target: targetArg,
192
- autoAccept,
193
- ...(runId !== undefined ? { runId } : {}),
194
- ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
195
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
196
- ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
197
- ...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
198
- ...(skipIfLocked ? { skipIfLocked } : {}),
199
- ...(profileArg !== undefined ? { profile: profileArg } : {}),
200
- ...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
201
- consolidateOptions: {
202
- target: targetArg,
158
+ improveResult = await runImproveSession({
159
+ runWork: () => akmImprove({
160
+ scope: scopeArg,
161
+ task: taskArg,
203
162
  dryRun,
163
+ target: targetArg,
204
164
  autoAccept,
205
- task: taskArg,
206
- ...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
207
- },
165
+ ...(runId !== undefined ? { runId } : {}),
166
+ ...(limitRaw !== undefined ? { limit: limitRaw } : {}),
167
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
168
+ ...(minRetrievalCount !== undefined ? { minRetrievalCount } : {}),
169
+ ...(requireFeedbackSignal ? { requireFeedbackSignal } : {}),
170
+ ...(skipIfLocked ? { skipIfLocked } : {}),
171
+ ...(profileArg !== undefined ? { profile: profileArg } : {}),
172
+ ...(Object.keys(syncOverride).length > 0 ? { sync: syncOverride } : {}),
173
+ consolidateOptions: {
174
+ target: targetArg,
175
+ dryRun,
176
+ autoAccept,
177
+ task: taskArg,
178
+ ...(consolidateRecovery !== undefined ? { recoveryMode: consolidateRecovery } : {}),
179
+ },
180
+ }),
181
+ }, {
182
+ signalSource: process,
183
+ exit: process.exit,
184
+ onTerminate: (reason) => persistTerminated(reason),
185
+ ack: (message) => process.stderr.write(`[improve] ${message}; recorded terminated run ${runId}\n`),
208
186
  });
209
187
  }
210
188
  catch (err) {
@@ -216,9 +194,6 @@ export const improveCommand = defineCommand({
216
194
  throw err;
217
195
  }
218
196
  finally {
219
- process.removeListener("SIGTERM", sigtermHandler);
220
- process.removeListener("SIGINT", sigintHandler);
221
- process.removeListener("SIGHUP", sighupHandler);
222
197
  clearLogFile();
223
198
  }
224
199
  const durationMs = Date.now() - startedAtMs;
@@ -246,7 +221,7 @@ export const improveCommand = defineCommand({
246
221
  runRecorded = true; // Suppress any late signal-handler write — the success path owns the row now.
247
222
  if (primaryStashDir) {
248
223
  try {
249
- writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso);
224
+ writeImproveResultFile(primaryStashDir, runId, improveResult, startedAtIso, profileArg ?? null);
250
225
  }
251
226
  catch (err) {
252
227
  // Stderr warning on the failure path is preferable to crashing
@@ -8,6 +8,7 @@ import profileFrequent from "../../assets/profiles/frequent.json" with { type: "
8
8
  import profileGraphRefresh from "../../assets/profiles/graph-refresh.json" with { type: "json" };
9
9
  import profileMemoryFocus from "../../assets/profiles/memory-focus.json" with { type: "json" };
10
10
  import profileQuick from "../../assets/profiles/quick.json" with { type: "json" };
11
+ import profileSynthesize from "../../assets/profiles/synthesize.json" with { type: "json" };
11
12
  import profileThorough from "../../assets/profiles/thorough.json" with { type: "json" };
12
13
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
13
14
  import { warn } from "../../core/warn.js";
@@ -31,6 +32,7 @@ const BUILTIN_PROFILES = {
31
32
  frequent: profileFrequent,
32
33
  consolidate: profileConsolidate,
33
34
  catchup: profileCatchup,
35
+ synthesize: profileSynthesize,
34
36
  };
35
37
  /**
36
38
  * Default enabled-state for known improve processes when neither the user
@@ -54,6 +56,18 @@ const IMPROVE_PROCESS_DEFAULTS = {
54
56
  // proposal-queue triage drains the standing backlog. Opt-in (default off),
55
57
  // like `validation` — needs an explicit `enabled: true`.
56
58
  triage: false,
59
+ // Layer 2 proactive-maintenance selector. Opt-in (default off) — surfaces
60
+ // stale high-value assets on a schedule. Enable per-profile with an explicit
61
+ // `processes.proactiveMaintenance.enabled: true`.
62
+ proactiveMaintenance: false,
63
+ // #609 recombine / synthesize pass — whole-corpus cross-episodic
64
+ // generalization. Opt-in (default off); enable per-profile with an explicit
65
+ // `processes.recombine.enabled: true`.
66
+ recombine: false,
67
+ // #615 procedural-compilation pass — detects recurring successful ordered
68
+ // action sequences and compiles them into workflow proposals. Opt-in (default
69
+ // off); enable per-profile with an explicit `processes.procedural.enabled: true`.
70
+ procedural: false,
57
71
  };
58
72
  /**
59
73
  * Compute the effective enabled-state for a named improve process.
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import crypto from "node:crypto";
29
29
  import path from "node:path";
30
- import { openStateDatabase, recordImproveRun } from "../../core/state-db.js";
30
+ import { recordImproveRun, withStateDb } from "../../core/state-db.js";
31
31
  /**
32
32
  * Build a stable run-id for a single improve invocation.
33
33
  *
@@ -72,10 +72,16 @@ export function relativeImproveResultPath(runId) {
72
72
  * dry-run column is indexed so productivity audits can filter cleanly
73
73
  * (closes the dry-run/real-run artifact-trap recorded in MEMORY.md
74
74
  * `feedback_akm_dryrun_artifact_trap`).
75
+ *
76
+ * @param profile - The `--profile` value passed to this invocation (e.g.
77
+ * `quick`, `reflect-distill`), or `null`/`undefined` when no profile was
78
+ * given. Mirrors {@link recordTerminatedImproveRun}'s `ctx.profile`
79
+ * convention so successful and terminated runs are equally queryable by
80
+ * profile. Previously hardcoded to `null` here, which meant only
81
+ * abnormally-terminated runs recorded their profile in state.db.
75
82
  */
76
- export function writeImproveResultFile(stashDir, runId, result, startedAt) {
77
- const db = openStateDatabase();
78
- try {
83
+ export function writeImproveResultFile(stashDir, runId, result, startedAt, profile) {
84
+ withStateDb((db) => {
79
85
  const completedAt = new Date().toISOString();
80
86
  // startedAt is the ISO timestamp captured at process launch (passed from the
81
87
  // CLI entry point). If omitted, fall back to the run-id's embedded timestamp
@@ -88,22 +94,14 @@ export function writeImproveResultFile(stashDir, runId, result, startedAt) {
88
94
  completedAt,
89
95
  stashDir,
90
96
  dryRun: Boolean(result.dryRun),
91
- profile: null,
97
+ profile: profile ?? null,
92
98
  scopeMode: result.scope?.mode ?? "all",
93
99
  scopeValue: result.scope?.value ?? null,
94
100
  guidance: result.guidance ?? null,
95
101
  ok: Boolean(result.ok),
96
102
  result,
97
103
  });
98
- }
99
- finally {
100
- try {
101
- db.close();
102
- }
103
- catch {
104
- // best-effort
105
- }
106
- }
104
+ });
107
105
  return relativeImproveResultPath(runId);
108
106
  }
109
107
  /**
@@ -138,8 +136,7 @@ export function recordTerminatedImproveRun(stashDir, runId, startedAt, reason, c
138
136
  ...(ctx?.errorMessage ? { errorMessage: ctx.errorMessage } : {}),
139
137
  },
140
138
  };
141
- const db = openStateDatabase();
142
- try {
139
+ withStateDb((db) => {
143
140
  recordImproveRun(db, {
144
141
  id: runId,
145
142
  startedAt,
@@ -160,13 +157,5 @@ export function recordTerminatedImproveRun(stashDir, runId, startedAt, reason, c
160
157
  },
161
158
  },
162
159
  });
163
- }
164
- finally {
165
- try {
166
- db.close();
167
- }
168
- catch {
169
- // best-effort
170
- }
171
- }
160
+ });
172
161
  }
@@ -0,0 +1,58 @@
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
+ /** Signal -> {exit code, reason, ack message}. */
5
+ export const SIGNAL_TABLE = {
6
+ SIGTERM: { code: 143, reason: "SIGTERM", ack: true },
7
+ SIGINT: { code: 130, reason: "SIGINT", ack: true },
8
+ SIGHUP: { code: 129, reason: "SIGHUP", ack: false },
9
+ };
10
+ /**
11
+ * Drive the improve-run lifecycle: register signal handlers, await `runWork`,
12
+ * and on clean completion remove the handlers and resolve with the result. The
13
+ * caller (improve-cli.ts) owns success-path result persistence + the final
14
+ * `exit(0)`; this function returns the work result so that choreography stays
15
+ * in the CLI exactly as before.
16
+ *
17
+ * Abnormal paths:
18
+ * - A registered signal fires -> persist terminated row (watchdog-guarded) ->
19
+ * optional ack -> `exit(code)`.
20
+ * - `runWork` rejects -> handlers are removed and the rejection propagates so
21
+ * the CLI's existing catch can persist `"exception"` and rethrow.
22
+ */
23
+ export async function runImproveSession(opts, deps) {
24
+ const { signalSource, exit, onTerminate, ack } = deps;
25
+ const makeSignalHandler = (sig) => () => {
26
+ const { code, reason, ack: shouldAck } = SIGNAL_TABLE[sig];
27
+ // Hard-exit fallback: if the synchronous persist ever hangs (e.g. a stuck
28
+ // sqlite lock under contention), the watchdog still exits with the correct
29
+ // code instead of leaving a zombie process. .unref() keeps the timer from
30
+ // holding the loop open on the normal (fast) path.
31
+ const watchdog = setTimeout(() => exit(code), 2000);
32
+ if (typeof watchdog.unref === "function")
33
+ watchdog.unref();
34
+ try {
35
+ onTerminate(reason);
36
+ }
37
+ finally {
38
+ clearTimeout(watchdog);
39
+ }
40
+ if (shouldAck)
41
+ ack?.(`received ${sig}`);
42
+ exit(code);
43
+ };
44
+ const sigtermHandler = makeSignalHandler("SIGTERM");
45
+ const sigintHandler = makeSignalHandler("SIGINT");
46
+ const sighupHandler = makeSignalHandler("SIGHUP");
47
+ signalSource.once("SIGTERM", sigtermHandler);
48
+ signalSource.once("SIGINT", sigintHandler);
49
+ signalSource.once("SIGHUP", sighupHandler);
50
+ try {
51
+ return await opts.runWork();
52
+ }
53
+ finally {
54
+ signalSource.removeListener("SIGTERM", sigtermHandler);
55
+ signalSource.removeListener("SIGINT", sigintHandler);
56
+ signalSource.removeListener("SIGHUP", sighupHandler);
57
+ }
58
+ }