akm-cli 0.9.31 → 0.10.261002-beta

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 (270) hide show
  1. package/CHANGELOG.md +420 -0
  2. package/STABILITY.md +99 -57
  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 +15 -29
  7. package/dist/assets/hints/cli-hints-short.md +2 -2
  8. package/dist/assets/improve-strategies/default.json +3 -3
  9. package/dist/assets/prompts/consolidate-pair.md +9 -7
  10. package/dist/assets/stash-skeleton/README.md +1 -1
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
  12. package/dist/cli/shared.js +31 -48
  13. package/dist/cli/unknown-flags.js +6 -43
  14. package/dist/cli.js +8 -34
  15. package/dist/commands/agent/contribute-cli.js +1 -1
  16. package/dist/commands/completions.js +1 -2
  17. package/dist/commands/config-cli.js +5 -28
  18. package/dist/commands/env/env-cli.js +5 -5
  19. package/dist/commands/env/secret-cli.js +2 -2
  20. package/dist/commands/feedback-cli.js +75 -49
  21. package/dist/commands/health/checks.js +41 -16
  22. package/dist/commands/health/plugin-staleness.js +52 -22
  23. package/dist/commands/improve/autonomy-gate.js +5 -3
  24. package/dist/commands/improve/consolidate/chunking.js +3 -10
  25. package/dist/commands/improve/consolidate/continuity-check.js +4 -4
  26. package/dist/commands/improve/consolidate/pair-pass.js +29 -10
  27. package/dist/commands/improve/consolidate.js +10 -65
  28. package/dist/commands/improve/distill/lesson-update.js +91 -0
  29. package/dist/commands/improve/distill-guards.js +1 -43
  30. package/dist/commands/improve/distill.js +148 -21
  31. package/dist/commands/improve/eligibility.js +14 -1
  32. package/dist/commands/improve/extract-cli.js +1 -1
  33. package/dist/commands/improve/extract-prompt.js +17 -7
  34. package/dist/commands/improve/extract.js +5 -7
  35. package/dist/commands/improve/improve-cli.js +92 -106
  36. package/dist/commands/improve/improve-strategies.js +8 -12
  37. package/dist/commands/improve/improve.js +134 -52
  38. package/dist/commands/improve/loop-stages.js +18 -5
  39. package/dist/commands/improve/planner.js +0 -1
  40. package/dist/commands/improve/preparation.js +40 -99
  41. package/dist/commands/improve/proactive-maintenance.js +2 -2
  42. package/dist/commands/improve/reflect-noise.js +3 -55
  43. package/dist/commands/improve/reflect.js +29 -34
  44. package/dist/commands/improve/retrieval-queries.js +37 -0
  45. package/dist/commands/improve/retrieval-scope.js +2 -2
  46. package/dist/commands/improve/salience.js +2 -9
  47. package/dist/commands/improve/session-asset.js +4 -1
  48. package/dist/commands/improve/stage.js +23 -12
  49. package/dist/commands/lint/index.js +5 -20
  50. package/dist/commands/observability-cli.js +3 -9
  51. package/dist/commands/proposal/drain.js +43 -6
  52. package/dist/commands/proposal/proposal-cli.js +11 -31
  53. package/dist/commands/proposal/repository.js +20 -14
  54. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -3
  55. package/dist/commands/read/knowledge.js +3 -3
  56. package/dist/commands/read/remember-cli.js +0 -13
  57. package/dist/commands/read/search-cli.js +29 -97
  58. package/dist/commands/read/search.js +0 -11
  59. package/dist/commands/read/show.js +0 -19
  60. package/dist/commands/sources/plugin-upgrade.js +164 -101
  61. package/dist/commands/sources/sources-cli.js +1 -15
  62. package/dist/commands/sources/stash-cli.js +6 -14
  63. package/dist/commands/tasks/tasks-cli.js +4 -115
  64. package/dist/commands/workflow/plan.js +11 -33
  65. package/dist/commands/workflow-cli.js +2 -7
  66. package/dist/core/adapter/adapter-ids.js +1 -2
  67. package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
  68. package/dist/core/adapter/adapters/akm-adapter.js +9 -48
  69. package/dist/core/adapter/adapters/akm-lint.js +10 -26
  70. package/dist/core/adapter/adapters/akm-metadata.js +2 -72
  71. package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
  72. package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
  73. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
  74. package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
  75. package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
  76. package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
  77. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
  78. package/dist/core/adapter/recognize-match.js +3 -4
  79. package/dist/core/common.js +18 -2
  80. package/dist/core/config/config-schema.js +12 -15
  81. package/dist/core/config/config-types.js +6 -15
  82. package/dist/core/config/config-walker.js +6 -23
  83. package/dist/core/config/config.js +17 -80
  84. package/dist/core/config/engine-semantics.js +7 -3
  85. package/dist/core/config/schema/engines.js +7 -7
  86. package/dist/core/config/schema/improve-processes.js +10 -63
  87. package/dist/core/config/schema/improve.js +2 -21
  88. package/dist/core/config/schema/index-config.js +6 -39
  89. package/dist/core/env-secret-ref.js +2 -45
  90. package/dist/core/errors.js +1 -2
  91. package/dist/core/extra-params.js +2 -86
  92. package/dist/core/file-lock.js +2 -6
  93. package/dist/core/improve-result.js +13 -1
  94. package/dist/core/mutation-target.js +2 -2
  95. package/dist/core/paths.js +4 -0
  96. package/dist/core/recognition-util.js +4 -5
  97. package/dist/core/run-lock.js +1 -1
  98. package/dist/core/subprocess.js +49 -0
  99. package/dist/core/warn.js +21 -4
  100. package/dist/core/write-source.js +3 -3
  101. package/dist/execution/target-ref.js +1 -2
  102. package/dist/indexer/indexer.js +15 -6
  103. package/dist/indexer/passes/memory-inference.js +15 -6
  104. package/dist/indexer/search/db-search.js +49 -36
  105. package/dist/indexer/search/fts-query.js +0 -19
  106. package/dist/indexer/search/ranking.js +29 -1
  107. package/dist/indexer/usage/usage-events.js +21 -16
  108. package/dist/integrations/agent/engine-resolution.js +5 -2
  109. package/dist/integrations/agent/index.js +1 -1
  110. package/dist/integrations/agent/profiles.js +41 -98
  111. package/dist/integrations/agent/request-lowering.js +3 -3
  112. package/dist/integrations/harnesses/aider/index.js +5 -0
  113. package/dist/integrations/harnesses/amazonq/index.js +1 -0
  114. package/dist/integrations/harnesses/claude/config-import.js +2 -3
  115. package/dist/integrations/harnesses/claude/index.js +5 -0
  116. package/dist/integrations/harnesses/codex/index.js +1 -0
  117. package/dist/integrations/harnesses/copilot/index.js +1 -0
  118. package/dist/integrations/harnesses/gemini/index.js +1 -0
  119. package/dist/integrations/harnesses/index.js +18 -39
  120. package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
  121. package/dist/integrations/harnesses/opencode/config-import.js +121 -52
  122. package/dist/integrations/harnesses/opencode/index.js +22 -0
  123. package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
  124. package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
  125. package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
  126. package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
  127. package/dist/integrations/harnesses/opencode/session-log.js +104 -115
  128. package/dist/integrations/harnesses/opencode/version.js +98 -0
  129. package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
  130. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
  131. package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
  132. package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +254 -0
  133. package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
  134. package/dist/integrations/harnesses/openhands/index.js +1 -0
  135. package/dist/integrations/harnesses/pi/index.js +1 -0
  136. package/dist/integrations/harnesses/shared.js +2 -0
  137. package/dist/integrations/harnesses/types.js +2 -0
  138. package/dist/llm/client.js +41 -11
  139. package/dist/output/cli-hints.js +1 -1
  140. package/dist/output/context.js +10 -19
  141. package/dist/output/format-exempt.js +5 -8
  142. package/dist/output/shapes/helpers.js +1 -22
  143. package/dist/output/shapes/passthrough.js +0 -1
  144. package/dist/output/shapes.js +3 -31
  145. package/dist/output/stdout.js +5 -1
  146. package/dist/output/text/command-format.js +6 -1
  147. package/dist/output/text/workflow-format.js +4 -22
  148. package/dist/scripts/akm-migrate-node.js +15745 -16711
  149. package/dist/scripts/akm-migrate.js +16318 -17280
  150. package/dist/setup/detect.js +10 -4
  151. package/dist/setup/harness-config-import.js +5 -14
  152. package/dist/setup/setup.js +1 -1
  153. package/dist/setup/steps/tasks.js +12 -13
  154. package/dist/sources/providers/git-stash.js +113 -16
  155. package/dist/storage/repositories/index-utility-repository.js +2 -32
  156. package/dist/storage/repositories/index-vec-repository.js +4 -0
  157. package/dist/storage/repositories/task-history-repository.js +0 -5
  158. package/dist/storage/repositories/workflow-runs-repository.js +5 -15
  159. package/dist/tasks/embedded.js +2 -1
  160. package/dist/tasks/run/run-command-task.js +9 -1
  161. package/dist/tasks/run/run-workflow-task.js +4 -3
  162. package/dist/tasks/run/task-history.js +0 -2
  163. package/dist/tasks/run/task-log.js +38 -1
  164. package/dist/tasks/schedule-gates.js +39 -0
  165. package/dist/tasks/scheduler-binding.js +0 -16
  166. package/dist/tasks/scheduler-sync.js +3 -97
  167. package/dist/tasks/source/bounded-document.js +1 -1
  168. package/dist/tasks/source/task-source-v4.js +2 -12
  169. package/dist/tasks/task-run-reserved-flags.js +2 -31
  170. package/dist/workflows/authoring/authoring.js +6 -6
  171. package/dist/workflows/compile.js +8 -65
  172. package/dist/workflows/exec/child-workflow.js +22 -3
  173. package/dist/workflows/exec/native-executor.js +12 -147
  174. package/dist/workflows/exec/run-workflow.js +31 -103
  175. package/dist/workflows/exec/step-work.js +9 -178
  176. package/dist/workflows/exec/unit-dispatch.js +0 -10
  177. package/dist/workflows/freeze/environment.js +3 -44
  178. package/dist/workflows/freeze/freeze.js +8 -41
  179. package/dist/workflows/freeze/targets/child-workflow.js +27 -63
  180. package/dist/workflows/freeze/targets/command.js +2 -16
  181. package/dist/workflows/freeze/targets/shell.js +0 -2
  182. package/dist/workflows/freeze/task-bindings.js +2 -28
  183. package/dist/workflows/ir/params.js +12 -2
  184. package/dist/workflows/parser.js +82 -232
  185. package/dist/workflows/plan.js +1 -1
  186. package/dist/workflows/renderer.js +10 -19
  187. package/dist/workflows/runtime/run-plan.js +11 -53
  188. package/dist/workflows/runtime/runs.js +4 -27
  189. package/dist/workflows/source-files.js +12 -16
  190. package/dist/workflows/source-semantics.js +1 -93
  191. package/docs/README.md +1 -1
  192. package/docs/agents/README.md +3 -3
  193. package/docs/agents/agent-install.md +1 -1
  194. package/docs/architecture/0.10-process-rules.md +827 -0
  195. package/docs/architecture/adapters.md +17 -31
  196. package/docs/architecture/architecture.md +4 -5
  197. package/docs/architecture/improvement.md +115 -76
  198. package/docs/architecture/internals/improve-workflow.md +22 -16
  199. package/docs/architecture/internals/indexing.md +1 -1
  200. package/docs/architecture/internals/search.md +1 -2
  201. package/docs/architecture/internals/storage-locations.md +6 -6
  202. package/docs/architecture/persisted-data-compat.md +13 -4
  203. package/docs/architecture/specs/0.9.0-decisions.md +6 -2
  204. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
  205. package/docs/architecture/specs/workflow-format-unification.md +1 -6
  206. package/docs/architecture/testing/manual-testing-checklist.md +21 -30
  207. package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
  208. package/docs/architecture/testing/testing-workflow.md +1 -1
  209. package/docs/architecture/workflow-engine.md +5 -56
  210. package/docs/guides/author-workflows.md +39 -69
  211. package/docs/guides/bundles.md +3 -1
  212. package/docs/guides/capture-knowledge.md +1 -1
  213. package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
  214. package/docs/guides/discover-and-load.md +4 -4
  215. package/docs/guides/getting-started.md +1 -1
  216. package/docs/guides/improve-the-library.md +10 -9
  217. package/docs/guides/recipes/headless-install.md +2 -2
  218. package/docs/guides/run-workflows.md +1 -1
  219. package/docs/guides/scheduling.md +9 -1
  220. package/docs/guides/use-with-any-agent.md +49 -4
  221. package/docs/integration/bundling-akm.md +7 -6
  222. package/docs/maintainers/curate-workmap.md +1 -2
  223. package/docs/maintainers/release-checklist.md +33 -1
  224. package/docs/migration/README.md +2 -0
  225. package/docs/migration/release-notes/0.10.0.md +201 -0
  226. package/docs/migration/release-notes/README.md +1 -0
  227. package/docs/migration/v0.9-to-v0.10.md +507 -0
  228. package/docs/posts/akm-command-tour.md +3 -3
  229. package/docs/product-surface.md +1 -1
  230. package/docs/reference/README.md +1 -0
  231. package/docs/reference/api.md +65 -0
  232. package/docs/reference/asset-types.md +3 -5
  233. package/docs/reference/bundle-types.md +2 -4
  234. package/docs/reference/cli.md +160 -188
  235. package/docs/reference/configuration.md +101 -20
  236. package/docs/reference/data-and-telemetry.md +2 -2
  237. package/docs/reference/env-and-secrets.md +2 -2
  238. package/docs/reference/supported-formats.md +3 -4
  239. package/docs/reference/tasks.md +12 -19
  240. package/docs/reference/website-sources.md +1 -1
  241. package/docs/reference/workflow-schema.md +89 -317
  242. package/docs/reference/workflows.md +20 -48
  243. package/package.json +12 -2
  244. package/schemas/akm-config.json +2 -243
  245. package/schemas/akm-task.json +2 -3
  246. package/schemas/akm-workflow.json +47 -84
  247. package/dist/assets/improve-strategies/catchup.json +0 -41
  248. package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
  249. package/dist/assets/improve-strategies/quick.json +0 -17
  250. package/dist/assets/improve-strategies/reflect-distill.json +0 -29
  251. package/dist/assets/improve-strategies/thorough.json +0 -49
  252. package/dist/assets/prompts/retrieval-relevance-judge.md +0 -6
  253. package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
  254. package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
  255. package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
  256. package/dist/cli/retired-commands.js +0 -118
  257. package/dist/commands/improve/retrieval-gate.js +0 -132
  258. package/dist/commands/tasks/validate.js +0 -176
  259. package/dist/core/config/legacy-source-shape-shim.js +0 -117
  260. package/dist/indexer/feedback/utility-policy.js +0 -75
  261. package/dist/integrations/harnesses/ids.js +0 -28
  262. package/dist/tasks/source/task-source-v3-frozen.js +0 -424
  263. package/dist/tasks/source/task-to-v4.js +0 -863
  264. package/dist/workflows/exec/worktree.js +0 -372
  265. package/dist/workflows/freeze/child-output-references.js +0 -83
  266. package/dist/workflows/freeze/identity.js +0 -22
  267. package/dist/workflows/freeze/targets/script.js +0 -50
  268. package/dist/workflows/freeze/targets/task.js +0 -124
  269. package/dist/workflows/github-yaml.js +0 -506
  270. package/dist/workflows/runtime/run-outputs.js +0 -80
@@ -21,15 +21,17 @@ import { getDbPath } from "../core/paths.js";
21
21
  import { withStateDb } from "../core/state-db.js";
22
22
  import { warn } from "../core/warn.js";
23
23
  import { withWriteTargetMutation } from "../core/write-source.js";
24
+ import { recomputeUtilityScores } from "../indexer/indexer.js";
24
25
  import { resolveSourceEntries } from "../indexer/search/search-source.js";
25
- import { countFeedbackSignals, insertUsageEvent, resolveUsageEventSource } from "../indexer/usage/usage-events.js";
26
+ import { insertUsageEvent, resolveUsageEventSource } from "../indexer/usage/usage-events.js";
26
27
  import { resolveSourcesForOrigin } from "../registry/origin-resolve.js";
27
28
  import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
28
29
  import { findEntryIdByRef, getEntryFilePathById, getItemRefById, } from "../storage/repositories/index-entries-repository.js";
29
- import { applyFeedbackToUtilityScore } from "../storage/repositories/index-utility-repository.js";
30
+ import { getUtilityScore } from "../storage/repositories/index-utility-repository.js";
30
31
  import { contentHash } from "./improve/content-hash.js";
31
32
  import { readEdgeList } from "./improve/memory/memory-belief.js";
32
- import { createProposal } from "./proposal/repository.js";
33
+ import { akmProposalAccept } from "./proposal/proposal.js";
34
+ import { createProposal, recordGateDecision, rejectProposalDurably } from "./proposal/repository.js";
33
35
  // ── Tag validation ────────────────────────────────────────────────────────────
34
36
  const TAG_KEY_RE = /^[a-z_][a-z0-9_]*$/;
35
37
  function validateFeedbackTags(raw) {
@@ -144,26 +146,25 @@ function buildLessonStrengthUpdate(rawBytes, feedbackRef) {
144
146
  const nextBytes = Buffer.from(next);
145
147
  return rawBytes.equals(nextBytes) ? { strength: strengthList.length } : { strength: strengthList.length, nextBytes };
146
148
  }
149
+ /** Utility at or above this is "high"; falling below it after feedback emits `improve_review_needed`. */
150
+ const UTILITY_REVIEW_THRESHOLD = 0.5;
147
151
  /**
148
- * Persist the feedback usage-event (state.db) and immediately fold it into the
149
- * entry's MemRL utility score (index.db). Chunk-8 WI-8.3: usage_events lives in
150
- * state.db; entries + utility_scores stay in `indexDb`. BOTH positive and
151
- * negative signals apply the EMA utility update unconditionally and
152
- * immediately when the source is user-attributed — no `akm index` run is
153
- * required for either signal to affect search ranking. Uses the bounded-step
154
- * EMA policy (F-5 / #386, arXiv:2601.03192).
155
- *
156
- * The update is intentionally SKIPPED for non-`user` event sources (`improve`,
157
- * `task`, `audit`, `unknown`) — an anti-self-reinforcement guard that stops an
158
- * agent or automated pipeline from boosting its own picks. This is by design
159
- * and must not be removed; R-034 only asks that the skip be surfaced to the
160
- * caller rather than passing silently. Best-effort: a utility-update failure
161
- * never fails the feedback record.
152
+ * Persist the feedback usage-event (state.db) and, for a user-attributed event,
153
+ * recompute the entry's utility score (index.db) with the same function the
154
+ * `akm index` finalize pass uses, scoped to this one entry, so there is one
155
+ * formula and the stored value is what the next index would compute. Chunk-8
156
+ * WI-8.3: usage_events lives in state.db; entries + utility_scores stay in `indexDb`.
157
+ * Search ranking does not read the utility score: it counts the recorded
158
+ * `source='user'` feedback events itself, so a user-attributed event affects
159
+ * ranking at once and an `improve`/`task`/`audit`/`unknown` one never does — an
160
+ * anti-self-reinforcement guard that stops an agent or automated pipeline from
161
+ * boosting its own picks. This is by design and must not be removed. The
162
+ * utility score is read by improve and metrics only; its update is best-effort
163
+ * and its failure never fails the feedback record or changes the ranking effect.
162
164
  */
163
165
  function recordFeedbackUsage(indexDb, entryId, durableEntryRef, signal, metadataStr) {
164
166
  let utilityResult;
165
- let rankingUpdateApplied = false;
166
- let rankingUpdateSkippedReason;
167
+ let rankingSkippedReason;
167
168
  const eventSource = resolveUsageEventSource();
168
169
  withStateDb((stateDb) => {
169
170
  insertUsageEvent(stateDb, {
@@ -175,22 +176,22 @@ function recordFeedbackUsage(indexDb, entryId, durableEntryRef, signal, metadata
175
176
  source: eventSource,
176
177
  });
177
178
  if (eventSource !== "user") {
178
- rankingUpdateSkippedReason =
179
- `feedback source is "${eventSource}", not "user" — ranking updates only apply to user-attributed ` +
180
- "feedback (anti-self-reinforcement guard; set AKM_EVENT_SOURCE=user to record as user demand).";
179
+ rankingSkippedReason =
180
+ `feedback source is "${eventSource}", not "user" — it does not count toward search ranking, which counts ` +
181
+ "user-attributed feedback only (anti-self-reinforcement guard; set AKM_EVENT_SOURCE=user to record as user demand).";
181
182
  return;
182
183
  }
183
184
  try {
184
- const { pos, neg } = countFeedbackSignals(stateDb, entryId);
185
- utilityResult = applyFeedbackToUtilityScore(indexDb, entryId, pos, neg);
186
- rankingUpdateApplied = true;
185
+ const previousUtility = getUtilityScore(indexDb, entryId)?.utility ?? 0;
186
+ recomputeUtilityScores(indexDb, stateDb, entryId);
187
+ utilityResult = { previousUtility, nextUtility: getUtilityScore(indexDb, entryId)?.utility ?? previousUtility };
187
188
  }
188
189
  catch (err) {
189
- // best-effort — feedback recording succeeds even if utility update fails
190
- rankingUpdateSkippedReason = `utility update failed: ${err instanceof Error ? err.message : String(err)}`;
190
+ // best-effort — the event is recorded (and ranked) even if the utility update fails
191
+ warn(`[feedback] utility update failed: ${err instanceof Error ? err.message : String(err)}`);
191
192
  }
192
193
  });
193
- return { utilityResult, rankingUpdateApplied, rankingUpdateSkippedReason };
194
+ return { utilityResult, countsTowardRanking: eventSource === "user", rankingSkippedReason };
194
195
  }
195
196
  // ── Judged text ──────────────────────────────────────────────────────────────
196
197
  /**
@@ -416,15 +417,20 @@ export const feedbackCommand = defineJsonCommand({
416
417
  'the exact fix: --replace "<exact current text>" --with "<corrected text>" --source "<URL,\n' +
417
418
  'command or file that shows it>" (repeat --replace/--with for several edits; use --with=...\n' +
418
419
  "for a value that starts with -). akm checks that each --replace text appears exactly once\n" +
419
- "and queues the edit as a proposal for review. To mark the asset's history instead, add\n" +
420
+ "and applies the edit at once, with no review (`akm proposal revert` undoes it). To mark\n" +
421
+ "the asset's history instead, add\n" +
420
422
  "--superseded-by <ref> (the asset that replaces it) or --outdated (it describes a past\n" +
421
- "state and nothing replaces it), with --reason and --source: the same proposal sets its\n" +
423
+ "state and nothing replaces it), with --reason and --source: the same edit sets its\n" +
422
424
  "beliefState and, for --superseded-by, adds the ref to supersededBy. `--positive` records\n" +
423
425
  "that an asset helped (it raises its ranking) and does not trigger a rewrite.\n\n" +
424
- "Both signals adjust the asset's usefulness score right away, in the same\n" +
425
- "process: positive feedback raises it, negative lowers it, and recent\n" +
426
- "feedback counts for more than old feedback. No reindex is needed — the new\n" +
427
- "score affects ranking starting with the very next `akm search`.",
426
+ "Feedback changes `akm search` and `akm curate` ranking starting with the very\n" +
427
+ "next search; no reindex is needed. Each recorded positive raises the asset's\n" +
428
+ "score and each negative lowers it, the same way for every asset: the more\n" +
429
+ "positives, the higher it ranks (5 outrank 2), and each further vote counts\n" +
430
+ "for a little less. The effect is small, so it reorders close matches and\n" +
431
+ "does not override relevance. Only feedback recorded as the user counts, not\n" +
432
+ "feedback from improve, task or audit runs. Feedback also moves the utility\n" +
433
+ "score that improve and `akm metrics` read.",
428
434
  },
429
435
  args: {
430
436
  // Optional in citty so run() is invoked even when omitted; we re-validate
@@ -565,8 +571,8 @@ export const feedbackCommand = defineJsonCommand({
565
571
  // background reindex it spawned — now that ensureIndex is removed, holding
566
572
  // the lock only causes feedback to block for the full improve run duration.
567
573
  let utilityResult;
568
- let rankingUpdateApplied = false;
569
- let rankingUpdateSkippedReason;
574
+ let countsTowardRanking = false;
575
+ let rankingSkippedReason;
570
576
  let durableRef = ref;
571
577
  let fix;
572
578
  const db = openExistingDatabase();
@@ -586,10 +592,10 @@ export const feedbackCommand = defineJsonCommand({
586
592
  throw new NotFoundError(`Ref "${ref}" is not in the index. ` +
587
593
  "Run 'akm search' to verify the asset exists, then 'akm index' if it was recently added.", "ASSET_NOT_FOUND");
588
594
  }
589
- // Persist the feedback signal into usage_events. Both positive and
590
- // negative signals apply the EMA utility update immediately — no
591
- // `akm index` run is required for either signal to affect ranking in
592
- // search results (see recordFeedbackUsage / applyFeedbackToUtilityScore).
595
+ // Persist the feedback signal into usage_events. A user-attributed signal
596
+ // also recomputes the entry's utility at once (see recordFeedbackUsage).
597
+ // Search ranking reads the recorded events directly, so no `akm index`
598
+ // run is needed.
593
599
  // WI-8.5b: the `feedback` / `improve_review_needed` events key on the
594
600
  // resolved entry's fully-qualified item_ref — the SAME durable key the
595
601
  // usage_events row carries and the SAME spelling the signal-delta
@@ -645,8 +651,8 @@ export const feedbackCommand = defineJsonCommand({
645
651
  const metadataStr = Object.keys(metadataObj).length > 1 ? JSON.stringify(metadataObj) : undefined;
646
652
  const recordResult = recordFeedbackUsage(db, entryId, itemRef, signal, metadataStr);
647
653
  utilityResult = recordResult.utilityResult;
648
- rankingUpdateApplied = recordResult.rankingUpdateApplied;
649
- rankingUpdateSkippedReason = recordResult.rankingUpdateSkippedReason;
654
+ countsTowardRanking = recordResult.countsTowardRanking;
655
+ rankingSkippedReason = recordResult.rankingSkippedReason;
650
656
  }
651
657
  finally {
652
658
  closeDatabase(db);
@@ -656,16 +662,34 @@ export const feedbackCommand = defineJsonCommand({
656
662
  ref: durableRef,
657
663
  metadata: metadataObj,
658
664
  });
659
- const fixProposal = fix && reason?.trim() && fixSource
660
- ? createProposal(resolveStashDir(), {
665
+ // A person typed the fix, so it is applied now: the proposal is minted for the audit trail, `akm proposal
666
+ // revert` and reflect's "already acted on" rule, then accepted through the ordinary accept path (the same
667
+ // checks and git commit). If that refuses, the proposal is rejected and the command fails.
668
+ let fixProposal;
669
+ if (fix && reason?.trim() && fixSource) {
670
+ const stashDir = resolveStashDir();
671
+ const minted = createProposal(stashDir, {
661
672
  ref: durableRef,
662
673
  itemRef: durableRef,
663
674
  target: fix.target,
664
675
  source: "feedback",
665
676
  payload: { content: fix.content },
666
677
  feedback: { reason: reason.trim(), source: fixSource },
667
- })
668
- : undefined;
678
+ });
679
+ try {
680
+ recordGateDecision(stashDir, minted.id, { outcome: "auto-accepted", reason: "manual-fix", gate: "feedback" });
681
+ fixProposal = (await akmProposalAccept({ stashDir, id: minted.id })).proposal;
682
+ }
683
+ catch (err) {
684
+ try {
685
+ rejectProposalDurably(stashDir, minted.id, `manual fix not applied: ${err instanceof Error ? err.message : String(err)}`);
686
+ }
687
+ catch (rejectErr) {
688
+ warn(`[feedback] could not reject proposal ${minted.id}: ${rejectErr instanceof Error ? rejectErr.message : String(rejectErr)}`);
689
+ }
690
+ throw err;
691
+ }
692
+ }
669
693
  // F-5 / #386: When a high-utility asset crosses below the review threshold,
670
694
  // auto-create a review-needed escalation proposal so a human can confirm
671
695
  // whether the negative feedback is valid before the asset falls out of
@@ -674,7 +698,9 @@ export const feedbackCommand = defineJsonCommand({
674
698
  // Emit a structured event rather than a proposal so the review-needed
675
699
  // signal doesn't risk an accidental asset overwrite if the proposal is
676
700
  // accepted.
677
- if (utilityResult?.crossedReviewThreshold) {
701
+ if (utilityResult &&
702
+ utilityResult.previousUtility >= UTILITY_REVIEW_THRESHOLD &&
703
+ utilityResult.nextUtility < UTILITY_REVIEW_THRESHOLD) {
678
704
  try {
679
705
  appendEvent({
680
706
  eventType: "improve_review_needed",
@@ -729,9 +755,9 @@ export const feedbackCommand = defineJsonCommand({
729
755
  signal,
730
756
  reason: reason?.trim() ?? null,
731
757
  tags: validatedTags,
732
- rankingUpdate: rankingUpdateApplied
758
+ rankingUpdate: countsTowardRanking
733
759
  ? { applied: true }
734
- : { applied: false, reason: rankingUpdateSkippedReason ?? "unknown" },
760
+ : { applied: false, reason: rankingSkippedReason ?? "unknown" },
735
761
  ...(fixProposal
736
762
  ? {
737
763
  fix: {
@@ -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
  }
@@ -3,7 +3,6 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /** Consolidate's chunk sizing and per-chunk prompt (pure token math and text). */
5
5
  import fs from "node:fs";
6
- import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
7
6
  import { contentHash, stripFrontmatterBody } from "../content-hash.js";
8
7
  /** A conservative chars-per-token estimate, shared by every context-length budget. */
9
8
  export const CHARS_PER_TOKEN = 3;
@@ -24,9 +23,8 @@ export function computeSafeChunkSize(contextLength, bodyTruncation, maxChunkSize
24
23
  /**
25
24
  * One chunk's user prompt. Each memory shows its body only (frontmatter can
26
25
  * outrun the truncation and leave the model judging metadata), marked
27
- * `captureMode: hot` when the user captured it explicitly and `already queued`
28
- * when a pending consolidate proposal carries its body hash — the system
29
- * prompt forbids promoting those.
26
+ * `already queued` when a pending consolidate proposal carries its body hash —
27
+ * the system prompt forbids promoting those.
30
28
  */
31
29
  export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks, bodyTruncation, pendingProposalBodyHashes = new Set()) {
32
30
  const lines = [
@@ -42,12 +40,7 @@ export function buildChunkPrompt(sourceName, memories, chunkIndex, totalChunks,
42
40
  catch {
43
41
  body = "(unreadable)";
44
42
  }
45
- const annotations = [];
46
- if (parseFrontmatter(body).data.captureMode === "hot")
47
- annotations.push("captureMode: hot");
48
- if (pendingProposalBodyHashes.has(contentHash(body, "body")))
49
- annotations.push("already queued");
50
- const annotationSuffix = annotations.length > 0 ? ` (${annotations.join("; ")})` : "";
43
+ const annotationSuffix = pendingProposalBodyHashes.has(contentHash(body, "body")) ? " (already queued)" : "";
51
44
  lines.push(`[${i + 1}] memories/${m.name}${annotationSuffix}`, `Description: ${m.description || "(none)"}`, `Tags: ${m.tags.length > 0 ? m.tags.join(", ") : "(none)"}`, "---", stripFrontmatterBody(body).slice(0, bodyTruncation), "");
52
45
  });
53
46
  return lines.join("\n");
@@ -4,7 +4,7 @@
4
4
  import { searchLocal } from "../../../indexer/search/db-search.js";
5
5
  import { stripFrontmatterBody } from "../content-hash.js";
6
6
  import { stripBundle } from "../ledger.js";
7
- import { loadRetrievalQueries } from "../retrieval-gate.js";
7
+ import { loadRetrievalQueries } from "../retrieval-queries.js";
8
8
  /** At most this many of the retired asset's most recent queries are replayed (plan §5.4, §7). */
9
9
  export const CONTINUITY_MAX_QUERIES = 5;
10
10
  /** "Top 10" per the plan's rule — both the pass/fail cutoff and the search `limit`. */
@@ -63,8 +63,8 @@ function normalizedBody(raw) {
63
63
  *
64
64
  * S3: two fixes against false flags measured on a real night-1 admission
65
65
  * (300 pairs, 6 flags, half spurious):
66
- * - queries are the SAME cleaned set `loadRetrievalQueries` replays for the
67
- * retrieval regression gate (`../retrieval-gate.ts`) — stash-README
66
+ * - queries are the cleaned set `loadRetrievalQueries` returns
67
+ * (`../retrieval-queries.ts`) — stash-README
68
68
  * boilerplate, harness/tool envelopes, pastes over 2,000 characters, and
69
69
  * duplicates are dropped before replay, not just capped at 5 raw entries;
70
70
  * - when the retired and successor bodies normalize identical, the check
@@ -92,7 +92,7 @@ export async function checkRetirementContinuity(args) {
92
92
  // no real risk here to check for.
93
93
  if (normalizedBody(args.retiredRaw) === normalizedBody(args.successorRaw))
94
94
  return undefined;
95
- // S3a: the SAME cleaned queries the retrieval regression gate replays —
95
+ // S3a: the cleaned queries from `loadRetrievalQueries` —
96
96
  // boilerplate, envelopes, pastes and duplicates dropped before replay.
97
97
  const queries = loadRetrievalQueries(args.ledgerAccess, args.retiredRef).slice(0, CONTINUITY_MAX_QUERIES);
98
98
  if (queries.length === 0)
@@ -42,10 +42,9 @@ import { assertRunnerCredentials } from "../../../integrations/agent/runner-disp
42
42
  import { runGit } from "../../../sources/providers/git-install.js";
43
43
  import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../../storage/repositories/index-connection.js";
44
44
  import { getAllEntries, getEntryById } from "../../../storage/repositories/index-entries-repository.js";
45
- import { getNeighborsByEntryId } from "../../../storage/repositories/index-vec-repository.js";
46
- import { isRetireProposal, PAIR_PASS_GATE, } from "../../proposal/proposal-types.js";
45
+ import { getNeighborsByEntryId, hasStoredEmbedding } from "../../../storage/repositories/index-vec-repository.js";
46
+ import { EXPIRED_GATE_REASON, isRetireProposal, PAIR_PASS_GATE, STALE_TARGET_GATE_REASON, } from "../../proposal/proposal-types.js";
47
47
  import { createRetireProposal, listProposalsReadOnly, proposalContentHash, recordGateDecision, } from "../../proposal/repository.js";
48
- import { isHotCapturedMemory } from "../consolidate.js";
49
48
  import { contentHash, stripFrontmatterBody } from "../content-hash.js";
50
49
  import { loadLedgerSnapshot, PAIR_PASS_LEDGER_SOURCE, recordLedgerAttempt, stripBundle } from "../ledger.js";
51
50
  import { isInRetrievalScope, loadRetrievalScope } from "../retrieval-scope.js";
@@ -313,6 +312,10 @@ export function loadRejectedPairKeys(stashDir, proposalsCtx) {
313
312
  for (const p of listProposalsReadOnly(stashDir, { status, includeArchive: true }, proposalsCtx)) {
314
313
  if (!isRetireProposal(p) || !p.retirement)
315
314
  continue;
315
+ // Expiry and a stale target are procedural, not a person saying no: the pair may be judged again.
316
+ const autoReason = p.gateDecision?.outcome === "auto-rejected" ? p.gateDecision.reason : undefined;
317
+ if (autoReason === EXPIRED_GATE_REASON || autoReason === STALE_TARGET_GATE_REASON)
318
+ continue;
316
319
  keys.add(rejectedPairKey(p.retirement.retiredRef, p.retirement.successorRef, p.retirement.retiredContentHash, p.retirement.successorContentHash));
317
320
  }
318
321
  }
@@ -364,12 +367,18 @@ export function selectInitiators(pool, opts, stashDir, gitFirstAdded) {
364
367
  * apart from a fully-judged one). S2: fetches `PAIR_NEIGHBOR_FETCH_K` (20)
365
368
  * raw neighbours and keeps the first `PAIR_NEIGHBOR_K` (5) that clear every
366
369
  * filter, so a few self/twin/bundle/tier misses in the raw top-5 no longer
367
- * starve an initiator down to zero real candidates.
370
+ * starve an initiator down to zero real candidates. An initiator with no
371
+ * stored vector (embeddings off, or not embedded yet) cannot be queried at
372
+ * all; its ref is added to `unvectored` so the caller does not settle it.
368
373
  */
369
- export function selectCandidates(db, initiators, bundleId) {
374
+ export function selectCandidates(db, initiators, bundleId, unvectored) {
370
375
  const candidates = [];
371
376
  const seenPairs = new Set();
372
377
  for (const initiator of initiators) {
378
+ if (!hasStoredEmbedding(db, initiator.entryId)) {
379
+ unvectored?.add(initiator.ref);
380
+ continue;
381
+ }
373
382
  // S1: a changed-content or new-material initiator judges at T_PAIR; the
374
383
  // rest of the backlog (no row, not recently git-added) needs the higher
375
384
  // BACKFILL_FLOOR.
@@ -465,7 +474,13 @@ function orderByAge(x, y) {
465
474
  }
466
475
  /** The user message: dates decide "A (older)" / "B (newer)" (plan Appendix A), matching the calibration sample's own ordering. */
467
476
  export function buildPairUserPrompt(older, newer) {
468
- return [...sideSection("A (older)", older), ...sideSection("B (newer)", newer)].join("\n");
477
+ // Equal dates (a batch added in one commit, notes from one file time) make neither side newer: say so, or the judge
478
+ // reads "B (newer)" as proof that B replaces A (#1134).
479
+ const tied = Date.parse(older.createdIso) === Date.parse(newer.createdIso);
480
+ return [
481
+ ...sideSection(tied ? "A (same age as B)" : "A (older)", older),
482
+ ...sideSection(tied ? "B (same age as A)" : "B (newer)", newer),
483
+ ].join("\n");
469
484
  }
470
485
  /** The tombstone-vocabulary reason a judge label maps to (`supersedes` -> `superseded`; the rest unchanged). */
471
486
  export function tombstoneReason(label) {
@@ -591,9 +606,6 @@ async function judgeOne(ctx, candidate) {
591
606
  [retired, successor] = [successor, retired];
592
607
  }
593
608
  // Guards (plan §5.2 step 4 / brief §A "Guards").
594
- if (retired.asset.type === "memory" && isHotCapturedMemory(retired.asset.filePath)) {
595
- return { failed: false }; // never propose retiring a captureMode: hot memory — leave the pair alone
596
- }
597
609
  if (retired.asset.type === "memory" && retired.asset.name.endsWith(DERIVED_SUFFIX)) {
598
610
  // S3: derive the parent path from the FULL file path, not from name +
599
611
  // dirname — for a subfolder memory (e.g. memories/sub/foo.derived) the
@@ -730,6 +742,7 @@ seams = {}) {
730
742
  let candidates;
731
743
  let gitFirstAdded;
732
744
  let db;
745
+ const unvectored = new Set();
733
746
  try {
734
747
  db = opts.dryRun ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
735
748
  if (!db)
@@ -737,7 +750,7 @@ seams = {}) {
737
750
  gitFirstAdded = loadGitFirstAddedMap(stashDir);
738
751
  const pool = loadPairPassPool(db, bundleId);
739
752
  initiators = selectInitiators(pool, opts, stashDir, gitFirstAdded).initiators;
740
- candidates = selectCandidates(db, initiators, bundleId);
753
+ candidates = selectCandidates(db, initiators, bundleId, unvectored);
741
754
  }
742
755
  catch (error) {
743
756
  warnings.push(`Pair pass: index unavailable — skipped (${error instanceof Error ? error.message : String(error)}).`);
@@ -748,6 +761,9 @@ seams = {}) {
748
761
  closeDatabase(db);
749
762
  }
750
763
  const initiatorsBacklog = initiators.filter((i) => i.backlog).length;
764
+ if (unvectored.size > 0) {
765
+ warnings.push(`Pair pass: ${unvectored.size} asset(s) have no stored embedding, so their neighbours cannot be found and they were not judged. The pair pass needs stored embeddings (set semanticSearchMode to "auto" in the config, then run akm index).`);
766
+ }
751
767
  // Never judge a pair when either side already has a pending retire
752
768
  // proposal, as the retired ref OR its successor (B2 widens this from the
753
769
  // retired ref alone): an asset spoken for by one pending decision cannot
@@ -861,6 +877,9 @@ seams = {}) {
861
877
  const admittedRefs = new Set(judgeable.map((c) => c.initiator.ref));
862
878
  const ledgerInputs = initiators
863
879
  .filter((i) => {
880
+ // No stored vector: the neighbour query never ran, so nothing was judged. Leave it unsettled.
881
+ if (unvectored.has(i.ref))
882
+ return false;
864
883
  const hasCandidates = byInitiator.has(i.ref);
865
884
  if (!hasCandidates)
866
885
  return true;