akm-cli 0.9.31 → 0.10.26101001-alpha

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 (234) hide show
  1. package/CHANGELOG.md +253 -0
  2. package/STABILITY.md +79 -53
  3. package/dist/api-node.mjs +20 -0
  4. package/dist/api.d.ts +23 -0
  5. package/dist/api.js +45 -0
  6. package/dist/assets/hints/cli-hints-full.md +13 -27
  7. package/dist/assets/hints/cli-hints-short.md +1 -1
  8. package/dist/assets/improve-strategies/default.json +3 -3
  9. package/dist/assets/prompts/consolidate-pair.md +9 -7
  10. package/dist/cli/shared.js +31 -48
  11. package/dist/cli/unknown-flags.js +6 -43
  12. package/dist/cli.js +8 -34
  13. package/dist/commands/agent/contribute-cli.js +1 -1
  14. package/dist/commands/completions.js +1 -2
  15. package/dist/commands/config-cli.js +5 -28
  16. package/dist/commands/env/env-cli.js +5 -5
  17. package/dist/commands/env/secret-cli.js +2 -2
  18. package/dist/commands/health/checks.js +41 -16
  19. package/dist/commands/health/plugin-staleness.js +52 -22
  20. package/dist/commands/improve/autonomy-gate.js +5 -3
  21. package/dist/commands/improve/consolidate/pair-pass.js +7 -1
  22. package/dist/commands/improve/consolidate.js +6 -35
  23. package/dist/commands/improve/distill/lesson-update.js +91 -0
  24. package/dist/commands/improve/distill-guards.js +1 -43
  25. package/dist/commands/improve/distill.js +140 -20
  26. package/dist/commands/improve/extract-cli.js +1 -1
  27. package/dist/commands/improve/improve-cli.js +87 -102
  28. package/dist/commands/improve/improve-strategies.js +8 -12
  29. package/dist/commands/improve/loop-stages.js +0 -1
  30. package/dist/commands/improve/planner.js +0 -1
  31. package/dist/commands/improve/preparation.js +40 -99
  32. package/dist/commands/improve/proactive-maintenance.js +2 -2
  33. package/dist/commands/improve/reflect-noise.js +3 -55
  34. package/dist/commands/improve/reflect.js +3 -8
  35. package/dist/commands/improve/retrieval-scope.js +2 -2
  36. package/dist/commands/improve/salience.js +2 -9
  37. package/dist/commands/improve/stage.js +14 -9
  38. package/dist/commands/lint/index.js +5 -20
  39. package/dist/commands/observability-cli.js +3 -9
  40. package/dist/commands/proposal/drain.js +39 -6
  41. package/dist/commands/proposal/proposal-cli.js +10 -30
  42. package/dist/commands/proposal/repository.js +3 -3
  43. package/dist/commands/read/knowledge.js +3 -3
  44. package/dist/commands/read/remember-cli.js +0 -13
  45. package/dist/commands/read/search-cli.js +29 -97
  46. package/dist/commands/read/search.js +0 -11
  47. package/dist/commands/read/show.js +0 -19
  48. package/dist/commands/sources/plugin-upgrade.js +164 -101
  49. package/dist/commands/sources/sources-cli.js +1 -15
  50. package/dist/commands/sources/stash-cli.js +4 -12
  51. package/dist/commands/tasks/tasks-cli.js +4 -115
  52. package/dist/commands/workflow/plan.js +11 -33
  53. package/dist/commands/workflow-cli.js +2 -7
  54. package/dist/core/adapter/adapter-ids.js +1 -2
  55. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  56. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  57. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  58. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  59. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  60. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  61. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  62. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  63. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  64. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  65. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  66. package/dist/core/adapter/recognize-match.js +3 -4
  67. package/dist/core/common.js +18 -2
  68. package/dist/core/config/config-schema.js +12 -15
  69. package/dist/core/config/config-types.js +6 -15
  70. package/dist/core/config/config-walker.js +6 -23
  71. package/dist/core/config/config.js +17 -80
  72. package/dist/core/config/engine-semantics.js +7 -3
  73. package/dist/core/config/schema/engines.js +7 -7
  74. package/dist/core/config/schema/improve-processes.js +8 -61
  75. package/dist/core/config/schema/improve.js +2 -21
  76. package/dist/core/config/schema/index-config.js +6 -39
  77. package/dist/core/env-secret-ref.js +2 -45
  78. package/dist/core/errors.js +1 -2
  79. package/dist/core/extra-params.js +2 -86
  80. package/dist/core/improve-result.js +11 -1
  81. package/dist/core/mutation-target.js +2 -2
  82. package/dist/core/recognition-util.js +4 -5
  83. package/dist/core/warn.js +21 -4
  84. package/dist/core/write-source.js +3 -3
  85. package/dist/execution/target-ref.js +1 -2
  86. package/dist/indexer/search/db-search.js +4 -19
  87. package/dist/indexer/search/fts-query.js +0 -19
  88. package/dist/integrations/agent/engine-resolution.js +5 -2
  89. package/dist/integrations/agent/index.js +1 -1
  90. package/dist/integrations/agent/profiles.js +41 -98
  91. package/dist/integrations/agent/request-lowering.js +3 -3
  92. package/dist/integrations/harnesses/aider/index.js +5 -0
  93. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  94. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  95. package/dist/integrations/harnesses/claude/index.js +5 -0
  96. package/dist/integrations/harnesses/codex/index.js +1 -0
  97. package/dist/integrations/harnesses/copilot/index.js +1 -0
  98. package/dist/integrations/harnesses/gemini/index.js +1 -0
  99. package/dist/integrations/harnesses/index.js +18 -39
  100. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  101. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  102. package/dist/integrations/harnesses/opencode/index.js +22 -0
  103. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  104. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  105. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  106. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  107. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  108. package/dist/integrations/harnesses/opencode/version.js +98 -0
  109. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  110. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  111. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  112. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +228 -0
  113. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  114. package/dist/integrations/harnesses/openhands/index.js +1 -0
  115. package/dist/integrations/harnesses/pi/index.js +1 -0
  116. package/dist/integrations/harnesses/shared.js +2 -0
  117. package/dist/integrations/harnesses/types.js +2 -0
  118. package/dist/llm/client.js +41 -11
  119. package/dist/output/cli-hints.js +1 -1
  120. package/dist/output/context.js +10 -19
  121. package/dist/output/format-exempt.js +5 -8
  122. package/dist/output/shapes/helpers.js +1 -22
  123. package/dist/output/shapes/passthrough.js +0 -1
  124. package/dist/output/shapes.js +3 -31
  125. package/dist/output/stdout.js +5 -1
  126. package/dist/output/text/command-format.js +6 -1
  127. package/dist/output/text/workflow-format.js +4 -22
  128. package/dist/scripts/akm-migrate-node.js +13049 -14013
  129. package/dist/scripts/akm-migrate.js +16316 -17276
  130. package/dist/setup/detect.js +10 -4
  131. package/dist/setup/harness-config-import.js +5 -14
  132. package/dist/setup/setup.js +1 -1
  133. package/dist/setup/steps/tasks.js +12 -13
  134. package/dist/storage/repositories/task-history-repository.js +0 -5
  135. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  136. package/dist/tasks/embedded.js +2 -1
  137. package/dist/tasks/run/task-history.js +0 -2
  138. package/dist/tasks/schedule-gates.js +39 -0
  139. package/dist/tasks/scheduler-binding.js +0 -16
  140. package/dist/tasks/scheduler-sync.js +3 -97
  141. package/dist/tasks/source/bounded-document.js +1 -1
  142. package/dist/tasks/source/task-source-v4.js +2 -12
  143. package/dist/tasks/task-run-reserved-flags.js +2 -31
  144. package/dist/workflows/authoring/authoring.js +6 -6
  145. package/dist/workflows/compile.js +8 -65
  146. package/dist/workflows/exec/child-workflow.js +22 -3
  147. package/dist/workflows/exec/native-executor.js +12 -147
  148. package/dist/workflows/exec/run-workflow.js +31 -103
  149. package/dist/workflows/exec/step-work.js +9 -178
  150. package/dist/workflows/exec/unit-dispatch.js +0 -10
  151. package/dist/workflows/freeze/environment.js +3 -44
  152. package/dist/workflows/freeze/freeze.js +8 -41
  153. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  154. package/dist/workflows/freeze/targets/command.js +2 -16
  155. package/dist/workflows/freeze/targets/shell.js +0 -2
  156. package/dist/workflows/freeze/task-bindings.js +2 -28
  157. package/dist/workflows/ir/params.js +12 -2
  158. package/dist/workflows/parser.js +82 -232
  159. package/dist/workflows/plan.js +1 -1
  160. package/dist/workflows/renderer.js +10 -19
  161. package/dist/workflows/runtime/run-plan.js +11 -53
  162. package/dist/workflows/runtime/runs.js +4 -27
  163. package/dist/workflows/source-files.js +12 -16
  164. package/dist/workflows/source-semantics.js +1 -93
  165. package/docs/README.md +1 -1
  166. package/docs/agents/README.md +3 -3
  167. package/docs/agents/agent-install.md +1 -1
  168. package/docs/architecture/adapters.md +17 -31
  169. package/docs/architecture/architecture.md +4 -5
  170. package/docs/architecture/improvement.md +52 -21
  171. package/docs/architecture/internals/improve-workflow.md +15 -8
  172. package/docs/architecture/internals/indexing.md +1 -1
  173. package/docs/architecture/internals/search.md +1 -2
  174. package/docs/architecture/internals/storage-locations.md +5 -5
  175. package/docs/architecture/persisted-data-compat.md +13 -4
  176. package/docs/architecture/specs/0.9.0-decisions.md +6 -2
  177. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
  178. package/docs/architecture/specs/workflow-format-unification.md +1 -6
  179. package/docs/architecture/testing/manual-testing-checklist.md +20 -29
  180. package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
  181. package/docs/architecture/testing/testing-workflow.md +1 -1
  182. package/docs/architecture/workflow-engine.md +5 -56
  183. package/docs/guides/author-workflows.md +39 -69
  184. package/docs/guides/capture-knowledge.md +1 -1
  185. package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
  186. package/docs/guides/discover-and-load.md +4 -4
  187. package/docs/guides/getting-started.md +1 -1
  188. package/docs/guides/improve-the-library.md +7 -7
  189. package/docs/guides/recipes/headless-install.md +2 -2
  190. package/docs/guides/run-workflows.md +1 -1
  191. package/docs/guides/use-with-any-agent.md +49 -4
  192. package/docs/integration/bundling-akm.md +7 -6
  193. package/docs/maintainers/curate-workmap.md +1 -2
  194. package/docs/maintainers/release-checklist.md +30 -1
  195. package/docs/posts/akm-command-tour.md +3 -3
  196. package/docs/product-surface.md +1 -1
  197. package/docs/reference/README.md +1 -0
  198. package/docs/reference/api.md +65 -0
  199. package/docs/reference/asset-types.md +3 -5
  200. package/docs/reference/bundle-types.md +2 -4
  201. package/docs/reference/cli.md +126 -171
  202. package/docs/reference/configuration.md +84 -19
  203. package/docs/reference/data-and-telemetry.md +1 -1
  204. package/docs/reference/env-and-secrets.md +2 -2
  205. package/docs/reference/supported-formats.md +3 -4
  206. package/docs/reference/tasks.md +12 -19
  207. package/docs/reference/website-sources.md +1 -1
  208. package/docs/reference/workflow-schema.md +89 -317
  209. package/docs/reference/workflows.md +20 -48
  210. package/package.json +12 -2
  211. package/schemas/akm-config.json +2 -243
  212. package/schemas/akm-task.json +2 -3
  213. package/schemas/akm-workflow.json +47 -84
  214. package/dist/assets/improve-strategies/catchup.json +0 -41
  215. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  216. package/dist/assets/improve-strategies/quick.json +0 -17
  217. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  218. package/dist/assets/improve-strategies/thorough.json +0 -49
  219. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  220. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  221. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  222. package/dist/cli/retired-commands.js +0 -118
  223. package/dist/commands/tasks/validate.js +0 -176
  224. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  225. package/dist/integrations/harnesses/ids.js +0 -28
  226. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  227. package/dist/tasks/source/task-to-v4.js +0 -863
  228. package/dist/workflows/exec/worktree.js +0 -372
  229. package/dist/workflows/freeze/child-output-references.js +0 -83
  230. package/dist/workflows/freeze/identity.js +0 -22
  231. package/dist/workflows/freeze/targets/script.js +0 -50
  232. package/dist/workflows/freeze/targets/task.js +0 -124
  233. package/dist/workflows/github-yaml.js +0 -506
  234. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -11,6 +11,8 @@ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js"
11
11
  import { isLlmCredentialAvailable, resolveEngine, } from "../../integrations/agent/engine-resolution.js";
12
12
  import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/execution.js";
13
13
  import { loadModelMap, mergeModelMapLayers, parseModelMapLayer, readInstalledModelMapText, resolveModelMapAlias, userModelMapPath, } from "../../integrations/agent/model-map.js";
14
+ import { detectOpencodeMajor, resolveOpencodeBin, } from "../../integrations/harnesses/opencode/version.js";
15
+ import { OPENCODE_CLIENT_PACKAGE } from "../../integrations/harnesses/opencode-sdk/wire.js";
14
16
  import { probeEndpointOnce } from "../../llm/client.js";
15
17
  import { STATE_DB_FREELIST_WARN_RATIO, } from "../../storage/state-db-integrity.js";
16
18
  import { listKeys } from "../env/env.js";
@@ -116,20 +118,15 @@ async function runConfiguredEngineProbe(checkName, engineName, config, deps, rea
116
118
  const env = deps.env ?? process.env;
117
119
  const configuredEngine = config.engines?.[engineName];
118
120
  if (configuredEngine?.kind === "agent" && configuredEngine.platform === "opencode-sdk") {
119
- let packageAvailable = false;
121
+ const binary = resolveOpencodeBin(configuredEngine.bin);
122
+ let detected;
120
123
  try {
121
- const resolvePackage = deps.resolvePackage ?? ((name) => import.meta.resolve(name));
122
- resolvePackage("@opencode-ai/sdk");
123
- packageAvailable = true;
124
- }
125
- catch {
126
- packageAvailable = false;
127
- }
128
- const binary = configuredEngine.bin ?? "opencode";
129
- let binaryAvailable;
130
- try {
131
- const version = (deps.spawnSync ?? spawnSync)(binary, ["--version"], { encoding: "utf8", timeout: 5_000 });
132
- binaryAvailable = (version.status ?? 1) === 0;
124
+ detected = detectOpencodeMajor(binary, {
125
+ probe: (bin) => {
126
+ const run = (deps.spawnSync ?? spawnSync)(bin, ["--version"], { encoding: "utf8", timeout: 5_000 });
127
+ return (run.status ?? 1) === 0 ? String(run.stdout ?? "").trim() : undefined;
128
+ },
129
+ });
133
130
  }
134
131
  catch {
135
132
  return {
@@ -141,6 +138,18 @@ async function runConfiguredEngineProbe(checkName, engineName, config, deps, rea
141
138
  evidence: { engine: engineName, runtimeKind: "sdk", binaryAvailable: false },
142
139
  };
143
140
  }
141
+ const binaryAvailable = detected.runnable;
142
+ // The client package of the OpenCode major the binary reports; never the other major's.
143
+ const clientPackage = OPENCODE_CLIENT_PACKAGE[detected.major];
144
+ let packageAvailable = false;
145
+ try {
146
+ const resolvePackage = deps.resolvePackage ?? ((name) => import.meta.resolve(name));
147
+ resolvePackage(clientPackage);
148
+ packageAvailable = true;
149
+ }
150
+ catch {
151
+ packageAvailable = false;
152
+ }
144
153
  const fallbackEngine = configuredEngine.llmEngine;
145
154
  let fallback;
146
155
  let fallbackCredential;
@@ -183,7 +192,7 @@ async function runConfiguredEngineProbe(checkName, engineName, config, deps, rea
183
192
  ? findSuppliedByEnvAsset(fallbackCredential, deps)
184
193
  : null;
185
194
  const missing = [
186
- !packageAvailable ? "@opencode-ai/sdk package" : undefined,
195
+ !packageAvailable ? `${clientPackage} package` : undefined,
187
196
  !binaryAvailable ? `${binary} binary` : undefined,
188
197
  fallbackEngine && !fallback ? "configured fallback LLM connection" : undefined,
189
198
  !fallbackCredentialAvailable
@@ -198,7 +207,9 @@ async function runConfiguredEngineProbe(checkName, engineName, config, deps, rea
198
207
  runtimeKind: "sdk",
199
208
  binary,
200
209
  binaryAvailable,
201
- package: "@opencode-ai/sdk",
210
+ package: clientPackage,
211
+ detectedVersion: detected.version || null,
212
+ detectedMajor: detected.major,
202
213
  packageAvailable,
203
214
  model: effectiveModel ?? null,
204
215
  configuredModel: configuredModel ?? null,
@@ -533,7 +544,21 @@ export function probeActiveImproveStrategy(deps = {}) {
533
544
  const requiredModelProcessNames = modelProcessNames.filter((name) => plan.processes[name].enabled || plan.engineUnavailable.some((item) => item.process === name));
534
545
  const availableModelProcessNames = modelProcessNames.filter((name) => plan.processes[name].enabled);
535
546
  const allRequiredUnavailable = requiredModelProcessNames.length > 0 && availableModelProcessNames.length === 0;
536
- const status = unavailableProcesses.length === 0 ? "pass" : allRequiredUnavailable ? "fail" : "warn";
547
+ // No engine configured at all (no unavailable item carries a resolved engine)
548
+ // is "not set up yet", not a broken credential: it keeps the unknown/warn
549
+ // result it had when the plan threw for it. Engine-free deterministic triage
550
+ // (#1143) now lets such a plan resolve, so this has to be explicit.
551
+ const noEngineConfigured = plan.engineUnavailable.length > 0 && plan.engineUnavailable.every((i) => !i.engine);
552
+ const explicitlyConfigured = config.defaults?.improveStrategy !== undefined || Object.keys(config.improve?.strategies ?? {}).length > 0;
553
+ const status = unavailableProcesses.length === 0
554
+ ? "pass"
555
+ : allRequiredUnavailable
556
+ ? noEngineConfigured
557
+ ? explicitlyConfigured
558
+ ? "warn"
559
+ : "unknown"
560
+ : "fail"
561
+ : "warn";
537
562
  return {
538
563
  check: {
539
564
  name: "active-improve-strategy",
@@ -52,6 +52,7 @@ import { spawnSync } from "node:child_process";
52
52
  import fs from "node:fs";
53
53
  import os from "node:os";
54
54
  import path from "node:path";
55
+ import { OPENCODE_V1, OPENCODE_V2, readPackageVersion, } from "../../integrations/harnesses/opencode/plugin-layout.js";
55
56
  import { isExactSemver, isSemverRange, maxSatisfying, satisfiesRange } from "../../registry/semver.js";
56
57
  /**
57
58
  * Root directory holding Claude Code's plugin cache + marketplace clones.
@@ -66,41 +67,69 @@ function claudePluginsDir() {
66
67
  /**
67
68
  * Root of OpenCode's package cache, same override pattern as
68
69
  * {@link claudePluginsDir} (`AKM_OPENCODE_CACHE_DIR` for tests).
69
- * The OpenCode plugin (`akm-opencode`) bundles its own `akm-cli` under
70
- * `packages/akm-opencode/node_modules/akm-cli` and runs it in-process,
70
+ * The OpenCode plugins (`akm-opencode`, and `akm-opencode-v2` for OpenCode 2) bundle their own `akm-cli`
71
+ * (layout: `integrations/harnesses/opencode/plugin-layout.ts`) and run it in-process,
71
72
  * sharing the host's databases with every other akm install — its version
72
73
  * lag matters the same way a stale Claude plugin's does.
73
74
  */
74
75
  function opencodeCacheDir() {
75
76
  return process.env.AKM_OPENCODE_CACHE_DIR ?? path.join(os.homedir(), ".cache", "opencode");
76
77
  }
78
+ const OPENCODE_PLUGIN_LOCATIONS = [
79
+ {
80
+ // OpenCode 1: <cache>/packages/akm-opencode/node_modules/akm-cli (the folder read since this check began).
81
+ flavor: OPENCODE_V1,
82
+ advisory: "opencode-plugin-version",
83
+ label: "OpenCode's",
84
+ remedy: "update the OpenCode plugin",
85
+ folders: (cacheRoot) => [{ folder: path.join(cacheRoot, OPENCODE_V1.cacheFolder(OPENCODE_V1.pkg)) }],
86
+ },
87
+ {
88
+ // OpenCode 2: <cache>/npm/akm-opencode-v2@<spec>/<epoch-ms>/node_modules/akm-cli, newest build per spec.
89
+ flavor: OPENCODE_V2,
90
+ advisory: "opencode-v2-plugin-version",
91
+ label: "OpenCode 2's akm-opencode-v2",
92
+ remedy: "update the akm-opencode-v2 plugin",
93
+ folders: (cacheRoot) => {
94
+ const npmDir = path.join(cacheRoot, OPENCODE_V2.cacheFolder(""));
95
+ let specs;
96
+ try {
97
+ specs = fs.readdirSync(npmDir).filter((name) => name.startsWith(`${OPENCODE_V2.pkg}@`));
98
+ }
99
+ catch {
100
+ return [];
101
+ }
102
+ return specs.sort().map((spec) => ({ spec, folder: path.join(npmDir, spec) }));
103
+ },
104
+ },
105
+ ];
77
106
  /**
78
- * Read the OpenCode plugin's bundled `akm-cli` version from its
79
- * `package.json` — never executed, just parsed, mirroring
80
- * {@link detectInstalledPlugins}'s manifest read. `undefined` when the
81
- * OpenCode plugin, or its bundled akm-cli, is not installed.
107
+ * The bundled `akm-cli` of each cached copy of `plugin`: its `package.json` is read, never executed,
108
+ * mirroring {@link detectInstalledPlugins}'s manifest read. Copies with no bundled akm-cli are skipped.
82
109
  */
83
- function detectOpencodeBundledAkmVersion(cacheRoot) {
84
- const pkgPath = path.join(cacheRoot, "packages", "akm-opencode", "node_modules", "akm-cli", "package.json");
85
- try {
86
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
87
- return typeof pkg.version === "string" ? pkg.version : undefined;
88
- }
89
- catch {
90
- return undefined;
110
+ function findBundledAkm(plugin, cacheRoot) {
111
+ const found = [];
112
+ for (const { spec, folder } of plugin.folders(cacheRoot)) {
113
+ const nodeModules = plugin.flavor.nodeModulesDir(folder);
114
+ const version = nodeModules && readPackageVersion(path.join(nodeModules, "akm-cli", "package.json"));
115
+ if (version !== undefined)
116
+ found.push(spec ? { spec, version } : { version });
91
117
  }
118
+ return found;
92
119
  }
93
- function buildOpencodeBundledAdvisory(bundledVersion, cliVersion) {
120
+ function buildOpencodeBundledAdvisory(plugin, bundled, cliVersion) {
121
+ const bundledVersion = bundled.version;
94
122
  const stale = bundledVersion !== cliVersion;
123
+ const where = bundled.spec ? ` (${bundled.spec})` : "";
95
124
  return {
96
- name: "opencode-plugin-version",
125
+ name: plugin.advisory,
97
126
  kind: "deterministic",
98
127
  status: stale ? "warn" : "pass",
99
128
  confidence: "high",
100
129
  message: stale
101
- ? `OpenCode's bundled akm-cli is v${bundledVersion}, but the running CLI is v${cliVersion} — it shares the same databases in-process; update the OpenCode plugin.`
102
- : `OpenCode's bundled akm-cli matches the running v${cliVersion}.`,
103
- evidence: { bundledVersion, cliVersion },
130
+ ? `${plugin.label} bundled akm-cli${where} is v${bundledVersion}, but the running CLI is v${cliVersion} — it shares the same databases in-process; ${plugin.remedy}.`
131
+ : `${plugin.label} bundled akm-cli${where} matches the running v${cliVersion}.`,
132
+ evidence: bundled.spec ? { bundledVersion, cliVersion, spec: bundled.spec } : { bundledVersion, cliVersion },
104
133
  };
105
134
  }
106
135
  /**
@@ -218,9 +247,10 @@ export function collectPluginStalenessAdvisories(options) {
218
247
  const plugins = detectInstalledPlugins(pluginsRoot);
219
248
  const results = plugins.map((plugin) => buildAdvisory(plugin, options.cliVersion, pluginsRoot, listRemoteTags));
220
249
  const opencodeCacheRoot = options.opencodeCacheRoot ?? opencodeCacheDir();
221
- const bundledVersion = detectOpencodeBundledAkmVersion(opencodeCacheRoot);
222
- if (bundledVersion !== undefined) {
223
- results.push(buildOpencodeBundledAdvisory(bundledVersion, options.cliVersion));
250
+ for (const plugin of OPENCODE_PLUGIN_LOCATIONS) {
251
+ for (const bundled of findBundledAkm(plugin, opencodeCacheRoot)) {
252
+ results.push(buildOpencodeBundledAdvisory(plugin, bundled, options.cliVersion));
253
+ }
224
254
  }
225
255
  return results;
226
256
  }
@@ -6,7 +6,7 @@ import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled, } from "../../co
6
6
  export const AUTONOMY_LANES = ["memoryInference", "triagePromote", "memoryCleanup"];
7
7
  const LANE_REASONS = {
8
8
  memoryInference: "writes derived memory children and rewrites parent frontmatter",
9
- triagePromote: "auto-accepts queued proposals into the stash (downgraded to queue)",
9
+ triagePromote: "auto-accepts queued proposals the triage judgment tier approves (downgraded to queue)",
10
10
  memoryCleanup: "rewrites belief-state frontmatter and moves files into the cleanup archive",
11
11
  };
12
12
  /**
@@ -59,8 +59,10 @@ export function applyAutonomyGate(strategy, config) {
59
59
  }
60
60
  // Triage stays ENABLED — queued proposals are still triaged, they just are not
61
61
  // auto-accepted. Disabling it would remove review work the user asked for,
62
- // which is the opposite of what a review-first default should do.
63
- if (processes.triage?.applyMode === "promote") {
62
+ // which is the opposite of what a review-first default should do. Only a
63
+ // promote that runs the judgment tier is gated (#1143); a deterministic-only
64
+ // promote is the default and needs no opt-in.
65
+ if (processes.triage?.applyMode === "promote" && processes.triage.judgment?.enabled === true) {
64
66
  processes.triage = { ...processes.triage, applyMode: "queue" };
65
67
  gated.push(gatedLane("triagePromote"));
66
68
  }
@@ -465,7 +465,13 @@ function orderByAge(x, y) {
465
465
  }
466
466
  /** The user message: dates decide "A (older)" / "B (newer)" (plan Appendix A), matching the calibration sample's own ordering. */
467
467
  export function buildPairUserPrompt(older, newer) {
468
- return [...sideSection("A (older)", older), ...sideSection("B (newer)", newer)].join("\n");
468
+ // Equal dates (a batch added in one commit, notes from one file time) make neither side newer: say so, or the judge
469
+ // reads "B (newer)" as proof that B replaces A (#1134).
470
+ const tied = Date.parse(older.createdIso) === Date.parse(newer.createdIso);
471
+ return [
472
+ ...sideSection(tied ? "A (same age as B)" : "A (older)", older),
473
+ ...sideSection(tied ? "B (same age as A)" : "B (newer)", newer),
474
+ ].join("\n");
469
475
  }
470
476
  /** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
471
477
  export function tombstoneReason(label) {
@@ -28,7 +28,7 @@ import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
28
28
  import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
29
29
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
30
30
  import { conceptIdFromTypeName, displayRef, parseRefInput } from "../../core/asset/resolve-ref.js";
31
- import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
31
+ import { loadConfig } from "../../core/config/config.js";
32
32
  import { parseEmbeddedJsonResponse } from "../../core/parse.js";
33
33
  import { openStateDatabase } from "../../core/state-db.js";
34
34
  import { warn, warnVerbose } from "../../core/warn.js";
@@ -210,36 +210,6 @@ async function clusterMemoriesBySimilarity(memories, config, stateDb, signal) {
210
210
  }
211
211
  return { ordered, embedTelemetry: telemetry };
212
212
  }
213
- /**
214
- * Anti-collapse (default on, `antiCollapse.enabled: false` opts out): a small
215
- * deterministic sample of the pool is spread through the similarity order so
216
- * consolidation is not purely similarity-driven.
217
- */
218
- function injectRandomClusterMembers(memories, profile, warnings) {
219
- const config = getImproveProcessConfig("consolidate", profile)?.antiCollapse ?? {};
220
- if (config.enabled === false || memories.length <= 2)
221
- return memories;
222
- const fraction = config.randomClusterFraction ?? 0.05;
223
- const randomCount = Math.max(1, Math.floor(memories.length * fraction));
224
- const sample = [...memories]
225
- .sort((a, b) => contentHash(a.name).localeCompare(contentHash(b.name)))
226
- .slice(0, randomCount);
227
- const sampled = new Set(sample.map((m) => m.name));
228
- const interval = Math.max(2, Math.floor(memories.length / randomCount));
229
- const out = [];
230
- let next = 0;
231
- for (let i = 0; i < memories.length; i++) {
232
- const m = memories[i];
233
- if (m && !sampled.has(m.name))
234
- out.push(m);
235
- if (i > 0 && i % interval === 0 && next < sample.length)
236
- out.push(sample[next++]);
237
- }
238
- while (next < sample.length)
239
- out.push(sample[next++]);
240
- warnings.push(`Anti-collapse: injected ${randomCount} random (non-similarity-driven) cluster member(s) into consolidation pool (fraction=${fraction}).`);
241
- return out;
242
- }
243
213
  /** Body hashes of pending consolidate proposals, so the prompt can mark memories already queued. */
244
214
  function loadPendingConsolidateProposalHashes(stashDir, proposalsCtx) {
245
215
  const hashes = new Set();
@@ -670,11 +640,12 @@ async function judgeConsolidationChunks(args) {
670
640
  }
671
641
  return planned;
672
642
  }
643
+ /** Assumed wall-clock seconds per consolidation chunk for the up-front budget cap. */
644
+ const CHUNK_SECONDS_ESTIMATE = 30;
673
645
  /**
674
646
  * The model's plan for the narrowed pool: chunk size from the context window,
675
647
  * an up-front cap when the remaining budget cannot cover every chunk (oldest
676
- * first, the rest deferred), similarity clustering, anti-collapse, then the
677
- * chunk loop.
648
+ * first, the rest deferred), similarity clustering, then the chunk loop.
678
649
  */
679
650
  async function planConsolidation(opts, config, stashDir, memories, warnings, stateDb, acc) {
680
651
  const llmRunner = opts.llmRunner ?? undefined;
@@ -685,7 +656,7 @@ async function planConsolidation(opts, config, stashDir, memories, warnings, sta
685
656
  let budgeted = memories;
686
657
  const budgetMs = opts.signal?.remainingBudgetMs;
687
658
  if (opts.signal && budgetMs !== undefined) {
688
- const safeChunks = Math.max(0, Math.floor((Math.max(0, budgetMs) / 1000 / (opts.p90ChunkSecondsDefault ?? 30)) * 0.6));
659
+ const safeChunks = Math.max(0, Math.floor((Math.max(0, budgetMs) / 1000 / CHUNK_SECONDS_ESTIMATE) * 0.6));
689
660
  if (safeChunks * chunkSize < memories.length) {
690
661
  budgeted = [...memories]
691
662
  .sort((a, b) => mtimeMsOf(a) - mtimeMsOf(b) || a.name.localeCompare(b.name))
@@ -700,7 +671,7 @@ async function planConsolidation(opts, config, stashDir, memories, warnings, sta
700
671
  if (llmRunner && willDispatch)
701
672
  assertRunnerCredentials(llmRunner);
702
673
  const { ordered, embedTelemetry } = await clusterMemoriesBySimilarity(budgeted, config, stateDb, opts.signal);
703
- const chunks = slice(injectRandomClusterMembers(ordered, opts.improveProfile, warnings));
674
+ const chunks = slice(ordered);
704
675
  const pendingProposalBodyHashes = loadPendingConsolidateProposalHashes(stashDir, opts.proposalsCtx);
705
676
  warn(`[consolidate] ${budgeted.length} memories / ${chunks.length} chunk(s) / chunk_size=${chunkSize}` +
706
677
  ` / pending-proposal hashes: ${pendingProposalBodyHashes.size}`);
@@ -0,0 +1,91 @@
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
+ * Distill's lesson update (#1090): when a memory's lesson repeats a lesson the library already holds, the writer
6
+ * is asked to extend that lesson with what the memory adds instead of the duplicate being thrown away. These are
7
+ * the prompt, the reply schema and the two deterministic checks; the call, the judge and the proposal are in
8
+ * `../distill`.
9
+ */
10
+ import { parseEmbeddedJsonResponse } from "../../../core/parse.js";
11
+ export const DISTILL_LESSON_UPDATE_JSON_SCHEMA = {
12
+ type: "object",
13
+ required: ["reason", "new_facts", "decision", "ref", "body"],
14
+ additionalProperties: false,
15
+ properties: {
16
+ reason: {
17
+ type: "string",
18
+ description: "One sentence: which listed lesson states the rule and what the memory adds to it, or why nothing.",
19
+ },
20
+ new_facts: {
21
+ type: "array",
22
+ items: { type: "string" },
23
+ description: "Written before the body: each cause, step, number, limit or consequence the memory states and the lesson lacks, one short phrase each. Empty when there is none.",
24
+ },
25
+ decision: {
26
+ type: "string",
27
+ enum: ["update", "none"],
28
+ description: "`none` when no listed lesson states the memory's rule, or `new_facts` is empty: leave `ref` and `body` empty. Otherwise `update`.",
29
+ },
30
+ ref: { type: "string", description: "The `Existing lesson ref:` of the lesson to extend. Empty for `none`." },
31
+ body: {
32
+ type: "string",
33
+ description: "That lesson's whole body with the new lines added, every existing line unchanged. No frontmatter. Empty for `none`.",
34
+ },
35
+ },
36
+ };
37
+ export function buildLessonUpdatePrompt(memory, feedback, candidates) {
38
+ const lines = [
39
+ "A lesson written from the memory below would repeat a lesson the library already holds. Do not write a new lesson. Extend the existing one with what the memory adds.",
40
+ "",
41
+ "Memory:",
42
+ "```",
43
+ memory.slice(0, 3000),
44
+ "```",
45
+ ];
46
+ if (feedback.length > 0)
47
+ lines.push("", "Feedback recorded about the memory:", "```", feedback.join("\n").slice(0, 1500), "```");
48
+ lines.push("", "Existing lessons, nearest first (they may be on another subject):");
49
+ for (const candidate of candidates) {
50
+ lines.push(`\nExisting lesson ref: ${candidate.ref}`, "```", candidate.body.slice(0, 3000), "```");
51
+ }
52
+ lines.push("", "Pick the one existing lesson that already states the rule the memory gives. Write its whole body again with the new lines added:", "- Copy every existing line unchanged, in its place. Do not reword, shorten, reorder or drop any of them.", "First list in new_facts each cause, step, number, limit or consequence the memory states and the lesson lacks, one short phrase each: check every sentence of the memory against the lesson. The memory's own incident retold (what happened in one test, project or day) is not a new fact unless it carries one of these. Then add one line to the body for each new fact, and say nothing the memory does not say.", "- If the memory contradicts a line of the lesson, answer none.", "- If no listed lesson states the memory's rule, or the memory adds nothing the lesson lacks, answer none.", "", 'Return ONLY valid JSON, no prose: {"reason": "...", "new_facts": ["..."], "decision": "update" | "none", "ref": "<the existing lesson ref>", "body": "<the whole body>"}');
53
+ return lines.join("\n");
54
+ }
55
+ /** The writer's chosen update, or `null` for `none` and for any reply that names no listed lesson, no new fact or no body. */
56
+ export function parseLessonUpdate(raw, candidates) {
57
+ const payload = parseEmbeddedJsonResponse(raw);
58
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || payload.decision !== "update")
59
+ return null;
60
+ const ref = typeof payload.ref === "string" ? payload.ref.trim() : "";
61
+ const body = typeof payload.body === "string" ? payload.body.trim() : "";
62
+ const candidate = candidates.find((c) => c.ref === ref);
63
+ const facts = Array.isArray(payload.new_facts)
64
+ ? payload.new_facts.filter((f) => typeof f === "string" && f.trim() !== "")
65
+ : [];
66
+ if (!candidate || !body || facts.length === 0)
67
+ return null;
68
+ return { candidate, body, reason: typeof payload.reason === "string" ? payload.reason.trim() : "" };
69
+ }
70
+ const normalizeLine = (line) => line.trim().replace(/\s+/g, " ").toLowerCase();
71
+ const bodyLines = (body) => body
72
+ .split("\n")
73
+ .map(normalizeLine)
74
+ .filter((line) => line !== "");
75
+ /**
76
+ * What an update does to a lesson's body: the existing lines it no longer holds (compared by words, in any case
77
+ * and spacing, anywhere in the new body) and the lines it adds.
78
+ */
79
+ export function diffLessonBody(existingBody, mergedBody) {
80
+ const existing = bodyLines(existingBody);
81
+ const merged = bodyLines(mergedBody);
82
+ const kept = new Set(merged);
83
+ const had = new Set(existing);
84
+ return {
85
+ dropped: existing.filter((line) => !kept.has(line)),
86
+ added: mergedBody
87
+ .split("\n")
88
+ .filter((line) => line.trim() !== "" && !had.has(normalizeLine(line)))
89
+ .map((line) => line.trim()),
90
+ };
91
+ }
@@ -3,8 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
5
  * Distill guards: the related lessons, knowledge notes and skills shown to the
6
- * writer so it does not repeat or overwrite them (CLS context), and a cheap check
7
- * that a proposal does not contradict the memories it came from.
6
+ * writer so it does not repeat or overwrite them (CLS context).
8
7
  */
9
8
  export const DEFAULT_CLS_ADJACENT_COUNT = 3;
10
9
  /** The CLS prompt section (each entry capped at 600 chars); empty when disabled (on unless `enabled: false`) or nothing is related. */
@@ -22,44 +21,3 @@ export function buildClsContext(adjacentItems, config) {
22
21
  lines.push(`### ${item.ref}`, item.content.trim().slice(0, 600), "");
23
22
  return lines.join("\n");
24
23
  }
25
- /**
26
- * Flag a proposal whose "always/must X" (or "never/must not X") claim meets
27
- * the opposite claim about X in a source. Deliberately conservative: a flag
28
- * only costs a human review, while a model call per proposal is expensive.
29
- */
30
- export function checkDistillFidelity(proposalBody, sourceBodies, config) {
31
- if (!config.enabled || sourceBodies.length === 0)
32
- return { contradictionDetected: false };
33
- const strongClaims = extractStrongClaims(proposalBody.toLowerCase());
34
- for (const sourceBody of sourceBodies) {
35
- const sourceLow = sourceBody.toLowerCase();
36
- for (const { polarity, term } of strongClaims) {
37
- if (hasStrongClaim(sourceLow, term, polarity === "positive" ? "negative" : "positive")) {
38
- return {
39
- contradictionDetected: true,
40
- reason: `Proposal makes a ${polarity} strong claim about "${term}" that conflicts with an opposing claim in a cited source. Route to human review.`,
41
- };
42
- }
43
- }
44
- }
45
- return { contradictionDetected: false };
46
- }
47
- const CLAIM_PATTERNS = [
48
- { polarity: "positive", re: /\b(?:always|must)\s+(\w+)/g },
49
- { polarity: "negative", re: /\b(?:never|must\s+not|should\s+not)\s+(\w+)/g },
50
- ];
51
- function extractStrongClaims(text) {
52
- const claims = [];
53
- for (const { polarity, re } of CLAIM_PATTERNS) {
54
- for (const m of text.matchAll(re)) {
55
- const term = m[1];
56
- if (term && term.length > 2)
57
- claims.push({ polarity, term });
58
- }
59
- }
60
- return claims;
61
- }
62
- function hasStrongClaim(text, term, polarity) {
63
- const marker = polarity === "positive" ? /\b(?:always|must)\s/ : /\b(?:never|must\s+not|should\s+not)\s/;
64
- return marker.test(text) && text.includes(term);
65
- }