akm-cli 0.9.5 → 0.9.7

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 (130) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/assets/hints/cli-hints-full.md +3 -3
  3. package/dist/assets/improve-strategies/catchup.json +40 -11
  4. package/dist/assets/improve-strategies/thorough.json +45 -7
  5. package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
  6. package/dist/commands/agent/contribute-cli.js +11 -0
  7. package/dist/commands/env/env-cli.js +1 -2
  8. package/dist/commands/env/secret-cli.js +1 -5
  9. package/dist/commands/feedback-cli.js +0 -4
  10. package/dist/commands/health/checks.js +0 -32
  11. package/dist/commands/health/surfaces.js +2 -2
  12. package/dist/commands/health.js +6 -15
  13. package/dist/commands/improve/autonomy-gate.js +1 -1
  14. package/dist/commands/improve/consolidate.js +25 -26
  15. package/dist/commands/improve/distill.js +2 -2
  16. package/dist/commands/improve/extract.js +8 -1
  17. package/dist/commands/improve/improve-cli.js +1 -1
  18. package/dist/commands/improve/improve-strategies.js +0 -4
  19. package/dist/commands/improve/improve.js +3 -5
  20. package/dist/commands/improve/locks.js +13 -5
  21. package/dist/commands/improve/loop-stages.js +8 -9
  22. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  23. package/dist/commands/improve/memory/memory-improve.js +2 -1
  24. package/dist/commands/improve/outcome-loop.js +0 -6
  25. package/dist/commands/improve/preparation.js +1 -1
  26. package/dist/commands/improve/reflect.js +21 -10
  27. package/dist/commands/lint/base-linter.js +141 -18
  28. package/dist/commands/lint/index.js +21 -10
  29. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  30. package/dist/commands/read/curate.js +47 -0
  31. package/dist/commands/read/search-cli.js +24 -1
  32. package/dist/commands/sources/bundle-config-ops.js +9 -4
  33. package/dist/commands/sources/info.js +14 -10
  34. package/dist/commands/tasks/tasks.js +12 -16
  35. package/dist/commands/workflow-cli.js +2 -2
  36. package/dist/core/asset/asset-placement.js +13 -2
  37. package/dist/core/asset/frontmatter.js +116 -0
  38. package/dist/core/asset/memory-archive.js +97 -0
  39. package/dist/core/common.js +61 -24
  40. package/dist/core/config/config-io.js +2 -2
  41. package/dist/core/config/config-sources.js +32 -2
  42. package/dist/core/config/config.js +1 -1
  43. package/dist/core/config/engine-semantics.js +0 -2
  44. package/dist/core/config/schema/engines.js +9 -7
  45. package/dist/core/config/schema/primitives.js +0 -5
  46. package/dist/core/config/schema/search.js +2 -1
  47. package/dist/core/file-lock.js +2 -1
  48. package/dist/core/maintenance-barrier.js +2 -14
  49. package/dist/core/paths.js +0 -3
  50. package/dist/core/redaction.js +2 -2
  51. package/dist/core/spawn-env.js +8 -12
  52. package/dist/core/state/migrations.js +1 -12
  53. package/dist/core/state-db.js +9 -27
  54. package/dist/core/write-source.js +12 -19
  55. package/dist/execution/directory-identity.js +36 -10
  56. package/dist/indexer/graph/graph-boost.js +0 -4
  57. package/dist/indexer/index-writer-lock.js +43 -24
  58. package/dist/indexer/index-written-assets.js +5 -6
  59. package/dist/indexer/indexer.js +2 -39
  60. package/dist/indexer/materialize-embeddings.js +85 -41
  61. package/dist/indexer/search/db-search.js +15 -48
  62. package/dist/indexer/search/ranking-contributors.js +0 -25
  63. package/dist/indexer/search/ranking.js +3 -13
  64. package/dist/integrations/agent/builder-shared.js +0 -25
  65. package/dist/integrations/agent/model-map.js +2 -60
  66. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  67. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  68. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  69. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  70. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  71. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  72. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  73. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  74. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  75. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  76. package/dist/integrations/session-logs/index.js +0 -9
  77. package/dist/llm/client.js +75 -42
  78. package/dist/llm/embedder.js +7 -3
  79. package/dist/llm/embedders/remote.js +141 -42
  80. package/dist/registry/network.js +5 -37
  81. package/dist/runtime.js +2 -10
  82. package/dist/scripts/akm-migrate-node.js +73 -133
  83. package/dist/scripts/akm-migrate.js +73 -133
  84. package/dist/setup/engine-config.js +2 -5
  85. package/dist/setup/registry-stash-loader.js +0 -8
  86. package/dist/setup/setup.js +9 -46
  87. package/dist/setup/steps/connection-shared.js +10 -13
  88. package/dist/sources/providers/git-install.js +1 -1
  89. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
  90. package/dist/storage/engines/sqlite-migrations.js +20 -1
  91. package/dist/storage/repositories/index-connection.js +45 -3
  92. package/dist/storage/repositories/index-entries-repository.js +0 -15
  93. package/dist/tasks/backends/cron.js +49 -9
  94. package/dist/tasks/backends/launchd.js +15 -20
  95. package/dist/tasks/backends/schtasks.js +18 -8
  96. package/dist/tasks/resolve-akm-bin.js +17 -2
  97. package/dist/tasks/run/run-native-task.js +8 -6
  98. package/dist/tasks/scheduler-invocation.js +8 -1
  99. package/dist/tasks/source/bounded-document.js +2 -4
  100. package/dist/tasks/source/parse-task-source.js +23 -9
  101. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  102. package/dist/tasks/source/task-source-v4.js +5 -10
  103. package/dist/tasks/source/task-to-v3.js +5 -10
  104. package/dist/tasks/source/task-to-v4.js +1 -4
  105. package/dist/tasks/source-v3.js +6 -6
  106. package/dist/workflows/exec/native-executor.js +21 -31
  107. package/dist/workflows/exec/run-workflow.js +5 -6
  108. package/dist/workflows/exec/scheduler.js +3 -19
  109. package/dist/workflows/exec/step-work.js +1 -4
  110. package/dist/workflows/exec/unit-dispatch.js +2 -2
  111. package/dist/workflows/exec/worktree.js +1 -13
  112. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  113. package/dist/workflows/ir/plan-hash.js +4 -6
  114. package/dist/workflows/ir/schema-v4.js +0 -12
  115. package/dist/workflows/ir/schema.js +20 -31
  116. package/dist/workflows/parser.js +11 -52
  117. package/dist/workflows/renderer.js +2 -3
  118. package/dist/workflows/resource-limits.js +11 -41
  119. package/dist/workflows/runtime/runs.js +3 -4
  120. package/dist/workflows/source-ir/schema.js +14 -30
  121. package/dist/workflows/validator.js +1 -7
  122. package/docs/reference/cli.md +5 -1
  123. package/docs/reference/configuration.md +1 -1
  124. package/package.json +1 -1
  125. package/schemas/akm-config.json +4 -23
  126. package/schemas/akm-task.json +1 -2
  127. package/schemas/akm-workflow.json +1 -13
  128. package/dist/assets/improve-strategies/frequent.json +0 -15
  129. package/dist/assets/improve-strategies/memory-focus.json +0 -15
  130. package/dist/indexer/search/semantic-status.js +0 -142
@@ -3,11 +3,25 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { isVerbose, warn, warnVerbose } from "../core/warn.js";
5
5
  import { embedBatch } from "../llm/embedder.js";
6
- import { estimateTokenCount } from "../llm/embedders/remote.js";
6
+ import { DETERMINISTIC_EMBED_MODEL_ID, isDeterministicEmbedEnabled } from "../llm/embedders/deterministic.js";
7
+ import { DEFAULT_LOCAL_MODEL } from "../llm/embedders/local.js";
8
+ import { buildTokenBoundedBatches, DEFAULT_REMOTE_BATCH_SIZE, DEFAULT_TOKEN_BUDGET, estimateTokenCount, hasRemoteEndpoint, } from "../llm/embedders/remote.js";
7
9
  import { getEmbeddableEntryCount } from "../storage/repositories/index-entries-repository.js";
8
10
  import { deleteMeta, getMeta, setMeta } from "../storage/repositories/index-meta-repository.js";
9
- import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
10
- import { classifySemanticFailure, clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "./search/semantic-status.js";
11
+ import { getAllEntriesForEmbedding, getEmbeddingCount, isVecFastPathComplete, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, } from "../storage/repositories/index-vec-repository.js";
12
+ /** Identifies the embedding provider+model+dimension a stored vector was generated with. */
13
+ export function deriveSemanticProviderFingerprint(embedding) {
14
+ if (isDeterministicEmbedEnabled()) {
15
+ return `deterministic:${DETERMINISTIC_EMBED_MODEL_ID}`;
16
+ }
17
+ if (embedding?.endpoint) {
18
+ // Fingerprint keys on vector identity only (model + dimension). The endpoint
19
+ // is transport/routing and has no bearing on vector compatibility, so moving
20
+ // the same model+dimension to a different host must not force a full re-embed.
21
+ return `remote:${embedding.model}|${embedding.dimension ?? "default"}`;
22
+ }
23
+ return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}`;
24
+ }
11
25
  function throwIfAborted(signal) {
12
26
  if (signal?.aborted) {
13
27
  throw signal.reason instanceof Error ? signal.reason : new Error("index interrupted");
@@ -17,7 +31,7 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
17
31
  throwIfAborted(signal);
18
32
  if (config.semanticSearchMode === "off") {
19
33
  onProgress({ phase: "embeddings", message: "Semantic search disabled; skipping embeddings." });
20
- return { success: false, reason: "index-missing", message: "Semantic search is disabled." };
34
+ return { success: false, message: "Semantic search is disabled." };
21
35
  }
22
36
  // A targeted call starts from an already-published generation. Preserve its
23
37
  // trust decision in O(1): successful writes for the changed IDs keep a
@@ -48,14 +62,35 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
48
62
  });
49
63
  const texts = allEntries.map((entry) => entry.searchText);
50
64
  if (isVerbose()) {
51
- const EMBED_BATCH_SIZE = 100;
52
- const totalBatches = Math.ceil(texts.length / EMBED_BATCH_SIZE);
53
- for (const [i, entry] of allEntries.entries()) {
54
- const batchNum = Math.floor(i / EMBED_BATCH_SIZE) + 1;
55
- const chars = entry.searchText.length;
56
- const tokens = estimateTokenCount(entry.searchText);
57
- const ref = entry.itemRef;
58
- warnVerbose(`[embed] ${ref} (${chars} chars, est. ${tokens} tokens) batch ${batchNum}/${totalBatches}`);
65
+ // Mirror RemoteEmbedder's actual token-bounded batching (#874) so this
66
+ // log reflects the real request grouping rather than a fixed count of
67
+ // 100 that no longer matches what gets sent over the wire. Local runs
68
+ // don't batch by size at all (LocalEmbedder chunks by a fixed count
69
+ // for inference throughput only, never fails/skips), so there's
70
+ // nothing meaningful to report per-batch for them.
71
+ if (hasRemoteEndpoint(config.embedding ?? {})) {
72
+ const tokenBudget = config.embedding?.maxTokens ?? config.embedding?.contextLength ?? DEFAULT_TOKEN_BUDGET;
73
+ const maxCount = config.embedding?.batchSize ?? DEFAULT_REMOTE_BATCH_SIZE;
74
+ const batches = buildTokenBoundedBatches(texts, tokenBudget, maxCount);
75
+ const batchNumberByIndex = new Map();
76
+ batches.forEach((batch, batchIdx) => {
77
+ for (const i of batch.indices)
78
+ batchNumberByIndex.set(i, batchIdx + 1);
79
+ });
80
+ for (const [i, entry] of allEntries.entries()) {
81
+ const chars = entry.searchText.length;
82
+ const tokens = estimateTokenCount(entry.searchText);
83
+ const batch = batches[batchNumberByIndex.get(i) - 1];
84
+ const label = batch?.oversized
85
+ ? "oversized (skipped)"
86
+ : `batch ${batchNumberByIndex.get(i)}/${batches.length}`;
87
+ warnVerbose(`[embed] ${entry.itemRef} (${chars} chars, est. ${tokens} tokens) → ${label}`);
88
+ }
89
+ }
90
+ else {
91
+ for (const entry of allEntries) {
92
+ warnVerbose(`[embed] ${entry.itemRef} (${entry.searchText.length} chars, est. ${estimateTokenCount(entry.searchText)} tokens)`);
93
+ }
59
94
  }
60
95
  }
61
96
  let heartbeatTimer;
@@ -66,17 +101,24 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
66
101
  message: `Still generating embeddings for ${allEntries.length} entr${allEntries.length === 1 ? "y" : "ies"}; waiting on embedding provider.`,
67
102
  });
68
103
  }, 15000);
69
- const embeddings = await embedBatch(texts, config.embedding, signal);
104
+ // A failing sub-batch or an oversized document is SKIPPED by embedBatch,
105
+ // not thrown (#874) — collect what couldn't be embedded and why, so a
106
+ // few bad documents don't discard every other entry's embedding.
107
+ const skips = [];
108
+ const embeddings = await embedBatch(texts, config.embedding, signal, (skip) => skips.push(skip));
70
109
  throwIfAborted(signal);
71
110
  let storedCount = 0;
72
111
  let skippedCount = 0;
112
+ let embedFailedCount = 0;
73
113
  let vecFailedCount = 0;
74
114
  let vecUnavailableCount = 0;
75
115
  db.transaction(() => {
76
116
  for (const [i, entry] of allEntries.entries()) {
77
117
  const embedding = embeddings[i];
78
- if (!embedding)
79
- throw new Error(`Embedding provider returned no vector for ${entry.itemRef}.`);
118
+ if (!embedding) {
119
+ embedFailedCount++;
120
+ continue;
121
+ }
80
122
  const result = upsertEmbedding(db, entry.id, embedding);
81
123
  if (result.stored)
82
124
  storedCount++;
@@ -91,6 +133,14 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
91
133
  if (skippedCount > 0) {
92
134
  warn(`[embed] ${skippedCount} embedding${skippedCount === 1 ? "" : "s"} skipped (entry deleted between queue and write)`);
93
135
  }
136
+ if (embedFailedCount > 0) {
137
+ const detail = skips
138
+ .slice(0, 20)
139
+ .map((skip) => ` - ${allEntries[skip.index]?.itemRef ?? skip.index} (${skip.reason}): ${skip.message}`)
140
+ .join("\n");
141
+ const more = skips.length > 20 ? `\n ...and ${skips.length - 20} more` : "";
142
+ warn(`[embed] ${embedFailedCount} embedding${embedFailedCount === 1 ? "" : "s"} could not be generated and ${embedFailedCount === 1 ? "was" : "were"} skipped:\n${detail}${more}`);
143
+ }
94
144
  const vecGenerationComplete = targetEntryIds === undefined ? isVecFastPathComplete(db) : vecFastPathWasReady;
95
145
  setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0 && vecGenerationComplete);
96
146
  if (vecFailedCount > 0) {
@@ -103,6 +153,19 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
103
153
  message: `Stored ${storedCount} embedding${storedCount === 1 ? "" : "s"}.`,
104
154
  });
105
155
  setMeta(db, "embeddingFingerprint", currentFingerprint);
156
+ // Only a total failure (nothing at all embedded, despite having entries
157
+ // to embed) turns into a phase failure. Any partial success — the vast
158
+ // majority of a large bundle embedding fine around a handful of skips —
159
+ // must not discard what DID get stored (#874).
160
+ if (storedCount === 0 && embedFailedCount > 0) {
161
+ const firstMessage = skips[0]?.message ?? "All embeddings failed.";
162
+ // #873 removed the persisted semantic verdict, so there is no failure
163
+ // class to record — just report what happened on this run.
164
+ return {
165
+ success: false,
166
+ message: `All ${embedFailedCount} embedding batch(es) failed: ${firstMessage}`,
167
+ };
168
+ }
106
169
  return { success: true, vecInsertFailures: vecFailedCount };
107
170
  }
108
171
  finally {
@@ -116,40 +179,21 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
116
179
  onProgress({ phase: "embeddings", message: `Embedding generation failed: ${message}` });
117
180
  return {
118
181
  success: false,
119
- reason: classifySemanticFailure(message),
120
182
  message: `Semantic search verification failed: ${message}`,
121
183
  };
122
184
  }
123
185
  }
124
- /** Publish the canonical semantic health snapshot after a targeted mutation. */
125
- export function publishTargetedSemanticStatus(db, config, result) {
186
+ /**
187
+ * Update the `hasEmbeddings` DB fact after a targeted mutation, from the
188
+ * index's actual current embedding coverage — read fresh, not cached.
189
+ */
190
+ export function publishTargetedEmbeddingMeta(db, config) {
126
191
  if (config.semanticSearchMode === "off") {
127
- clearSemanticStatus();
128
192
  setMeta(db, "hasEmbeddings", "0");
129
193
  return;
130
194
  }
131
195
  const entryCount = getEmbeddableEntryCount(db);
132
196
  const embeddingCount = getEmbeddingCount(db);
133
- let status;
134
- if (entryCount === 0)
135
- status = "pending";
136
- else if (embeddingCount >= entryCount)
137
- status = isVecAvailable(db) && isVecFastPathReady(db) ? "ready-vec" : "ready-js";
138
- else
139
- status = "blocked";
140
- setMeta(db, "hasEmbeddings", status === "ready-js" || status === "ready-vec" ? "1" : "0");
141
- writeSemanticStatus({
142
- status,
143
- ...(status === "blocked" ? { reason: result.reason ?? "index-failed" } : {}),
144
- ...(status === "blocked"
145
- ? {
146
- message: result.message ??
147
- `Semantic search verification failed (${embeddingCount}/${entryCount} embeddings available).`,
148
- }
149
- : {}),
150
- providerFingerprint: deriveSemanticProviderFingerprint(config.embedding),
151
- lastCheckedAt: new Date().toISOString(),
152
- entryCount,
153
- embeddingCount,
154
- });
197
+ const ready = entryCount > 0 && embeddingCount >= entryCount;
198
+ setMeta(db, "hasEmbeddings", ready ? "1" : "0");
155
199
  }
@@ -26,7 +26,7 @@ import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from ".
26
26
  import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
27
27
  import { searchFts } from "../../storage/repositories/index-fts-repository.js";
28
28
  import { getMeta } from "../../storage/repositories/index-meta-repository.js";
29
- import { searchVec } from "../../storage/repositories/index-vec-repository.js";
29
+ import { getEmbeddingCount, searchVec } from "../../storage/repositories/index-vec-repository.js";
30
30
  import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
31
31
  import { ensureIndex } from "../ensure-index.js";
32
32
  import { collectGraphRelatedHit, loadGraphBoostContext } from "../graph/graph-boost.js";
@@ -38,7 +38,6 @@ import { typeBoostFor } from "./ranking-contributors.js";
38
38
  import { attachSearchHitAttribution, copySearchHitAttribution, getSearchHitAttribution } from "./search-attribution.js";
39
39
  import { enrichSearchHit } from "./search-hit-enrichers.js";
40
40
  import { buildEditHint, findSourceForPath, isEditable } from "./search-source.js";
41
- import { deriveSemanticProviderFingerprint, getEffectiveSemanticStatus, isSemanticRuntimeReady, readSemanticStatus, } from "./semantic-status.js";
42
41
  /**
43
42
  * Age past which search surfaces a "run akm index" hint. Reads serve the
44
43
  * existing index as-is (freshness is the writers' job — `indexWrittenAssets`
@@ -126,49 +125,14 @@ export async function searchLocal(input) {
126
125
  const disableScopedUtility = input.disableScopedUtility === true;
127
126
  const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
128
127
  const allSourceDirs = sources.map((s) => s.path);
129
- const rawStatus = readSemanticStatus();
130
- const semanticStatus = getEffectiveSemanticStatus(config, rawStatus);
131
128
  const warnings = [];
132
- if (config.semanticSearchMode === "auto" && semanticStatus === "pending") {
133
- const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
134
- if (rawStatus && rawStatus.providerFingerprint !== currentFingerprint) {
135
- warnings.push("Embedding config changed. Run 'akm index --full' to rebuild the semantic index with the new provider.");
136
- }
137
- else if (!hasConfiguredEmbeddingProvider(config)) {
138
- // #480: when semantic mode is `auto` but no embedding provider is
139
- // configured (e.g. `akm setup --yes` ran without picking one), telling
140
- // the user to "run akm setup" is misleading — they just did. Surface
141
- // the actual remediation: configure an embedding endpoint OR switch
142
- // semanticSearchMode to `off` to silence the warning.
143
- warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
144
- 'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
145
- "(b) `akm config set semanticSearchMode off` to use keyword-only search.");
146
- }
147
- else {
148
- warnings.push("Semantic search is pending verification. Run 'akm index --full' to build the semantic index now, or wait for the next background index pass.");
149
- }
150
- }
151
- if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
152
- if (!hasConfiguredEmbeddingProvider(config)) {
153
- // F7/A2: same predicate as the `pending` branch above (#480) — a
154
- // `blocked` status can outlive the provider config that produced it
155
- // (e.g. the embedding config was later unset). This is not a fault;
156
- // there is simply nothing configured to use.
157
- warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
158
- 'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
159
- "(b) `akm config set semanticSearchMode off` to use keyword-only search.");
160
- }
161
- else {
162
- // F7/A2: surface the ACTUAL diagnostic instead of one fixed generic
163
- // string. `rawStatus.reason`/`rawStatus.message` record why the
164
- // semantic backend failed (auth, network, a stuck local-model
165
- // download, …) — read at `readSemanticStatus()` above and, before
166
- // this fix, discarded here in favor of a message that never varied.
167
- const detail = rawStatus?.message ?? (rawStatus?.reason ? `reason: ${rawStatus.reason}` : undefined);
168
- warnings.push(`Semantic search is blocked${detail ? ` (${detail})` : ""}. Using keyword search until the semantic ` +
169
- "backend is healthy again. Run 'akm index --full' to retry, or " +
170
- "`akm config set semanticSearchMode off` to silence this warning.");
171
- }
129
+ // Semantic search is attempted fresh on every query (see `tryVecScores`);
130
+ // there is no cached readiness verdict to consult here. The only thing
131
+ // worth flagging ahead of the attempt is a config that can never succeed.
132
+ if (config.semanticSearchMode === "auto" && !hasConfiguredEmbeddingProvider(config)) {
133
+ warnings.push("Semantic search is enabled (semanticSearchMode='auto') but no embedding provider is configured. " +
134
+ 'Either: (a) `akm config set embedding \'{"endpoint":"...","model":"..."}\'`, or ' +
135
+ "(b) `akm config set semanticSearchMode off` to use keyword-only search.");
172
136
  }
173
137
  // Bootstrap-only: builds the index inline when it cannot serve this stash.
174
138
  // Content freshness is the writers' job (indexWrittenAssets + full runs);
@@ -662,11 +626,14 @@ function matchBeliefFilter(beliefState, filter) {
662
626
  }
663
627
  // ── Vector scorer ───────────────────────────────────────────────────────────
664
628
  async function tryVecScores(db, query, k, config) {
665
- const semanticStatus = getEffectiveSemanticStatus(config, readSemanticStatus());
666
- if (!isSemanticRuntimeReady(semanticStatus))
629
+ if (config.semanticSearchMode === "off")
667
630
  return { scores: null };
668
- const hasEmbeddings = getMeta(db, "hasEmbeddings");
669
- if (hasEmbeddings !== "1")
631
+ // A real-time completeness fact, not a cached verdict: skip the network
632
+ // round trip only when the index has never embedded anything. A PARTIAL
633
+ // failure (some entries embedded, one write degraded) still attempts —
634
+ // and if the endpoint is genuinely down, the failure surfaces as a live
635
+ // `semanticWarning` below instead of silently skipping with no signal.
636
+ if (getEmbeddingCount(db) === 0)
670
637
  return { scores: null };
671
638
  try {
672
639
  const { embed } = await import("../../llm/embedder.js");
@@ -490,31 +490,6 @@ export const salienceRankingContributor = {
490
490
  // comment. Do not add it back here without a config gate + a measured
491
491
  // curate-golden-bench delta justifying it.
492
492
  export const defaultUtilityRankingContributors = [utilityRankingContributor];
493
- /**
494
- * EVAL/DEBUG ONLY — remove named ranking contributors from a list.
495
- *
496
- * Driven by the `AKM_ABLATE_CONTRIBUTORS` env var (comma-separated contributor
497
- * `name`s). A no-op — returns the input list unchanged (same reference) — when
498
- * the env value is unset/empty, so production ranking is never affected unless
499
- * the operator opts in. Its sole purpose is per-contributor ablation for the
500
- * curate ablation harness (`scripts/akm-eval/`): run the same fixture with and without a contributor
501
- * and diff the ranked results to measure whether that contributor is load-bearing.
502
- *
503
- * NOTE (from the 2026-06 ablation analysis): a contributor's ablation delta is only observable
504
- * in the UNSATURATED score regime — once entries saturate at the `displayScore`
505
- * ceiling their contributor deltas are absorbed and ablation reads Δ=0.
506
- */
507
- export function applyContributorAblation(contributors, ablateEnv) {
508
- if (!ablateEnv)
509
- return contributors;
510
- const ablated = new Set(ablateEnv
511
- .split(",")
512
- .map((s) => s.trim())
513
- .filter(Boolean));
514
- if (ablated.size === 0)
515
- return contributors;
516
- return contributors.filter((c) => !ablated.has(c.name));
517
- }
518
493
  export function applyScoreContributors(item, ctx, contributors = defaultRankingContributors) {
519
494
  let boostSum = 0;
520
495
  for (const contributor of contributors) {
@@ -3,7 +3,7 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
5
5
  import { buildLexicalQueryPlan } from "./fts-query.js";
6
- import { applyBeliefStateScoreCeiling, applyContributorAblation, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
6
+ import { applyBeliefStateScoreCeiling, applyScoreContributors, applyUtilityContributors, defaultRankingContributors, defaultUtilityRankingContributors, } from "./ranking-contributors.js";
7
7
  export function normalizeFtsScores(results) {
8
8
  const ftsScoreMap = new Map();
9
9
  if (results.length === 0)
@@ -75,18 +75,8 @@ export function applyRankingRules(options) {
75
75
  graphContext: options.graphContext,
76
76
  projectContext: options.projectContext,
77
77
  };
78
- // Eval/debug only: lets the ablation harness drop named contributors to
79
- // measure their effect. Resolved once per query; a no-op (full lists) when
80
- // unset — see applyContributorAblation. C9: DI via
81
- // `options.ablateContributors` is now the primary path (see the field's
82
- // doc comment on RankEntriesOptions); AKM_ABLATE_CONTRIBUTORS is a
83
- // temporary fallback for the one caller that cannot yet thread the option
84
- // through (see that doc comment for why).
85
- const ablateEnv = options.ablateContributors ?? process.env.AKM_ABLATE_CONTRIBUTORS;
86
- const activeScoreContributors = applyContributorAblation(defaultRankingContributors, ablateEnv);
87
- const activeUtilityContributors = applyContributorAblation(defaultUtilityRankingContributors, ablateEnv);
88
78
  for (const item of options.items) {
89
- applyScoreContributors(item, rankingContext, activeScoreContributors);
79
+ applyScoreContributors(item, rankingContext, defaultRankingContributors);
90
80
  }
91
81
  const { global: utilScoresMap, scoped: scopedUtilScoresMap } = getUtilityScoresByIds(options.db, options.items.map((item) => item.id), options.scopeKey);
92
82
  // R2 / #692 — salience-ranking is not in defaultUtilityRankingContributors
@@ -104,7 +94,7 @@ export function applyRankingRules(options) {
104
94
  salienceRankScores,
105
95
  };
106
96
  for (const item of options.items) {
107
- applyUtilityContributors(item, utilityContext, activeUtilityContributors);
97
+ applyUtilityContributors(item, utilityContext, defaultUtilityRankingContributors);
108
98
  applyRelaxedLexicalScoreCeiling(item, queryTokens);
109
99
  // SPEC-5: demoting belief states (superseded/contradicted/archived/
110
100
  // deprecated) cap the FINAL score. The additive belief penalty inside the
@@ -1,35 +1,10 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * Shared agent-command-builder types and helpers (#563).
6
- *
7
- * Extracted from `agent/builders.ts` so per-harness builders (e.g.
8
- * `harnesses/claude/agent-builder.ts`) can depend on the common
9
- * types/validation WITHOUT creating an import cycle back through
10
- * `builders.ts` (which imports the per-harness builders into its registry).
11
- * This module is a dependency-graph LEAF for the builder subsystem.
12
- *
13
- * Behaviour-preserving: the type shapes and helper bodies are unchanged from
14
- * their previous home in `builders.ts`.
15
- */
16
- import { UsageError } from "../../core/errors.js";
17
4
  /** Models reaching a harness have already crossed the single model-map boundary. */
18
5
  export function resolveDispatchModel(request, _profile, _platform) {
19
6
  return request.model;
20
7
  }
21
- /**
22
- * Guard against values that start with `--`, which would be mis-interpreted as
23
- * CLI flags by the spawned process when used as flag values (model, systemPrompt).
24
- * Bun.spawn uses array argv so there is no shell injection, but a `--`-prefixed
25
- * value passed as the argument to `--model` or `--system-prompt` can still
26
- * confuse the CLI parser of the target process.
27
- */
28
- export function assertNotFlag(value, field) {
29
- if (value?.trimStart().startsWith("--")) {
30
- throw new UsageError(`${field} must not start with "--": ${JSON.stringify(value.slice(0, 60))}`, "INVALID_FLAG_VALUE");
31
- }
32
- }
33
8
  /**
34
9
  * Normalize a toolPolicy value to a comma-separated string suitable for a
35
10
  * CLI flag. Structured policy objects are JSON-serialized.
@@ -5,7 +5,7 @@ import { randomBytes } from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
- import { MAX_CONFIG_FILE_BYTES, readTextFileDescriptorWithLimit, writeFileAtomic } from "../../core/common.js";
8
+ import { writeFileAtomic } from "../../core/common.js";
9
9
  import { ENGINE_NAME_PATTERN_SOURCE } from "../../core/config/engine-semantics.js";
10
10
  import { ConfigError, UsageError } from "../../core/errors.js";
11
11
  import { getConfigDir } from "../../core/paths.js";
@@ -21,9 +21,6 @@ import { cloneExecutionJsonObject } from "../../execution/json.js";
21
21
  * provider mappings.
22
22
  */
23
23
  export const MODEL_MAP_VERSION = 1;
24
- export const MODEL_MAP_MAX_BYTES = MAX_CONFIG_FILE_BYTES;
25
- export const MODEL_MAP_MAX_DEPTH = 64;
26
- export const MODEL_MAP_MAX_NODES = 100_000;
27
24
  let standaloneInstalledModelMapText;
28
25
  const ENGINE_KEY_PATTERN = new RegExp(ENGINE_NAME_PATTERN_SOURCE);
29
26
  const ALIAS_KEY_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -92,62 +89,8 @@ function parseProfileLayer(value, source, jsonPath) {
92
89
  }
93
90
  return Object.freeze(out);
94
91
  }
95
- /**
96
- * Bound nesting before JSON.parse or any recursive durable-JSON clone runs.
97
- * Brackets inside strings are ignored, including escaped quote sequences.
98
- */
99
- function assertJsonDepthBudget(text, source) {
100
- let depth = 0;
101
- let inString = false;
102
- let escaped = false;
103
- for (const character of text) {
104
- if (inString) {
105
- if (escaped)
106
- escaped = false;
107
- else if (character === "\\")
108
- escaped = true;
109
- else if (character === '"')
110
- inString = false;
111
- continue;
112
- }
113
- if (character === '"') {
114
- inString = true;
115
- continue;
116
- }
117
- if (character === "{" || character === "[") {
118
- depth += 1;
119
- if (depth > MODEL_MAP_MAX_DEPTH) {
120
- invalid(source, "$", `JSON nesting exceeds the ${MODEL_MAP_MAX_DEPTH}-level limit`);
121
- }
122
- }
123
- else if (character === "}" || character === "]") {
124
- depth -= 1;
125
- }
126
- }
127
- }
128
- /** Iterative node budget, deliberately completed before recursive inference cloning. */
129
- function assertJsonNodeBudget(value, source) {
130
- const pending = [value];
131
- let count = 0;
132
- while (pending.length > 0) {
133
- const current = pending.pop();
134
- count += 1;
135
- if (count > MODEL_MAP_MAX_NODES) {
136
- invalid(source, "$", `JSON value exceeds the ${MODEL_MAP_MAX_NODES}-node limit`);
137
- }
138
- if (current !== null && typeof current === "object") {
139
- const children = Array.isArray(current) ? current : Object.values(current);
140
- for (const child of children)
141
- pending.push(child);
142
- }
143
- }
144
- }
145
92
  /** Parse one installed or user layer. Partial structured profiles are valid at this stage. */
146
93
  export function parseModelMapLayer(text, source) {
147
- if (Buffer.byteLength(text, "utf8") > MODEL_MAP_MAX_BYTES) {
148
- invalid(source, "$", `document exceeds the ${MODEL_MAP_MAX_BYTES}-byte limit`);
149
- }
150
- assertJsonDepthBudget(text, source);
151
94
  let parsed;
152
95
  try {
153
96
  parsed = JSON.parse(text);
@@ -155,7 +98,6 @@ export function parseModelMapLayer(text, source) {
155
98
  catch {
156
99
  invalid(source, "$", "invalid JSON syntax");
157
100
  }
158
- assertJsonNodeBudget(parsed, source);
159
101
  const root = requireRecord(parsed, source, "$");
160
102
  assertOnlyKeys(root, ["version", "aliases"], source, "$");
161
103
  if (root.version !== MODEL_MAP_VERSION) {
@@ -314,7 +256,7 @@ function readModelMapFile(filePath, label, optional) {
314
256
  if (!sameFileIdentity(linkStat, openedStat)) {
315
257
  throw new ConfigError(`${label} changed while it was being opened: ${filePath}.`, "INVALID_CONFIG_FILE", "Retry after ensuring no other process is replacing models.json.");
316
258
  }
317
- const text = readTextFileDescriptorWithLimit(fd, MODEL_MAP_MAX_BYTES, label, filePath);
259
+ const text = fs.readFileSync(fd, "utf8");
318
260
  fs.closeSync(fd);
319
261
  fd = undefined;
320
262
  return text;
@@ -67,7 +67,7 @@
67
67
  * name without any further wiring. The registry entry also declares
68
68
  * `structuredOutput: "none"` alongside it (`./index.ts`).
69
69
  */
70
- import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
70
+ import { resolveDispatchModel } from "../../agent/builder-shared.js";
71
71
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
72
72
  /** Canonical harness/platform id used for model-alias resolution. */
73
73
  export const AIDER_PLATFORM = "aider";
@@ -103,8 +103,6 @@ export const aiderBuilder = {
103
103
  outputSchema: true,
104
104
  }),
105
105
  build(profile, req) {
106
- assertNotFlag(req.systemPrompt, "systemPrompt");
107
- assertNotFlag(req.model, "model");
108
106
  const args = [...profile.args];
109
107
  if (req.model) {
110
108
  const resolved = resolveDispatchModel(req, profile, AIDER_PLATFORM);
@@ -32,7 +32,7 @@
32
32
  * - **systemPrompt** — `q chat` has no system-prompt flag (persona/context
33
33
  * comes from Q's own agent config files), so the system prompt is folded
34
34
  * into the positional payload ahead of the task prompt, separated by a
35
- * blank line. `assertNotFlag` still guards it.
35
+ * blank line.
36
36
  * - **schema** — the matrix places Q in the NO-structured-output tier
37
37
  * ("via prompt+validate": *(none documented)* — there is no `--json` or
38
38
  * `--output-format` to ask for). The JSON Schema is therefore passed
@@ -62,7 +62,7 @@
62
62
  * pattern `local-runner`, structuredOutput `none` — is declared alongside it
63
63
  * (`./index.ts`).
64
64
  */
65
- import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
65
+ import { resolveDispatchModel } from "../../agent/builder-shared.js";
66
66
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
67
67
  /** Canonical harness/platform id used for model-alias resolution. */
68
68
  export const AMAZONQ_PLATFORM = "amazonq";
@@ -116,8 +116,6 @@ export const amazonqBuilder = {
116
116
  outputSchema: true,
117
117
  }),
118
118
  build(profile, req) {
119
- assertNotFlag(req.systemPrompt, "systemPrompt");
120
- assertNotFlag(req.model, "model");
121
119
  // Built-in q profiles would ship `args: []`; headless dispatch is the
122
120
  // `chat` subcommand. Don't double it when a user profile already pins it.
123
121
  const extra = profile.args[0] === "chat" ? profile.args.slice(1) : [...profile.args];
@@ -129,9 +127,6 @@ export const amazonqBuilder = {
129
127
  // flags: dropping a restriction must never widen to --trust-all-tools.
130
128
  const entries = toolPolicyEntries(req.tools);
131
129
  if (entries !== undefined) {
132
- for (const tool of entries) {
133
- assertNotFlag(tool, "tools entry");
134
- }
135
130
  // Q's documented allowlist form is equals-joined and comma-separated
136
131
  // (`--trust-tools=fs_read,fs_write`); an empty list trusts no tools.
137
132
  args.push(`--trust-tools=${entries.join(",")}`);
@@ -34,7 +34,7 @@
34
34
  *
35
35
  * The builder's `platform` stays `'claude'` (the canonical harness id).
36
36
  */
37
- import { assertNotFlag, normalizeTools, resolveDispatchModel, } from "../../agent/builder-shared.js";
37
+ import { normalizeTools, resolveDispatchModel, } from "../../agent/builder-shared.js";
38
38
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
39
39
  /**
40
40
  * Assemble the positional prompt: the task prompt and — when a schema is
@@ -68,9 +68,6 @@ export const claudeBuilder = {
68
68
  outputSchema: true,
69
69
  }),
70
70
  build(profile, req) {
71
- assertNotFlag(req.systemPrompt, "systemPrompt");
72
- assertNotFlag(req.model, "model");
73
- assertNotFlag(req.agent, "agent");
74
71
  const args = [...profile.args];
75
72
  if (req.agent) {
76
73
  args.push("--agent", req.agent);
@@ -55,7 +55,7 @@
55
55
  import { mkdtempSync, writeFileSync } from "node:fs";
56
56
  import { tmpdir } from "node:os";
57
57
  import { join } from "node:path";
58
- import { assertNotFlag, resolveDispatchModel } from "../../agent/builder-shared.js";
58
+ import { resolveDispatchModel } from "../../agent/builder-shared.js";
59
59
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
60
60
  /**
61
61
  * Write a node's JSON Schema to a fresh temp file for `--output-schema`.
@@ -77,7 +77,6 @@ export function writeCodexOutputSchemaFile(schema) {
77
77
  * extractor); akm never depends on it (plan §"Session, MCP, and identity").
78
78
  */
79
79
  export function codexResumeArgs(sessionId) {
80
- assertNotFlag(sessionId, "sessionId");
81
80
  return ["exec", "resume", sessionId];
82
81
  }
83
82
  /**
@@ -114,8 +113,6 @@ export const codexBuilder = {
114
113
  outputSchema: true,
115
114
  }),
116
115
  build(profile, req) {
117
- assertNotFlag(req.systemPrompt, "systemPrompt");
118
- assertNotFlag(req.model, "model");
119
116
  // Built-in codex profiles ship `args: []`; headless dispatch is the `exec`
120
117
  // subcommand. Don't double it when a user profile already pins it.
121
118
  const extra = profile.args[0] === "exec" ? profile.args.slice(1) : [...profile.args];
@@ -21,8 +21,7 @@
21
21
  * - **systemPrompt** — Copilot CLI has no system-prompt flag (it reads
22
22
  * repo/user custom-instructions files instead), so the system prompt is
23
23
  * folded into the `-p` payload ahead of the task prompt, separated by a
24
- * blank line. `assertNotFlag` still guards it so a `--`-prefixed system
25
- * prompt cannot turn the front of the `-p` value into a flag.
24
+ * blank line.
26
25
  * - **schema** — the matrix places Copilot in the "via prompt+validate" tier
27
26
  * (no native `--output-schema` equivalent, unlike Codex), so the JSON
28
27
  * Schema is passed through the prompt: a directive matching the engine's
@@ -45,7 +44,7 @@
45
44
  * that registry, so this builder is reachable under the `"copilot"` platform
46
45
  * name without any further wiring.
47
46
  */
48
- import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
47
+ import { resolveDispatchModel } from "../../agent/builder-shared.js";
49
48
  import { createAgentRequestLowerer } from "../../agent/request-lowering.js";
50
49
  /** Canonical harness/platform id used for model-alias resolution. */
51
50
  export const COPILOT_PLATFORM = "copilot";
@@ -98,8 +97,6 @@ export const copilotBuilder = {
98
97
  outputSchema: true,
99
98
  }),
100
99
  build(profile, req) {
101
- assertNotFlag(req.systemPrompt, "systemPrompt");
102
- assertNotFlag(req.model, "model");
103
100
  const args = [...profile.args];
104
101
  if (req.model) {
105
102
  const resolved = resolveDispatchModel(req, profile, COPILOT_PLATFORM);
@@ -110,7 +107,6 @@ export const copilotBuilder = {
110
107
  // Structured policy objects (entries === undefined) emit NO allow flags:
111
108
  // dropping a restriction must never widen to --allow-all-tools.
112
109
  for (const tool of entries ?? []) {
113
- assertNotFlag(tool, "tools entry");
114
110
  args.push("--allow-tool", tool);
115
111
  }
116
112
  }