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

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 (207) hide show
  1. package/CHANGELOG.md +709 -0
  2. package/dist/assets/profiles/default.json +9 -4
  3. package/dist/assets/profiles/frequent.json +1 -1
  4. package/dist/assets/profiles/memory-focus.json +1 -1
  5. package/dist/assets/profiles/quick.json +1 -1
  6. package/dist/assets/profiles/synthesize.json +15 -0
  7. package/dist/assets/profiles/thorough.json +1 -1
  8. package/dist/assets/prompts/consolidate-system.md +23 -0
  9. package/dist/assets/prompts/contradiction-judge.md +33 -0
  10. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  11. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  12. package/dist/assets/prompts/extract-session.md +6 -2
  13. package/dist/assets/prompts/graph-extract-system.md +1 -0
  14. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  15. package/dist/assets/prompts/memory-infer-system.md +1 -0
  16. package/dist/assets/prompts/memory-infer-user.md +5 -0
  17. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  18. package/dist/assets/prompts/procedural-system.md +44 -0
  19. package/dist/assets/prompts/recombine-system.md +40 -0
  20. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  21. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  31. package/dist/assets/templates/html/health.html +281 -111
  32. package/dist/assets/wiki/ingest-workflow-template.md +17 -10
  33. package/dist/cli/shared.js +28 -0
  34. package/dist/cli.js +15 -5
  35. package/dist/commands/agent/agent-dispatch.js +2 -2
  36. package/dist/commands/agent/agent-support.js +0 -7
  37. package/dist/commands/agent/contribute-cli.js +17 -4
  38. package/dist/commands/env/env-cli.js +16 -24
  39. package/dist/commands/env/secret-cli.js +12 -20
  40. package/dist/commands/feedback-cli.js +15 -6
  41. package/dist/commands/graph/graph-cli.js +5 -13
  42. package/dist/commands/graph/graph.js +76 -72
  43. package/dist/commands/health/checks.js +48 -0
  44. package/dist/commands/health/html-report.js +422 -80
  45. package/dist/commands/health.js +386 -9
  46. package/dist/commands/improve/calibration.js +161 -0
  47. package/dist/commands/improve/consolidate/chunking.js +141 -0
  48. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  49. package/dist/commands/improve/consolidate/merge.js +145 -0
  50. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  51. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  52. package/dist/commands/improve/consolidate.js +635 -660
  53. package/dist/commands/improve/dedup.js +482 -0
  54. package/dist/commands/improve/distill.js +159 -69
  55. package/dist/commands/improve/eligibility.js +434 -0
  56. package/dist/commands/improve/encoding-salience.js +205 -0
  57. package/dist/commands/improve/extract-cli.js +124 -2
  58. package/dist/commands/improve/extract-prompt.js +39 -2
  59. package/dist/commands/improve/extract-watch.js +140 -0
  60. package/dist/commands/improve/extract.js +389 -40
  61. package/dist/commands/improve/feedback-valence.js +54 -0
  62. package/dist/commands/improve/homeostatic.js +467 -0
  63. package/dist/commands/improve/improve-auto-accept.js +109 -6
  64. package/dist/commands/improve/improve-cli.js +35 -60
  65. package/dist/commands/improve/improve-profiles.js +14 -0
  66. package/dist/commands/improve/improve-result-file.js +5 -23
  67. package/dist/commands/improve/improve-session.js +58 -0
  68. package/dist/commands/improve/improve.js +485 -2498
  69. package/dist/commands/improve/locks.js +154 -0
  70. package/dist/commands/improve/loop-stages.js +1083 -0
  71. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  72. package/dist/commands/improve/outcome-loop.js +256 -0
  73. package/dist/commands/improve/preparation.js +1966 -0
  74. package/dist/commands/improve/proactive-maintenance.js +115 -0
  75. package/dist/commands/improve/procedural.js +418 -0
  76. package/dist/commands/improve/recombine.js +813 -0
  77. package/dist/commands/improve/reflect-noise.js +0 -0
  78. package/dist/commands/improve/reflect.js +183 -40
  79. package/dist/commands/improve/salience.js +438 -0
  80. package/dist/commands/improve/triage.js +93 -0
  81. package/dist/commands/lint/agent-linter.js +19 -24
  82. package/dist/commands/lint/base-linter.js +173 -60
  83. package/dist/commands/lint/command-linter.js +19 -24
  84. package/dist/commands/lint/env-key-rules.js +34 -1
  85. package/dist/commands/lint/fact-linter.js +39 -0
  86. package/dist/commands/lint/index.js +31 -13
  87. package/dist/commands/lint/memory-linter.js +1 -1
  88. package/dist/commands/lint/registry.js +7 -2
  89. package/dist/commands/lint/task-linter.js +3 -3
  90. package/dist/commands/lint/workflow-linter.js +26 -1
  91. package/dist/commands/proposal/drain-policies.js +5 -0
  92. package/dist/commands/proposal/drain.js +43 -50
  93. package/dist/commands/proposal/proposal-cli.js +21 -31
  94. package/dist/commands/proposal/proposal.js +5 -0
  95. package/dist/commands/proposal/propose.js +7 -2
  96. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  97. package/dist/commands/proposal/validators/proposals.js +189 -63
  98. package/dist/commands/read/curate.js +414 -94
  99. package/dist/commands/read/knowledge.js +2 -2
  100. package/dist/commands/read/search-cli.js +7 -0
  101. package/dist/commands/read/search.js +1 -0
  102. package/dist/commands/read/show.js +67 -2
  103. package/dist/commands/sources/init.js +36 -9
  104. package/dist/commands/sources/installed-stashes.js +5 -1
  105. package/dist/commands/sources/schema-repair.js +13 -1
  106. package/dist/commands/sources/self-update.js +2 -2
  107. package/dist/commands/sources/stash-cli.js +28 -40
  108. package/dist/commands/sources/stash-skeleton.js +23 -8
  109. package/dist/commands/tasks/tasks-cli.js +19 -27
  110. package/dist/commands/tasks/tasks.js +1 -1
  111. package/dist/commands/wiki-cli.js +21 -35
  112. package/dist/core/asset/asset-registry.js +2 -0
  113. package/dist/core/asset/asset-spec.js +14 -0
  114. package/dist/core/asset/frontmatter.js +166 -167
  115. package/dist/core/asset/markdown.js +8 -0
  116. package/dist/core/authoring-rules.js +92 -0
  117. package/dist/core/common.js +0 -5
  118. package/dist/core/config/config-schema.js +340 -56
  119. package/dist/core/config/config-types.js +3 -3
  120. package/dist/core/config/config.js +28 -7
  121. package/dist/core/events.js +3 -7
  122. package/dist/core/improve-types.js +11 -8
  123. package/dist/core/logs-db.js +10 -66
  124. package/dist/core/parse.js +36 -16
  125. package/dist/core/paths.js +3 -0
  126. package/dist/core/standards/resolve-standards-context.js +87 -0
  127. package/dist/core/standards/resolve-stash-standards.js +99 -0
  128. package/dist/core/standards/resolve-type-conventions.js +66 -0
  129. package/dist/core/state/migrations.js +714 -0
  130. package/dist/core/state-db.js +525 -474
  131. package/dist/indexer/db/db.js +439 -247
  132. package/dist/indexer/db/graph-db.js +129 -86
  133. package/dist/indexer/ensure-index.js +152 -17
  134. package/dist/indexer/graph/graph-boost.js +51 -41
  135. package/dist/indexer/graph/graph-extraction.js +218 -4
  136. package/dist/indexer/index-writer-lock.js +99 -0
  137. package/dist/indexer/indexer.js +123 -221
  138. package/dist/indexer/passes/dir-staleness.js +114 -0
  139. package/dist/indexer/passes/memory-inference.js +10 -3
  140. package/dist/indexer/passes/staleness-detect.js +2 -5
  141. package/dist/indexer/search/db-search.js +15 -4
  142. package/dist/indexer/search/ranking-contributors.js +22 -0
  143. package/dist/indexer/search/ranking.js +4 -0
  144. package/dist/indexer/search/search-source.js +10 -24
  145. package/dist/indexer/search/semantic-status.js +4 -0
  146. package/dist/indexer/walk/matchers.js +9 -0
  147. package/dist/integrations/agent/config.js +6 -53
  148. package/dist/integrations/agent/index.js +2 -18
  149. package/dist/integrations/agent/prompts.js +74 -8
  150. package/dist/integrations/agent/runner-dispatch.js +59 -0
  151. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  152. package/dist/integrations/harnesses/index.js +2 -3
  153. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  154. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  155. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  156. package/dist/integrations/session-logs/index.js +16 -0
  157. package/dist/llm/client.js +45 -15
  158. package/dist/llm/embedder.js +42 -3
  159. package/dist/llm/embedders/deterministic.js +66 -0
  160. package/dist/llm/embedders/local.js +66 -2
  161. package/dist/llm/feature-gate.js +8 -4
  162. package/dist/llm/graph-extract.js +67 -44
  163. package/dist/llm/memory-infer.js +38 -30
  164. package/dist/llm/metadata-enhance.js +44 -31
  165. package/dist/llm/structured-call.js +49 -0
  166. package/dist/output/context.js +5 -5
  167. package/dist/output/renderers.js +73 -1
  168. package/dist/output/shapes/curate.js +14 -2
  169. package/dist/output/shapes/passthrough.js +0 -1
  170. package/dist/output/text/helpers.js +16 -1
  171. package/dist/registry/providers/skills-sh.js +21 -147
  172. package/dist/registry/providers/static-index.js +15 -157
  173. package/dist/registry/resolve.js +22 -9
  174. package/dist/runtime.js +25 -1
  175. package/dist/scripts/migrate-storage.js +2136 -1596
  176. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +682 -433
  177. package/dist/setup/setup.js +29 -8
  178. package/dist/sources/providers/filesystem.js +0 -1
  179. package/dist/sources/providers/git-install.js +206 -0
  180. package/dist/sources/providers/git-provider.js +234 -0
  181. package/dist/sources/providers/git-stash.js +248 -0
  182. package/dist/sources/providers/git.js +10 -661
  183. package/dist/sources/providers/npm.js +2 -6
  184. package/dist/sources/providers/sync-from-ref.js +9 -1
  185. package/dist/sources/providers/tar-utils.js +16 -8
  186. package/dist/sources/providers/website.js +2 -3
  187. package/dist/sources/website-ingest.js +51 -9
  188. package/dist/sources/wiki-fetchers/registry.js +53 -0
  189. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  190. package/dist/storage/database.js +45 -10
  191. package/dist/storage/managed-db.js +82 -0
  192. package/dist/storage/repositories/registry-cache.js +92 -0
  193. package/dist/storage/sqlite-pragmas.js +146 -0
  194. package/dist/tasks/backends/cron.js +1 -1
  195. package/dist/tasks/backends/launchd.js +1 -1
  196. package/dist/tasks/backends/schtasks.js +1 -1
  197. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  198. package/dist/tasks/runner.js +5 -13
  199. package/dist/wiki/wiki.js +37 -0
  200. package/dist/workflows/db.js +3 -4
  201. package/dist/workflows/runtime/runs.js +1 -117
  202. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  203. package/dist/workflows/validate-summary.js +2 -7
  204. package/docs/data-and-telemetry.md +1 -0
  205. package/package.json +9 -7
  206. package/dist/commands/db-cli.js +0 -23
  207. package/dist/indexer/db/db-backup.js +0 -376
@@ -3,8 +3,26 @@
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
8
  import { promoteProposal, recordGateDecision } from "../proposal/validators/proposals.js";
9
+ /**
10
+ * Derive a stable, low-cardinality reason bucket from an auto-accept promotion
11
+ * error. `promoteProposal` throws a `validateProposal` report formatted as
12
+ * `[kind] message` lines; we extract the first finding kind. Non-validation
13
+ * throws collapse to `promote-error`.
14
+ */
15
+ function classifyPromoteFailure(err) {
16
+ const message = err instanceof Error ? err.message : String(err);
17
+ const finding = /\[([a-z][a-z0-9-]*)\]/i.exec(message);
18
+ if (finding)
19
+ return `validation:${finding[1]}`;
20
+ if (/not pending/i.test(message))
21
+ return "not-pending";
22
+ if (/unknown asset type/i.test(message))
23
+ return "unknown-type";
24
+ return "promote-error";
25
+ }
8
26
  // ---------------------------------------------------------------------------
9
27
  // Gate implementation
10
28
  // ---------------------------------------------------------------------------
@@ -18,13 +36,25 @@ import { promoteProposal, recordGateDecision } from "../proposal/validators/prop
18
36
  * @param promoteFn Injectable override for `promoteProposal` (test seam).
19
37
  */
20
38
  export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProposal) {
21
- const result = { promoted: [], skipped: [], failed: [] };
39
+ const result = { promoted: [], skipped: [], failed: [], failedByReason: {} };
22
40
  // --- Guard: gate is disabled or context is incomplete ---
23
41
  if (cfg.dryRun || cfg.globalThreshold === undefined || !cfg.stashDir) {
24
42
  result.skipped = candidates.map((c) => c.proposalId);
25
43
  return result;
26
44
  }
27
- const effectiveThreshold = Math.max(cfg.globalThreshold, cfg.minimumThreshold ?? 0) / 100;
45
+ // WS-4: per-phase threshold from state.db overrides the global threshold.
46
+ // The per-phase value is populated by makeGateConfig when a stateDbPath is
47
+ // available; callers that don't pass it get the global threshold unchanged.
48
+ const resolvedThreshold = cfg.phaseThreshold ?? cfg.globalThreshold;
49
+ const effectiveThreshold = Math.max(resolvedThreshold, cfg.minimumThreshold ?? 0) / 100;
50
+ // WS-4: Exploration budget — promote at most N candidates regardless of
51
+ // confidence to prevent the gate converging to pure exploitation.
52
+ // Exploration candidates are chosen from the LOWEST-confidence eligible set
53
+ // (i.e. those that would be deferred) so the budget truly samples the low-
54
+ // confidence tail and is meaningfully distinct from normal auto-accept.
55
+ // Promoted exploration proposals are logged with eligibilitySource="exploration".
56
+ const explorationBudget = cfg.explorationBudgetCount ?? 0;
57
+ let explorationRemaining = explorationBudget;
28
58
  const resolvedConfig = typeof cfg.config === "function" ? cfg.config() : cfg.config;
29
59
  const gateLabel = `improve:${cfg.phase}`;
30
60
  // #577: stamp the gate's verdict onto each proposal so `akm proposal show`
@@ -40,7 +70,12 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
40
70
  };
41
71
  for (const candidate of candidates) {
42
72
  const { proposalId, confidence } = candidate;
43
- if (confidence === undefined || confidence < effectiveThreshold) {
73
+ // Determine if this candidate is exploration-eligible: below-threshold
74
+ // (would normally be deferred) but with a valid confidence score and budget
75
+ // remaining. No-confidence candidates are never exploration-promoted.
76
+ const belowThreshold = confidence === undefined || confidence < effectiveThreshold;
77
+ const isExploration = belowThreshold && confidence !== undefined && explorationRemaining > 0;
78
+ if (belowThreshold && !isExploration) {
44
79
  stamp(proposalId, {
45
80
  outcome: "deferred",
46
81
  reason: confidence === undefined ? "no-confidence" : "below-threshold",
@@ -51,15 +86,25 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
51
86
  result.skipped.push(proposalId);
52
87
  continue;
53
88
  }
89
+ // Either above-threshold (normal auto-accept) or exploration-budget promoted.
90
+ if (isExploration)
91
+ explorationRemaining -= 1;
92
+ const promoteReason = isExploration ? "exploration-budget" : "above-threshold";
54
93
  try {
55
94
  const promotion = await promoteFn(cfg.stashDir, resolvedConfig, proposalId, {}, undefined);
56
95
  stamp(promotion.proposal.id, {
57
96
  outcome: "auto-accepted",
58
- reason: "above-threshold",
97
+ reason: promoteReason,
59
98
  confidence,
60
99
  thresholds: { autoAccept: effectiveThreshold },
61
100
  gate: gateLabel,
62
101
  });
102
+ // Resolve the eligibilitySource: exploration-promoted proposals get
103
+ // eligibilitySource="exploration" (WS-4); normal auto-accepts carry
104
+ // whatever the proposal was tagged with at selection time.
105
+ const resolvedEligibilitySource = isExploration
106
+ ? "exploration"
107
+ : promotion.proposal.eligibilitySource;
63
108
  appendEvent({
64
109
  eventType: "promoted",
65
110
  ref: promotion.ref,
@@ -72,14 +117,41 @@ export async function runAutoAcceptGate(candidates, cfg, promoteFn = promoteProp
72
117
  confidence,
73
118
  threshold: effectiveThreshold,
74
119
  phase: cfg.phase,
120
+ // Attribution tagging: carry the eligibility lane from the proposal
121
+ // record onto the auto-accept promoted event so the lane survives to
122
+ // accept time even when promotion happens in a later run.
123
+ ...(resolvedEligibilitySource !== undefined ? { eligibilitySource: resolvedEligibilitySource } : {}),
124
+ // WS-4: mark exploration promotions so health/telemetry can
125
+ // distinguish them from calibration-signal promotions.
126
+ ...(isExploration ? { explorationBudget: true } : {}),
75
127
  },
76
128
  }, cfg.eventsCtx ?? {});
77
- info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
129
+ if (isExploration) {
130
+ info(`[improve] exploration-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)}; budgetRemaining=${explorationRemaining})`);
131
+ }
132
+ else {
133
+ info(`[improve] auto-accepted ${promotion.ref} (${cfg.phase}; confidence=${confidence.toFixed(2)} >= threshold=${effectiveThreshold.toFixed(2)})`);
134
+ }
78
135
  result.promoted.push(proposalId);
79
136
  }
80
137
  catch (err) {
81
- warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId}: ${err instanceof Error ? err.message : String(err)}`);
138
+ const reason = classifyPromoteFailure(err);
139
+ warn(`[improve] ${cfg.phase} auto-accept failed for ${proposalId} (${reason}): ${err instanceof Error ? err.message : String(err)}`);
82
140
  result.failed.push(proposalId);
141
+ result.failedByReason[reason] = (result.failedByReason[reason] ?? 0) + 1;
142
+ // Record WHY on the proposal so `akm proposal show` explains the rejection
143
+ // and the leak is no longer blind. Best-effort.
144
+ stamp(proposalId, {
145
+ outcome: "auto-rejected",
146
+ reason,
147
+ confidence,
148
+ thresholds: { autoAccept: effectiveThreshold },
149
+ gate: gateLabel,
150
+ });
151
+ // If exploration budget was consumed but promotion failed, restore the slot
152
+ // so the budget isn't exhausted on errors.
153
+ if (isExploration)
154
+ explorationRemaining += 1;
83
155
  }
84
156
  }
85
157
  return result;
@@ -108,15 +180,46 @@ export function resolveExtractConfidence(proposal) {
108
180
  /**
109
181
  * Build a gate config for a phase, inheriting global settings from the
110
182
  * improve options. Callers supply only the phase-specific overrides.
183
+ *
184
+ * WS-4 additions:
185
+ * - When `shared.stateDbPath` is provided, reads the persisted per-phase
186
+ * threshold from `improve_gate_thresholds` (Migration 012). The phase
187
+ * value overrides `globalThreshold` but is still floored by
188
+ * `minimumThreshold`. Falls back to `globalThreshold` when no row exists.
189
+ * - Computes `explorationBudgetCount` from
190
+ * `config.improve.exploration.budgetFraction × candidateCount` when the
191
+ * exploration budget is enabled. Defaults to 0 (no exploration).
111
192
  */
112
193
  export function makeGateConfig(phase, shared, overrides = {}) {
194
+ // WS-4: read per-phase threshold from state.db when available.
195
+ let phaseThreshold;
196
+ if (shared.stateDbPath && shared.globalThreshold !== undefined) {
197
+ try {
198
+ phaseThreshold = withStateDb((db) => getPhaseThreshold(db, phase) ?? undefined, {
199
+ path: shared.stateDbPath,
200
+ });
201
+ }
202
+ catch {
203
+ // DB unavailable — fall back to globalThreshold silently.
204
+ }
205
+ }
206
+ // WS-4: compute exploration budget count from config fraction × candidateCount.
207
+ let explorationBudgetCount;
208
+ const resolvedConfig = typeof shared.config === "function" ? shared.config() : shared.config;
209
+ const explorationCfg = resolvedConfig.improve?.exploration;
210
+ if (explorationCfg?.enabled && shared.candidateCount !== undefined && shared.candidateCount > 0) {
211
+ const fraction = Math.min(1, Math.max(0, explorationCfg.budgetFraction ?? 0.05));
212
+ explorationBudgetCount = Math.max(0, Math.floor(fraction * shared.candidateCount));
213
+ }
113
214
  return {
114
215
  phase,
115
216
  globalThreshold: shared.globalThreshold,
217
+ ...(phaseThreshold !== undefined ? { phaseThreshold } : {}),
116
218
  dryRun: shared.dryRun,
117
219
  stashDir: shared.stashDir,
118
220
  config: shared.config,
119
221
  eventsCtx: shared.eventsCtx,
222
+ ...(explorationBudgetCount !== undefined && explorationBudgetCount > 0 ? { explorationBudgetCount } : {}),
120
223
  ...overrides,
121
224
  };
122
225
  }
@@ -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;
@@ -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
  *
@@ -74,8 +74,7 @@ export function relativeImproveResultPath(runId) {
74
74
  * `feedback_akm_dryrun_artifact_trap`).
75
75
  */
76
76
  export function writeImproveResultFile(stashDir, runId, result, startedAt) {
77
- const db = openStateDatabase();
78
- try {
77
+ withStateDb((db) => {
79
78
  const completedAt = new Date().toISOString();
80
79
  // startedAt is the ISO timestamp captured at process launch (passed from the
81
80
  // CLI entry point). If omitted, fall back to the run-id's embedded timestamp
@@ -95,15 +94,7 @@ export function writeImproveResultFile(stashDir, runId, result, startedAt) {
95
94
  ok: Boolean(result.ok),
96
95
  result,
97
96
  });
98
- }
99
- finally {
100
- try {
101
- db.close();
102
- }
103
- catch {
104
- // best-effort
105
- }
106
- }
97
+ });
107
98
  return relativeImproveResultPath(runId);
108
99
  }
109
100
  /**
@@ -138,8 +129,7 @@ export function recordTerminatedImproveRun(stashDir, runId, startedAt, reason, c
138
129
  ...(ctx?.errorMessage ? { errorMessage: ctx.errorMessage } : {}),
139
130
  },
140
131
  };
141
- const db = openStateDatabase();
142
- try {
132
+ withStateDb((db) => {
143
133
  recordImproveRun(db, {
144
134
  id: runId,
145
135
  startedAt,
@@ -160,13 +150,5 @@ export function recordTerminatedImproveRun(stashDir, runId, startedAt, reason, c
160
150
  },
161
151
  },
162
152
  });
163
- }
164
- finally {
165
- try {
166
- db.close();
167
- }
168
- catch {
169
- // best-effort
170
- }
171
- }
153
+ });
172
154
  }
@@ -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
+ }