akm-cli 0.9.0-rc.1 → 0.9.0-rc.2

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 (228) hide show
  1. package/CHANGELOG.md +223 -9
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/stash-skeleton/README.md +28 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  23. package/dist/assets/tasks/core/backup.yml +1 -0
  24. package/dist/assets/tasks/core/extract.yml +1 -0
  25. package/dist/assets/tasks/core/improve.yml +1 -0
  26. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  27. package/dist/assets/tasks/core/sync.yml +1 -0
  28. package/dist/assets/tasks/core/version-check.yml +1 -0
  29. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  30. package/dist/assets/templates/html/health.html +5 -1
  31. package/dist/cli/config-migrate.js +31 -138
  32. package/dist/cli/config-validate.js +10 -8
  33. package/dist/cli.js +48 -14
  34. package/dist/commands/agent/agent-dispatch.js +17 -16
  35. package/dist/commands/agent/agent-support.js +0 -24
  36. package/dist/commands/agent/contribute-cli.js +5 -15
  37. package/dist/commands/backup-cli.js +54 -0
  38. package/dist/commands/config-cli.js +45 -159
  39. package/dist/commands/env/secret.js +8 -5
  40. package/dist/commands/health/checks.js +130 -83
  41. package/dist/commands/health/html-report.js +4 -0
  42. package/dist/commands/health/improve-metrics.js +30 -32
  43. package/dist/commands/health/llm-usage.js +19 -19
  44. package/dist/commands/health/md-report.js +4 -0
  45. package/dist/commands/health/metrics.js +2 -1
  46. package/dist/commands/health/surfaces.js +5 -4
  47. package/dist/commands/health.js +1 -1
  48. package/dist/commands/improve/consolidate/chunking.js +2 -2
  49. package/dist/commands/improve/consolidate.js +28 -25
  50. package/dist/commands/improve/distill/promote-memory.js +5 -12
  51. package/dist/commands/improve/distill/quality-gate.js +5 -7
  52. package/dist/commands/improve/distill.js +16 -5
  53. package/dist/commands/improve/eligibility.js +22 -12
  54. package/dist/commands/improve/extract-cli.js +47 -19
  55. package/dist/commands/improve/extract.js +110 -62
  56. package/dist/commands/improve/improve-cli.js +38 -16
  57. package/dist/commands/improve/improve-result-file.js +30 -24
  58. package/dist/commands/improve/improve-strategies.js +137 -0
  59. package/dist/commands/improve/improve.js +60 -30
  60. package/dist/commands/improve/locks.js +66 -45
  61. package/dist/commands/improve/loop-stages.js +75 -33
  62. package/dist/commands/improve/memory/memory-belief.js +79 -7
  63. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  64. package/dist/commands/improve/preparation.js +71 -73
  65. package/dist/commands/improve/procedural.js +3 -2
  66. package/dist/commands/improve/recombine.js +2 -1
  67. package/dist/commands/improve/reflect.js +119 -214
  68. package/dist/commands/improve/shared.js +11 -5
  69. package/dist/commands/lint/base-linter.js +152 -42
  70. package/dist/commands/mv-cli.js +809 -0
  71. package/dist/commands/proposal/proposal-cli.js +18 -8
  72. package/dist/commands/proposal/propose.js +64 -69
  73. package/dist/commands/read/knowledge.js +436 -4
  74. package/dist/commands/read/remember-cli.js +39 -2
  75. package/dist/commands/read/search-cli.js +6 -1
  76. package/dist/commands/registry-cli.js +29 -14
  77. package/dist/commands/remember.js +2 -0
  78. package/dist/commands/sources/init.js +13 -14
  79. package/dist/commands/sources/schema-repair.js +2 -4
  80. package/dist/commands/sources/source-add.js +62 -73
  81. package/dist/commands/sources/source-manage.js +50 -46
  82. package/dist/commands/sources/stash-cli.js +41 -4
  83. package/dist/commands/tasks/default-tasks.js +12 -12
  84. package/dist/commands/tasks/tasks-cli.js +7 -3
  85. package/dist/commands/tasks/tasks.js +113 -18
  86. package/dist/commands/wiki-cli.js +9 -10
  87. package/dist/core/asset/frontmatter.js +12 -2
  88. package/dist/core/common.js +5 -3
  89. package/dist/core/config/config-io.js +28 -17
  90. package/dist/core/config/config-schema.js +358 -66
  91. package/dist/core/config/config-types.js +3 -3
  92. package/dist/core/config/config-version.js +29 -0
  93. package/dist/core/config/config-walker.js +98 -27
  94. package/dist/core/config/config.js +132 -266
  95. package/dist/core/config/deep-merge.js +41 -0
  96. package/dist/core/config/engine-semantics.js +32 -0
  97. package/dist/core/errors.js +2 -2
  98. package/dist/core/extra-params.js +61 -0
  99. package/dist/core/file-lock.js +201 -56
  100. package/dist/core/improve-result.js +178 -0
  101. package/dist/core/maintenance-barrier.js +119 -0
  102. package/dist/core/migration-backup.js +416 -0
  103. package/dist/core/paths.js +3 -0
  104. package/dist/core/redaction.js +358 -0
  105. package/dist/core/state/migrations.js +17 -2
  106. package/dist/core/state-db.js +44 -1
  107. package/dist/indexer/db/db.js +116 -1
  108. package/dist/indexer/graph/graph-extraction.js +28 -16
  109. package/dist/indexer/index-writer-lock.js +31 -24
  110. package/dist/indexer/index-written-assets.js +15 -6
  111. package/dist/indexer/indexer.js +47 -2
  112. package/dist/indexer/passes/memory-inference.js +10 -6
  113. package/dist/indexer/passes/metadata.js +250 -0
  114. package/dist/indexer/search/db-search.js +111 -44
  115. package/dist/indexer/search/fts-query.js +41 -0
  116. package/dist/indexer/search/ranking-contributors.js +48 -0
  117. package/dist/indexer/search/ranking.js +36 -23
  118. package/dist/indexer/search/search-fields.js +11 -1
  119. package/dist/integrations/agent/builder-shared.js +7 -0
  120. package/dist/integrations/agent/builders.js +5 -52
  121. package/dist/integrations/agent/config.js +3 -143
  122. package/dist/integrations/agent/detect.js +17 -2
  123. package/dist/integrations/agent/engine-resolution.js +202 -0
  124. package/dist/integrations/agent/index.js +1 -2
  125. package/dist/integrations/agent/model-aliases.js +7 -2
  126. package/dist/integrations/agent/profiles.js +6 -99
  127. package/dist/integrations/agent/runner-dispatch.js +76 -13
  128. package/dist/integrations/agent/runner.js +76 -207
  129. package/dist/integrations/agent/spawn.js +4 -6
  130. package/dist/integrations/harnesses/aider/agent-builder.js +2 -3
  131. package/dist/integrations/harnesses/aider/index.js +0 -1
  132. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -3
  133. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  134. package/dist/integrations/harnesses/claude/agent-builder.js +2 -3
  135. package/dist/integrations/harnesses/claude/index.js +0 -2
  136. package/dist/integrations/harnesses/codex/agent-builder.js +2 -3
  137. package/dist/integrations/harnesses/codex/index.js +0 -1
  138. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -3
  139. package/dist/integrations/harnesses/copilot/index.js +0 -1
  140. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -3
  141. package/dist/integrations/harnesses/gemini/index.js +0 -1
  142. package/dist/integrations/harnesses/index.js +1 -24
  143. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  144. package/dist/integrations/harnesses/opencode/index.js +0 -6
  145. package/dist/integrations/harnesses/opencode-sdk/harness.js +1 -6
  146. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +274 -191
  147. package/dist/integrations/harnesses/openhands/agent-builder.js +2 -3
  148. package/dist/integrations/harnesses/openhands/index.js +0 -1
  149. package/dist/integrations/harnesses/pi/agent-builder.js +2 -3
  150. package/dist/integrations/harnesses/pi/index.js +0 -1
  151. package/dist/integrations/harnesses/types.js +1 -32
  152. package/dist/integrations/lockfile.js +32 -21
  153. package/dist/llm/client.js +48 -14
  154. package/dist/llm/feature-gate.js +15 -47
  155. package/dist/llm/graph-extract.js +1 -1
  156. package/dist/llm/index-passes.js +8 -42
  157. package/dist/llm/memory-infer-impl.js +1 -1
  158. package/dist/llm/usage-persist.js +4 -0
  159. package/dist/llm/usage-telemetry.js +35 -5
  160. package/dist/output/shapes/helpers.js +2 -1
  161. package/dist/output/shapes/passthrough.js +2 -0
  162. package/dist/output/text/helpers.js +3 -1
  163. package/dist/schemas/akm-config.json +11013 -8600
  164. package/dist/schemas/akm-task.json +87 -0
  165. package/dist/schemas/akm-workflow.json +57 -13
  166. package/dist/scripts/migrate-storage.js +8591 -509
  167. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +8623 -484
  168. package/dist/setup/detected-engines.js +142 -0
  169. package/dist/setup/engine-config.js +89 -0
  170. package/dist/setup/setup.js +236 -132
  171. package/dist/setup/steps/connection.js +61 -32
  172. package/dist/setup/steps/platforms.js +4 -4
  173. package/dist/setup/steps.js +3 -2
  174. package/dist/storage/database.js +13 -1
  175. package/dist/storage/engines/sqlite-migrations.js +1 -0
  176. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  177. package/dist/storage/repositories/task-history-repository.js +78 -0
  178. package/dist/storage/repositories/workflow-runs-repository.js +9 -8
  179. package/dist/tasks/parser.js +138 -52
  180. package/dist/tasks/runner.js +71 -75
  181. package/dist/tasks/schema.js +1 -1
  182. package/dist/tasks/validator.js +11 -6
  183. package/dist/wiki/wiki.js +9 -8
  184. package/dist/workflows/authoring/workflow-program-template.yaml +3 -3
  185. package/dist/workflows/concurrency-policy.js +15 -0
  186. package/dist/workflows/db.js +65 -13
  187. package/dist/workflows/exec/brief.js +18 -24
  188. package/dist/workflows/exec/frozen-judge.js +47 -0
  189. package/dist/workflows/exec/native-executor.js +124 -65
  190. package/dist/workflows/exec/report.js +93 -33
  191. package/dist/workflows/exec/run-workflow.js +38 -25
  192. package/dist/workflows/exec/scheduler.js +12 -41
  193. package/dist/workflows/exec/step-work.js +91 -35
  194. package/dist/workflows/ir/compile.js +13 -26
  195. package/dist/workflows/ir/freeze.js +243 -0
  196. package/dist/workflows/ir/plan-hash.js +40 -5
  197. package/dist/workflows/ir/schema.js +542 -1
  198. package/dist/workflows/parser.js +7 -0
  199. package/dist/workflows/program/parser.js +132 -23
  200. package/dist/workflows/program/project.js +3 -4
  201. package/dist/workflows/program/schema.js +2 -2
  202. package/dist/workflows/resource-limits.js +20 -0
  203. package/dist/workflows/runtime/plan-classifier.js +187 -0
  204. package/dist/workflows/runtime/runs.js +54 -47
  205. package/dist/workflows/runtime/workflow-asset-loader.js +0 -22
  206. package/dist/workflows/validator.js +25 -0
  207. package/docs/data-and-telemetry.md +2 -2
  208. package/docs/migration/release-notes/0.6.0.md +1 -1
  209. package/docs/migration/release-notes/0.7.0.md +5 -4
  210. package/docs/migration/v0.8-to-v0.9.md +401 -0
  211. package/package.json +4 -2
  212. package/schemas/akm-config.json +16638 -0
  213. package/schemas/akm-task.json +87 -0
  214. package/schemas/akm-workflow.json +372 -0
  215. package/dist/commands/improve/improve-profiles.js +0 -168
  216. package/dist/core/config/config-migration.js +0 -602
  217. package/dist/core/deep-merge.js +0 -38
  218. package/dist/llm/call-ai.js +0 -62
  219. package/dist/setup/legacy-config.js +0 -106
  220. package/docs/README.md +0 -104
  221. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  222. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  223. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  224. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  225. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  226. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  227. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  228. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -16,7 +16,7 @@
16
16
  * 1. An LLM profile must be configured (no provider = no extraction). When
17
17
  * absent, `resolveIndexPassLLM("graph", config)` returns `undefined`
18
18
  * and the pass short-circuits.
19
- * 2. `profiles.improve.default.processes.graphExtraction.enabled !== false`
19
+ * 2. The selected strategy's `processes.graphExtraction.enabled !== false`
20
20
  * — the feature-gate layer (historically v1 spec §14, since superseded by
21
21
  * the 0.8.0 profile shape). Set to `false` to block the pass at the
22
22
  * feature-gate layer (no network call may ever issue).
@@ -260,7 +260,7 @@ function reuseGraphNode(previousNodes, candidate, bodyHash) {
260
260
  * 1. **Provider configured** — an LLM profile must be selectable. Without a
261
261
  * configured provider, `resolveIndexPassLLM("graph", config)` returns
262
262
  * `undefined` (the pass cannot run because there is no model to call).
263
- * 2. **Feature gate** — `profiles.improve.default.processes.graphExtraction.enabled`
263
+ * 2. **Feature gate** — the selected strategy's `processes.graphExtraction.enabled`
264
264
  * (defaults to `true`). When `false`, no network call may issue regardless
265
265
  * of per-pass settings.
266
266
  * 3. **Per-pass gate** — `index.graph.llm` (defaults to `true`). When
@@ -276,21 +276,25 @@ function reuseGraphNode(previousNodes, candidate, bodyHash) {
276
276
  */
277
277
  export async function runGraphExtractionPass(ctx) {
278
278
  const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
279
+ const invocationOwnsConnection = Object.hasOwn(ctx, "llmConfig");
279
280
  // Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
280
- // (profiles.improve.default.processes.graphExtraction.enabled). Defaults to
281
+ // (selected strategy's processes.graphExtraction.enabled). Defaults to
281
282
  // enabled when the key is absent.
282
- if (!isProcessEnabled("index", "graph_extraction", config))
283
+ if (!invocationOwnsConnection && !isProcessEnabled("index", "graph_extraction", config))
283
284
  return { ...EMPTY_RESULT };
284
285
  // Gate 2 — per-pass opt-out (#208). Returns the resolved llm config or
285
286
  // `undefined` when the pass should not run.
286
- const llmConfig = resolveIndexPassLLM("graph", config);
287
+ const llmConfig = Object.hasOwn(ctx, "llmConfig") ? ctx.llmConfig : resolveIndexPassLLM("graph", config);
287
288
  if (!llmConfig) {
288
- const reason = getIndexPassConfig(config.index, "graph")?.llm === false
289
- ? "index.graph.llm is false"
290
- : "no default LLM profile is configured";
289
+ const reason = getIndexPassConfig(config.index, "graph")?.enabled === false
290
+ ? "index.graph.enabled is false"
291
+ : "no LLM engine is configured";
291
292
  warnVerbose(`graph extraction: skipped because ${reason}.`);
292
293
  return { ...EMPTY_RESULT };
293
294
  }
295
+ const featureConfig = invocationOwnsConnection
296
+ ? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
297
+ : config;
294
298
  // The pass only writes to the primary (working) stash. Read-only caches
295
299
  // (git, npm, website) are deliberately untouched — the graph artifact for
296
300
  // those sources would be clobbered by the next sync().
@@ -309,10 +313,14 @@ export async function runGraphExtractionPass(ctx) {
309
313
  for (const queued of drained) {
310
314
  if (signal?.aborted)
311
315
  break;
312
- await extractGraphForSingleFile(db, primary.path, queued.filePath, queued.bodyHash, { config, signal });
316
+ await extractGraphForSingleFile(db, primary.path, queued.filePath, queued.bodyHash, {
317
+ config: featureConfig,
318
+ signal,
319
+ llmConfig,
320
+ });
313
321
  }
314
322
  }
315
- const includeTypes = getGraphExtractionIncludeTypes(config);
323
+ const includeTypes = options.includeTypes ?? getGraphExtractionIncludeTypes(config);
316
324
  let eligible = collectEligibleFiles(primary.path, includeTypes).filter((candidate) => !options.candidatePaths || options.candidatePaths.has(candidate.absPath));
317
325
  // P2 (#624): when topN is set and a DB is available, rank the (already
318
326
  // candidate-filtered) eligible set by utility_scores DESC and keep only the
@@ -357,7 +365,7 @@ export async function runGraphExtractionPass(ctx) {
357
365
  // Resolve the effective batch size. Falls back to
358
366
  // DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE (4) when unset, and clamps against
359
367
  // `llm.contextLength` if the model's context window is configured.
360
- const batchSize = resolveBatchSize(getIndexPassConfig(config.index, "graph")?.graphExtractionBatchSize, llmConfig.contextLength);
368
+ const batchSize = resolveBatchSize(options.batchSize ?? getIndexPassConfig(config.index, "graph")?.graphExtractionBatchSize, llmConfig.contextLength);
361
369
  const extractionRunId = crypto.randomUUID();
362
370
  const extractorId = getGraphExtractorId({ model: llmConfig.model, batchSize, includeTypes });
363
371
  const cacheVariant = extractorId;
@@ -438,7 +446,7 @@ export async function runGraphExtractionPass(ctx) {
438
446
  }
439
447
  if (!cached) {
440
448
  telemetry.cacheMisses += 1;
441
- const extraction = await graphExtract.extractGraphFromBody(llmConfig, candidate.body, signal, config, onFallback, { batchState, telemetry: runtimeTelemetry });
449
+ const extraction = await graphExtract.extractGraphFromBody(llmConfig, candidate.body, signal, featureConfig, onFallback, { batchState, telemetry: runtimeTelemetry });
442
450
  cached = {
443
451
  entities: extraction.entities,
444
452
  relations: extraction.relations,
@@ -562,7 +570,7 @@ export async function runGraphExtractionPass(ctx) {
562
570
  telemetry.cacheMisses += uncachedChunk.length;
563
571
  // extractGraphFromBodies always returns an array of the same length
564
572
  // as bodies (it falls back per-asset for any missing indices).
565
- const batchExtractions = await graphExtract.extractGraphFromBodies(llmConfig, bodies, signal, config, onFallback, { batchState, telemetry: runtimeTelemetry });
573
+ const batchExtractions = await graphExtract.extractGraphFromBodies(llmConfig, bodies, signal, featureConfig, onFallback, { batchState, telemetry: runtimeTelemetry });
566
574
  // Map LLM results back to original positions and write cache entries.
567
575
  let llmIdx = 0;
568
576
  for (let j = 0; j < chunk.length; j++) {
@@ -718,12 +726,16 @@ export async function extractGraphForSingleFile(db, stashRoot, filePath, bodyHas
718
726
  }
719
727
  else {
720
728
  const config = opts?.config ?? loadConfig();
721
- if (!isProcessEnabled("index", "graph_extraction", config))
729
+ const invocationOwnsConnection = Object.hasOwn(opts ?? {}, "llmConfig");
730
+ if (!invocationOwnsConnection && !isProcessEnabled("index", "graph_extraction", config))
722
731
  return false;
723
- const llmConfig = resolveIndexPassLLM("graph", config);
732
+ const llmConfig = Object.hasOwn(opts ?? {}, "llmConfig") ? opts?.llmConfig : resolveIndexPassLLM("graph", config);
724
733
  if (!llmConfig)
725
734
  return false; // model-available guard
726
- const result = await graphExtract.extractGraphFromBody(llmConfig, body, opts?.signal, config);
735
+ const featureConfig = invocationOwnsConnection
736
+ ? { ...config, index: { ...config.index, graph: { ...config.index?.graph, enabled: true } } }
737
+ : config;
738
+ const result = await graphExtract.extractGraphFromBody(llmConfig, body, opts?.signal, featureConfig);
727
739
  extraction = {
728
740
  entities: result.entities,
729
741
  relations: result.relations,
@@ -3,15 +3,15 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "../core/file-lock.js";
6
+ import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync, } from "../core/file-lock.js";
7
+ import { tryAcquireMaintenanceBarrier } from "../core/maintenance-barrier.js";
7
8
  import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
8
9
  const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
9
10
  const INDEX_WRITER_WAIT_MS = 100;
10
11
  const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
11
12
  const heldLocks = new Map();
12
- function buildPayload(purpose, pid = process.pid) {
13
- return JSON.stringify({
14
- pid,
13
+ function buildPayload(purpose) {
14
+ return createLockPayload({
15
15
  purpose,
16
16
  dbPath: getDbPath(),
17
17
  startedAt: new Date().toISOString(),
@@ -34,17 +34,19 @@ function releaseHeldLock(lockPath) {
34
34
  return;
35
35
  heldLocks.delete(lockPath);
36
36
  process.off("exit", held.exitHandler);
37
- releaseLockIfOwned(lockPath, process.pid);
37
+ releaseLock(held.ownership);
38
38
  }
39
- function retainHeldLock(lockPath) {
39
+ function retainHeldLock(lockPath, ownership) {
40
40
  const existing = heldLocks.get(lockPath);
41
41
  if (existing) {
42
42
  existing.depth += 1;
43
43
  return { lockPath, release: () => releaseHeldLock(lockPath) };
44
44
  }
45
- const exitHandler = () => releaseLockIfOwned(lockPath, process.pid);
45
+ if (!ownership)
46
+ throw new Error(`Missing ownership for index writer lock at ${lockPath}.`);
47
+ const exitHandler = () => releaseLock(ownership);
46
48
  process.on("exit", exitHandler);
47
- heldLocks.set(lockPath, { depth: 1, exitHandler });
49
+ heldLocks.set(lockPath, { depth: 1, exitHandler, ownership });
48
50
  return { lockPath, release: () => releaseHeldLock(lockPath) };
49
51
  }
50
52
  export async function acquireIndexWriterLease(options) {
@@ -53,25 +55,30 @@ export async function acquireIndexWriterLease(options) {
53
55
  const startedAt = Date.now();
54
56
  const maxWaitMs = options.maxWaitMs ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
55
57
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
56
- if (heldLocks.has(lockPath)) {
57
- options.onAcquired?.({ waitedMs: 0 });
58
- return retainHeldLock(lockPath);
59
- }
60
58
  let lastWaitNoticeMs = 0;
61
59
  while (true) {
62
60
  throwIfAborted(options.signal);
63
- if (tryAcquireLockSync(lockPath, buildPayload(options.purpose))) {
64
- options.onAcquired?.({ waitedMs: Date.now() - startedAt });
65
- return retainHeldLock(lockPath);
66
- }
67
- const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
68
- if (probe.state === "held" && probe.holderPid === process.pid) {
69
- options.onAcquired?.({ waitedMs: Date.now() - startedAt });
70
- return retainHeldLock(lockPath);
71
- }
72
- if (probe.state === "stale") {
73
- releaseLock(lockPath);
74
- continue;
61
+ const releaseBarrier = tryAcquireMaintenanceBarrier();
62
+ if (releaseBarrier) {
63
+ try {
64
+ if (heldLocks.has(lockPath)) {
65
+ options.onAcquired?.({ waitedMs: Date.now() - startedAt });
66
+ return retainHeldLock(lockPath);
67
+ }
68
+ const ownership = tryAcquireLockSync(lockPath, buildPayload(options.purpose));
69
+ if (ownership) {
70
+ options.onAcquired?.({ waitedMs: Date.now() - startedAt });
71
+ return retainHeldLock(lockPath, ownership);
72
+ }
73
+ const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
74
+ if (probe.state === "stale") {
75
+ if (reclaimStaleLock(lockPath, probe))
76
+ continue;
77
+ }
78
+ }
79
+ finally {
80
+ releaseBarrier();
81
+ }
75
82
  }
76
83
  if (mode === "try")
77
84
  return undefined;
@@ -23,7 +23,8 @@ import fs from "node:fs";
23
23
  import path from "node:path";
24
24
  import { getDbPath } from "../core/paths.js";
25
25
  import { warnVerbose } from "../core/warn.js";
26
- import { closeDatabase, getEntryCount, openExistingDatabase, rebuildFts, upsertEntry } from "./db/db.js";
26
+ import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
27
+ import { closeDatabase, getEntryCount, openExistingDatabase, rebuildFts, upsertEntry, upsertWorkflowDocument, } from "./db/db.js";
27
28
  import { generateMetadataFlat } from "./passes/metadata.js";
28
29
  import { buildSearchText } from "./search/search-fields.js";
29
30
  /**
@@ -69,10 +70,10 @@ export async function indexWrittenAssets(stashDir, filePaths) {
69
70
  for (const file of files) {
70
71
  const generated = await generateMetadataFlat(stashDir, [file]);
71
72
  const entry = generated.entries[0];
72
- // Workflows carry a side-table document upsert this fast path doesn't
73
- // do; no current caller writes them, but guard so one never lands
74
- // half-indexed.
75
- if (entry && entry.type !== "workflow")
73
+ // Workflows also carry a workflow_documents side-table upsert handled
74
+ // below, mirroring the full walk since `akm mv` rewrites citer files
75
+ // that can be workflows.
76
+ if (entry)
76
77
  pairs.push({ file, entry });
77
78
  }
78
79
  if (pairs.length === 0)
@@ -91,7 +92,15 @@ export async function indexWrittenAssets(stashDir, filePaths) {
91
92
  catch {
92
93
  // stat raced a delete — index without the size, like the full walk does.
93
94
  }
94
- upsertEntry(db, entryKey, path.dirname(file), file, stashDir, entryWithSize, buildSearchText(entry));
95
+ const entryId = upsertEntry(db, entryKey, path.dirname(file), file, stashDir, entryWithSize, buildSearchText(entry));
96
+ if (entry.type === "workflow") {
97
+ // Same contract as the full walk (indexer.ts): the renderer cached
98
+ // the parsed document during metadata generation; persist it so the
99
+ // workflow runtime never sees an entry without its document.
100
+ const doc = takeWorkflowDocument(entry);
101
+ if (doc)
102
+ upsertWorkflowDocument(db, entryId, doc, fs.readFileSync(file));
103
+ }
95
104
  }
96
105
  rebuildFts(db, { incremental: true });
97
106
  }
@@ -182,6 +182,11 @@ async function runFinalizePhase(ctx) {
182
182
  setMeta(db, "stashDir", stashDir);
183
183
  setMeta(db, "stashDirs", JSON.stringify(sourceDirs));
184
184
  setMeta(db, "hasEmbeddings", embeddingResult.success ? "1" : "0");
185
+ // Stash-organization conventions (SPEC-8): track which `index.indexBodyOpening`
186
+ // state the index was built with, and warn while the flag diverges from it.
187
+ const bodyOpeningWarning = reconcileBodyOpeningIndexState(db, config.index?.indexBodyOpening === true, ctx.full || !isIncremental);
188
+ if (bodyOpeningWarning)
189
+ warn(bodyOpeningWarning);
185
190
  warnIfVecMissing(db);
186
191
  const totalEntries = getEntryCount(db);
187
192
  const semanticEntryCount = getEmbeddableEntryCount(db);
@@ -209,6 +214,41 @@ async function runFinalizePhase(ctx) {
209
214
  // suppress unused warning — sources was previously used inline
210
215
  void sources;
211
216
  }
217
+ /**
218
+ * Stash-organization conventions (SPEC-8): reconcile the `index.indexBodyOpening`
219
+ * flag with the state the index was last FULLY built with (index_meta key
220
+ * `indexBodyOpening`), returning a warning message while they diverge.
221
+ *
222
+ * Incremental runs re-extract only changed files (and embeddings are only
223
+ * generated for rows lacking one), so a flag toggle leaves the index MIXED
224
+ * until a full rebuild — `akm index --full` re-extracts every entry and wipes
225
+ * embeddings so they regenerate from the new text. The warning therefore
226
+ * repeats on every incremental run until a full walk records the flag state
227
+ * as applied.
228
+ *
229
+ * A missing meta key on an incremental run means the index predates this
230
+ * feature, i.e. it was necessarily built with the flag OFF — so the absent
231
+ * key reads (and is seeded) as "0", never as the current flag value. This
232
+ * keeps the most likely real toggle scenario — upgrade, enable the flag, run
233
+ * a plain `akm index` — warning until `--full` runs (review finding).
234
+ *
235
+ * Exported for tests; production's only caller is the finalize phase above.
236
+ */
237
+ export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
238
+ const bodyOpeningFlag = flagEnabled ? "1" : "0";
239
+ const prevBodyOpeningFlag = getMeta(db, "indexBodyOpening") ?? "0";
240
+ // Only a full walk (which includes the first build ever) may record the
241
+ // current flag as the applied state; incremental runs preserve — or, for a
242
+ // pre-feature index, seed — the state of the last full build.
243
+ setMeta(db, "indexBodyOpening", isFullWalk ? bodyOpeningFlag : prevBodyOpeningFlag);
244
+ if (isFullWalk || prevBodyOpeningFlag === bodyOpeningFlag)
245
+ return undefined;
246
+ return (`index.indexBodyOpening is ${flagEnabled ? "enabled" : "disabled"} but the index was built with it ` +
247
+ `${flagEnabled ? "disabled" : "enabled"}. Incremental runs only re-extract changed files, so ` +
248
+ "indexed text and embeddings are stale for unchanged entries. Run `akm index --full` to apply the new " +
249
+ "setting everywhere (embeddings regenerate), and re-mint collapse-detector canary baselines via " +
250
+ "`akm improve canary --refresh` if you use them.");
251
+ }
212
252
  // ── Clean pass ───────────────────────────────────────────────────────────────
213
253
  /**
214
254
  * Post-index clean pass: scan the `entries` table for rows whose source file
@@ -733,10 +773,11 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
733
773
  // P3 — wall-clock budget for the enrichment pass. Defaults to llm.timeoutMs
734
774
  // (or 10 minutes if not set). Users can extend this via llm.timeoutMs in
735
775
  // config — no separate knob needed.
736
- const budgetMs = (llmConfig.timeoutMs ?? 10 * 60 * 1000) * Math.max(totalEntries, 1);
737
- const enrichDeadline = AbortSignal.timeout(budgetMs);
776
+ const enrichDeadline = createEnrichmentDeadline(llmConfig.timeoutMs, totalEntries);
738
777
  let deadlineHit = false;
739
778
  const enrichSignal = (() => {
779
+ if (!enrichDeadline)
780
+ return signal ?? new AbortController().signal;
740
781
  if (!signal)
741
782
  return enrichDeadline;
742
783
  // Combine: abort when either fires.
@@ -865,6 +906,10 @@ async function enhanceDirsWithLlm(db, config, dirsNeedingLlm, onProgress, signal
865
906
  warn(`LLM enhancement failed for ${failed}/${summary.attempted} entries — they were left un-enhanced.${sample}`);
866
907
  }
867
908
  }
909
+ export function createEnrichmentDeadline(timeoutMs, totalEntries) {
910
+ const perEntryTimeoutMs = timeoutMs === undefined ? 10 * 60 * 1000 : timeoutMs;
911
+ return perEntryTimeoutMs === null ? undefined : AbortSignal.timeout(perEntryTimeoutMs * Math.max(totalEntries, 1));
912
+ }
868
913
  async function generateEmbeddingsForDb(db, config, onProgress, signal) {
869
914
  throwIfAborted(signal);
870
915
  if (config.semanticSearchMode === "off") {
@@ -19,7 +19,7 @@
19
19
  * the parent without re-running the LLM.
20
20
  *
21
21
  * Disabling — two orthogonal gates:
22
- * 1. `profiles.improve.default.processes.memoryInference.enabled = false`
22
+ * 1. The selected strategy sets `processes.memoryInference.enabled = false`
23
23
  * blocks the pass at the feature-flag layer (no network call may ever
24
24
  * issue). Historically the v1 spec §14 gate, superseded by the 0.8.0
25
25
  * profile shape.
@@ -60,7 +60,7 @@ const FM_CAPTURE_MODE = "captureMode";
60
60
  *
61
61
  * Two orthogonal gates:
62
62
  *
63
- * 1. **Feature gate** — `profiles.improve.default.processes.memoryInference.enabled`
63
+ * 1. **Feature gate** — the selected strategy's `processes.memoryInference.enabled`
64
64
  * (defaults to `true`). When `false`, no network call may issue regardless
65
65
  * of per-pass settings.
66
66
  * 2. **Per-pass gate** — `resolveIndexPassLLM("memory", config)` (which
@@ -72,6 +72,7 @@ const FM_CAPTURE_MODE = "captureMode";
72
72
  */
73
73
  export async function runMemoryInferencePass(ctx) {
74
74
  const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
75
+ const invocationOwnsConnection = Object.hasOwn(ctx, "llmConfig");
75
76
  const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
76
77
  const result = {
77
78
  considered: 0,
@@ -90,15 +91,18 @@ export async function runMemoryInferencePass(ctx) {
90
91
  // gate) bubbles up into the pass result.
91
92
  const inferTelemetry = {};
92
93
  // Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
93
- // (profiles.improve.default.processes.memoryInference.enabled). Defaults to
94
+ // (selected strategy's processes.memoryInference.enabled). Defaults to
94
95
  // enabled when the key is absent.
95
- if (!isProcessEnabled("index", "memory_inference", config))
96
+ if (!invocationOwnsConnection && !isProcessEnabled("index", "memory_inference", config))
96
97
  return result;
97
98
  // Gate 2 — per-pass opt-out (#208). Returns the resolved llm config or
98
99
  // `undefined` when the pass should not run.
99
- const llmConfig = resolveIndexPassLLM("memory", config);
100
+ const llmConfig = Object.hasOwn(ctx, "llmConfig") ? ctx.llmConfig : resolveIndexPassLLM("memory", config);
100
101
  if (!llmConfig)
101
102
  return result;
103
+ const featureConfig = invocationOwnsConnection
104
+ ? { ...config, index: { ...config.index, memory: { ...config.index?.memory, enabled: true } } }
105
+ : config;
102
106
  // The pass only writes to the primary (working) stash. Read-only caches
103
107
  // (git, npm, website) are deliberately untouched — writing inferred
104
108
  // children there would be clobbered by the next sync().
@@ -173,7 +177,7 @@ export async function runMemoryInferencePass(ctx) {
173
177
  retryAttempts += 1;
174
178
  };
175
179
  const derived = db
176
- ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
180
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, featureConfig, (evt) => {
177
181
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
178
182
  }, inferTelemetry, onRetryAttempt), validate, undefined, "", {
179
183
  onCacheHit: () => {