akm-cli 0.9.1 → 0.9.2-alpha.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 (350) hide show
  1. package/CHANGELOG.md +103 -28
  2. package/README.md +3 -1
  3. package/SECURITY.md +1 -1
  4. package/STABILITY.md +1 -1
  5. package/dist/akm +2 -2
  6. package/dist/akm-migrate +2 -2
  7. package/dist/assets/hints/cli-hints-full.md +14 -9
  8. package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
  9. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  10. package/dist/assets/models.json +35 -0
  11. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
  12. package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
  13. package/dist/assets/tasks/core/extract.yml +6 -5
  14. package/dist/assets/tasks/core/improve.yml +6 -5
  15. package/dist/assets/tasks/core/index-refresh.yml +6 -5
  16. package/dist/assets/tasks/core/sync.yml +6 -5
  17. package/dist/assets/tasks/core/version-check.yml +6 -5
  18. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
  19. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
  20. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
  21. package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
  22. package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
  23. package/dist/cli/confirm.js +2 -2
  24. package/dist/cli/parse-args.js +3 -24
  25. package/dist/cli/retired-commands.js +1 -1
  26. package/dist/cli/shared.js +2 -2
  27. package/dist/cli.js +11 -9
  28. package/dist/commands/agent/agent-dispatch.js +55 -89
  29. package/dist/commands/agent/contribute-cli.js +12 -45
  30. package/dist/commands/command/builtin-action.js +32 -0
  31. package/dist/commands/command/command-cli.js +99 -0
  32. package/dist/commands/command/command-execution.js +308 -0
  33. package/dist/commands/command/execution-source-loader.js +176 -0
  34. package/dist/commands/command/portable-template.js +60 -0
  35. package/dist/commands/config-cli.js +10 -4
  36. package/dist/commands/env/env.js +4 -2
  37. package/dist/commands/feedback-cli.js +1 -1
  38. package/dist/commands/health/checks.js +241 -29
  39. package/dist/commands/health/html-report.js +0 -14
  40. package/dist/commands/health/report-view-model.js +0 -1
  41. package/dist/commands/health/surfaces.js +6 -7
  42. package/dist/commands/health/types.js +0 -2
  43. package/dist/commands/health.js +63 -18
  44. package/dist/commands/improve/collapse-detector.js +5 -6
  45. package/dist/commands/improve/consolidate.js +251 -214
  46. package/dist/commands/improve/distill/promote-memory.js +71 -34
  47. package/dist/commands/improve/distill/quality-gate.js +17 -5
  48. package/dist/commands/improve/distill.js +232 -155
  49. package/dist/commands/improve/eligibility.js +112 -79
  50. package/dist/commands/improve/execution.js +57 -0
  51. package/dist/commands/improve/extract-cli.js +5 -5
  52. package/dist/commands/improve/extract-prompt.js +64 -22
  53. package/dist/commands/improve/extract.js +608 -360
  54. package/dist/commands/improve/improve-strategies.js +43 -14
  55. package/dist/commands/improve/improve.js +249 -29
  56. package/dist/commands/improve/loop-stages.js +11 -17
  57. package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
  58. package/dist/commands/improve/outcome-loop.js +22 -38
  59. package/dist/commands/improve/planner.js +134 -0
  60. package/dist/commands/improve/preparation.js +730 -409
  61. package/dist/commands/improve/reflect.js +386 -223
  62. package/dist/commands/improve/run-context.js +3 -4
  63. package/dist/commands/improve/salience.js +6 -58
  64. package/dist/commands/improve/session-asset.js +12 -12
  65. package/dist/commands/lint/index.js +101 -29
  66. package/dist/commands/migrate-cli.js +11 -69
  67. package/dist/commands/migration-tool.js +6 -9
  68. package/dist/commands/models-cli.js +27 -0
  69. package/dist/commands/proposal/drain.js +258 -186
  70. package/dist/commands/proposal/proposal-cli.js +32 -10
  71. package/dist/commands/proposal/proposal.js +2 -5
  72. package/dist/commands/proposal/propose.js +192 -172
  73. package/dist/commands/proposal/repository.js +54 -91
  74. package/dist/commands/proposal/validators/proposal-validators.js +9 -7
  75. package/dist/commands/read/curate.js +53 -22
  76. package/dist/commands/read/registry-search.js +25 -9
  77. package/dist/commands/read/remember-cli.js +14 -2
  78. package/dist/commands/read/search.js +10 -4
  79. package/dist/commands/read/show.js +139 -153
  80. package/dist/commands/registry-cli.js +16 -7
  81. package/dist/commands/remember.js +33 -18
  82. package/dist/commands/sources/add-cli.js +19 -178
  83. package/dist/commands/sources/bundle-cli.js +15 -3
  84. package/dist/commands/sources/dangerous-env-audit.js +135 -0
  85. package/dist/commands/sources/info.js +2 -1
  86. package/dist/commands/sources/installed-stashes.js +901 -177
  87. package/dist/commands/sources/schema-repair.js +174 -95
  88. package/dist/commands/sources/self-update.js +30 -74
  89. package/dist/commands/sources/source-add.js +3 -5
  90. package/dist/commands/sources/sources-cli.js +2 -15
  91. package/dist/commands/sources/update-transaction.js +220 -0
  92. package/dist/commands/tasks/tasks-cli.js +3 -3
  93. package/dist/commands/tasks/tasks.js +736 -317
  94. package/dist/commands/workflow-cli.js +2 -2
  95. package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
  96. package/dist/core/adapter/adapters/akm-adapter.js +85 -35
  97. package/dist/core/adapter/adapters/akm-lint.js +54 -39
  98. package/dist/core/adapter/adapters/akm-metadata.js +45 -45
  99. package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
  100. package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
  101. package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
  102. package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
  103. package/dist/core/adapter/adapters/index.js +0 -9
  104. package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
  105. package/dist/core/adapter/adapters/okf-adapter.js +4 -0
  106. package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
  107. package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
  108. package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
  109. package/dist/core/adapter/execution-source.js +308 -0
  110. package/dist/core/adapter/recognize-match.js +36 -13
  111. package/dist/core/adapter/registry.js +0 -9
  112. package/dist/core/asset/stash-meta.js +94 -4
  113. package/dist/core/common.js +6 -11
  114. package/dist/core/config/config-io.js +3 -3
  115. package/dist/core/config/config-schema.js +18 -40
  116. package/dist/core/config/config-sources.js +11 -21
  117. package/dist/core/config/config-walker.js +31 -13
  118. package/dist/core/config/config.js +23 -26
  119. package/dist/core/config/schema/engines.js +8 -7
  120. package/dist/core/config/schema/improve-processes.js +29 -5
  121. package/dist/core/config/schema/index-config.js +0 -27
  122. package/dist/core/config/schema/primitives.js +1 -23
  123. package/dist/core/config/schema/sources-bundles.js +13 -16
  124. package/dist/core/errors.js +2 -0
  125. package/dist/core/events.js +68 -32
  126. package/dist/core/extra-params.js +1 -0
  127. package/dist/core/improve-result.js +315 -0
  128. package/dist/core/lesson-lint.js +0 -6
  129. package/dist/core/maintenance-barrier.js +4 -4
  130. package/dist/core/network-policy.js +152 -0
  131. package/dist/core/paths.js +1 -1
  132. package/dist/core/recognition-util.js +4 -4
  133. package/dist/core/registry-url.js +456 -0
  134. package/dist/core/state/migrations.js +161 -47
  135. package/dist/core/state-db.js +453 -80
  136. package/dist/core/system-error.js +32 -0
  137. package/dist/core/time.js +2 -12
  138. package/dist/core/write-source.js +0 -18
  139. package/dist/execution/directory-identity.js +52 -0
  140. package/dist/execution/executable-identity.js +107 -0
  141. package/dist/execution/guarded-source.js +398 -0
  142. package/dist/execution/json.js +95 -0
  143. package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
  144. package/dist/execution/record.js +55 -0
  145. package/dist/execution/resolved-request.js +730 -0
  146. package/dist/execution/source.js +320 -0
  147. package/dist/indexer/bundle-identity-guard.js +5 -4
  148. package/dist/indexer/db/graph-db.js +33 -0
  149. package/dist/indexer/graph/graph-boost.js +3 -4
  150. package/dist/indexer/graph/graph-extraction.js +562 -373
  151. package/dist/indexer/index-written-assets.js +78 -39
  152. package/dist/indexer/indexer.js +471 -432
  153. package/dist/indexer/installations.js +6 -0
  154. package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
  155. package/dist/indexer/materialize-embeddings.js +155 -0
  156. package/dist/indexer/passes/memory-inference.js +227 -174
  157. package/dist/indexer/passes/metadata.js +263 -118
  158. package/dist/indexer/scan/doc-to-entry.js +7 -10
  159. package/dist/indexer/scan/drain-dir.js +51 -23
  160. package/dist/indexer/search/db-search.js +156 -50
  161. package/dist/indexer/search/fts-query.js +40 -40
  162. package/dist/indexer/search/ranking.js +36 -1
  163. package/dist/indexer/search/search-attribution.js +3 -1
  164. package/dist/indexer/search/search-fields.js +23 -14
  165. package/dist/indexer/search/search-hit-enrichers.js +1 -1
  166. package/dist/indexer/search/search-source.js +7 -16
  167. package/dist/indexer/search/semantic-status.js +10 -1
  168. package/dist/indexer/usage/show-usage.js +105 -0
  169. package/dist/indexer/usage/usage-events.js +7 -2
  170. package/dist/indexer/walk/matchers.js +40 -10
  171. package/dist/indexer/walk/path-resolver.js +5 -2
  172. package/dist/indexer/walk/walker.js +20 -2
  173. package/dist/integrations/agent/builder-shared.js +3 -6
  174. package/dist/integrations/agent/conversation-fallback.js +16 -0
  175. package/dist/integrations/agent/engine-resolution.js +87 -87
  176. package/dist/integrations/agent/execution-cascade.js +566 -0
  177. package/dist/integrations/agent/execution-definitions.js +211 -0
  178. package/dist/integrations/agent/execution-lowering.js +811 -0
  179. package/dist/integrations/agent/execution-preparation.js +67 -0
  180. package/dist/integrations/agent/index.js +0 -2
  181. package/dist/integrations/agent/inline-execution.js +74 -0
  182. package/dist/integrations/agent/model-map.js +515 -0
  183. package/dist/integrations/agent/persona-fallback.js +30 -0
  184. package/dist/integrations/agent/request-lowering.js +186 -0
  185. package/dist/integrations/agent/runner-dispatch.js +230 -37
  186. package/dist/integrations/agent/runner.js +12 -83
  187. package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
  188. package/dist/integrations/harnesses/aider/index.js +0 -1
  189. package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
  190. package/dist/integrations/harnesses/amazonq/index.js +0 -1
  191. package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
  192. package/dist/integrations/harnesses/claude/index.js +1 -5
  193. package/dist/integrations/harnesses/claude/session-log.js +3 -33
  194. package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
  195. package/dist/integrations/harnesses/codex/index.js +0 -1
  196. package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
  197. package/dist/integrations/harnesses/copilot/index.js +0 -1
  198. package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
  199. package/dist/integrations/harnesses/gemini/index.js +0 -1
  200. package/dist/integrations/harnesses/index.js +4 -44
  201. package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
  202. package/dist/integrations/harnesses/opencode/index.js +0 -2
  203. package/dist/integrations/harnesses/opencode/session-log.js +14 -204
  204. package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
  205. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
  206. package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
  207. package/dist/integrations/harnesses/openhands/index.js +0 -1
  208. package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
  209. package/dist/integrations/harnesses/pi/index.js +0 -1
  210. package/dist/integrations/harnesses/shared.js +0 -1
  211. package/dist/integrations/harnesses/types.js +1 -3
  212. package/dist/integrations/lockfile.js +82 -79
  213. package/dist/integrations/session-logs/index.js +6 -17
  214. package/dist/integrations/session-logs/provider-base.js +1 -29
  215. package/dist/llm/client.js +10 -5
  216. package/dist/llm/embedder.js +6 -7
  217. package/dist/llm/embedders/local.js +37 -88
  218. package/dist/llm/embedders/types.js +1 -1
  219. package/dist/llm/graph-extract.js +75 -50
  220. package/dist/llm/index-passes.js +43 -5
  221. package/dist/llm/memory-infer.js +8 -6
  222. package/dist/llm/metadata-enhance.js +5 -3
  223. package/dist/llm/structured-call.js +122 -25
  224. package/dist/output/format-exempt.js +1 -1
  225. package/dist/output/render-registry.js +0 -16
  226. package/dist/output/renderers.js +12 -7
  227. package/dist/output/shapes/curate.js +1 -0
  228. package/dist/output/shapes/helpers.js +10 -2
  229. package/dist/output/shapes/passthrough.js +2 -0
  230. package/dist/output/text/command-format.js +31 -33
  231. package/dist/output/text/health-format.js +1 -29
  232. package/dist/output/text/migrate.js +6 -56
  233. package/dist/output/text/proposal-format.js +16 -1
  234. package/dist/output/text/workflow-format.js +16 -0
  235. package/dist/registry/network.js +279 -0
  236. package/dist/registry/pinned-request-helper.js +247 -0
  237. package/dist/registry/pinned-transport.js +717 -0
  238. package/dist/registry/providers/skills-sh.js +18 -6
  239. package/dist/registry/providers/static-index.js +20 -7
  240. package/dist/registry/resolve.js +53 -28
  241. package/dist/scripts/akm-migrate-node.js +19334 -52269
  242. package/dist/scripts/akm-migrate.js +19270 -51612
  243. package/dist/setup/registry-stash-loader.js +64 -20
  244. package/dist/setup/semantic-assets.js +9 -34
  245. package/dist/setup/setup.js +12 -30
  246. package/dist/setup/source-identity.js +17 -0
  247. package/dist/setup/steps/sources.js +36 -15
  248. package/dist/setup/steps/tasks.js +39 -11
  249. package/dist/sources/providers/git-provider.js +3 -3
  250. package/dist/sources/providers/npm.js +2 -2
  251. package/dist/sources/providers/provider-utils.js +4 -3
  252. package/dist/sources/providers/website.js +11 -7
  253. package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
  254. package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
  255. package/dist/sources/website-url.js +73 -0
  256. package/dist/storage/engines/sqlite-migrations.js +81 -26
  257. package/dist/storage/managed-db.js +27 -24
  258. package/dist/storage/repositories/events-repository.js +3 -0
  259. package/dist/storage/repositories/index-connection.js +42 -10
  260. package/dist/storage/repositories/index-entries-repository.js +203 -229
  261. package/dist/storage/repositories/index-entry-mapper.js +8 -12
  262. package/dist/storage/repositories/index-entry-schema.js +255 -0
  263. package/dist/storage/repositories/index-fts-repository.js +64 -71
  264. package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
  265. package/dist/storage/repositories/index-meta-repository.js +0 -11
  266. package/dist/storage/repositories/index-schema.js +74 -350
  267. package/dist/storage/repositories/index-utility-repository.js +12 -17
  268. package/dist/storage/repositories/index-vec-repository.js +56 -7
  269. package/dist/storage/repositories/proposals-repository.js +4 -127
  270. package/dist/storage/repositories/registry-cache.js +2 -1
  271. package/dist/storage/repositories/task-history-repository.js +20 -40
  272. package/dist/storage/repositories/workflow-runs-repository.js +228 -129
  273. package/dist/storage/sqlite-read-snapshot.js +148 -0
  274. package/dist/tasks/backends/cron.js +170 -42
  275. package/dist/tasks/backends/index.js +1 -1
  276. package/dist/tasks/backends/launchd.js +787 -202
  277. package/dist/tasks/backends/schtasks.js +282 -83
  278. package/dist/tasks/embedded.js +7 -7
  279. package/dist/tasks/frozen-script.js +50 -0
  280. package/dist/tasks/resolve-akm-bin.js +5 -1
  281. package/dist/tasks/runner.js +239 -251
  282. package/dist/tasks/runtime-v3.js +281 -0
  283. package/dist/tasks/scheduler-binding.js +272 -0
  284. package/dist/tasks/scheduler-invocation.js +57 -43
  285. package/dist/tasks/scheduler-sync.js +654 -0
  286. package/dist/tasks/source-v3.js +752 -0
  287. package/dist/tasks/standalone-script-entry.js +5 -0
  288. package/dist/tasks/task-id.js +29 -0
  289. package/dist/workflows/authoring/authoring.js +15 -32
  290. package/dist/workflows/exec/dispatch-redaction.js +14 -8
  291. package/dist/workflows/exec/exec-unit.js +7 -28
  292. package/dist/workflows/exec/frozen-judge.js +57 -89
  293. package/dist/workflows/exec/lowering-notices.js +23 -0
  294. package/dist/workflows/exec/native-executor.js +301 -458
  295. package/dist/workflows/exec/param-secrets.js +4 -3
  296. package/dist/workflows/exec/run-workflow.js +26 -32
  297. package/dist/workflows/exec/step-work.js +105 -109
  298. package/dist/workflows/exec/unit-dispatch.js +103 -27
  299. package/dist/workflows/exec/unit-writer.js +3 -3
  300. package/dist/workflows/exec/worktree.js +2 -2
  301. package/dist/workflows/ir/compile.js +86 -72
  302. package/dist/workflows/ir/environment-v4.js +328 -0
  303. package/dist/workflows/ir/freeze-v4.js +122 -0
  304. package/dist/workflows/ir/plan-hash.js +13 -7
  305. package/dist/workflows/ir/schema-v4.js +525 -0
  306. package/dist/workflows/ir/schema.js +25 -284
  307. package/dist/workflows/ir/source-freeze-v4.js +506 -0
  308. package/dist/workflows/parser.js +27 -24
  309. package/dist/workflows/program/schema.js +1 -2
  310. package/dist/workflows/renderer.js +42 -29
  311. package/dist/workflows/resource-limits.js +4 -5
  312. package/dist/workflows/runtime/agent-identity.js +11 -13
  313. package/dist/workflows/runtime/plan-classifier.js +8 -8
  314. package/dist/workflows/runtime/runs.js +27 -43
  315. package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
  316. package/dist/workflows/source-files.js +373 -0
  317. package/dist/workflows/source-ir/compile.js +196 -0
  318. package/dist/workflows/source-ir/github-yaml.js +577 -0
  319. package/dist/workflows/source-ir/ordering.js +38 -0
  320. package/dist/workflows/source-ir/program.js +50 -0
  321. package/dist/workflows/source-ir/result.js +26 -0
  322. package/dist/workflows/source-ir/schema.js +772 -0
  323. package/dist/workflows/source-ir/semantics.js +242 -0
  324. package/dist/workflows/source-ir/uses.js +14 -0
  325. package/docs/README.md +2 -0
  326. package/docs/migration/README.md +3 -1
  327. package/docs/migration/release-notes/0.9.2.md +55 -0
  328. package/docs/migration/release-notes/README.md +5 -0
  329. package/docs/migration/v0.8-to-v0.9.md +76 -1077
  330. package/docs/migration/v0.9.0-troubleshooting.md +104 -516
  331. package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
  332. package/docs/reference/README.md +1 -0
  333. package/docs/reference/cli.md +230 -98
  334. package/docs/reference/configuration.md +159 -36
  335. package/docs/reference/data-and-telemetry.md +19 -1
  336. package/docs/reference/supported-formats.md +23 -3
  337. package/docs/reference/tasks.md +182 -0
  338. package/docs/reference/workflow-schema.md +91 -40
  339. package/docs/reference/workflows.md +33 -6
  340. package/package.json +10 -6
  341. package/schemas/akm-config.json +372 -224
  342. package/schemas/akm-task.json +324 -80
  343. package/schemas/akm-workflow.json +6 -9
  344. package/dist/core/migration-operation.js +0 -75
  345. package/dist/integrations/agent/model-aliases.js +0 -74
  346. package/dist/tasks/parser.js +0 -380
  347. package/dist/tasks/schema.js +0 -123
  348. package/dist/tasks/validator.js +0 -80
  349. package/dist/workflows/ir/freeze.js +0 -320
  350. package/dist/workflows/runtime/document-cache.js +0 -13
@@ -10,12 +10,13 @@ import { NotFoundError, rethrowIfTestIsolationError, UsageError } from "../../co
10
10
  import { readEvents } from "../../core/events.js";
11
11
  import { isPathAbsent } from "../../core/path-access.js";
12
12
  import { getDbPath } from "../../core/paths.js";
13
- import { deriveInstallations } from "../../indexer/installations.js";
14
- import { getWritableStashDirs, resolveSourceEntries } from "../../indexer/search/search-source.js";
13
+ import { deriveInstallations, deriveWritableBundleIds } from "../../indexer/installations.js";
14
+ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
15
15
  import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
16
16
  import { closeDatabase, openExistingDatabase, openReadonlyExistingDatabase, } from "../../storage/repositories/index-connection.js";
17
17
  import { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
18
18
  import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
19
+ import { SqliteReadSnapshotUnavailableError } from "../../storage/sqlite-read-snapshot.js";
19
20
  import { isDistillRefusedInputType } from "./distill.js";
20
21
  import { isStrategyFilteredForAllPasses } from "./improve-strategies.js";
21
22
  import { parseMemoryRef } from "./memory/derived-ref.js";
@@ -36,9 +37,37 @@ import { improveStateReadRefs } from "./source-identity.js";
36
37
  */
37
38
  function openEligibilityDb(readOnly) {
38
39
  if (readOnly)
39
- return openReadonlyExistingDatabase();
40
+ return openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true });
40
41
  return isPathAbsent(getDbPath()) ? undefined : openExistingDatabase();
41
42
  }
43
+ function describeIndexSnapshot(readOnly, status) {
44
+ if (status === "ready") {
45
+ return {
46
+ status,
47
+ reason: readOnly ? "loaded a non-mutating point-in-time copy of the existing index" : "loaded the prepared index",
48
+ };
49
+ }
50
+ if (status === "missing") {
51
+ return {
52
+ status,
53
+ reason: readOnly
54
+ ? "index.db is missing; dry-run uses an empty snapshot and does not create it"
55
+ : "index.db is missing after index preparation; the selector uses an empty snapshot",
56
+ };
57
+ }
58
+ return {
59
+ status,
60
+ reason: readOnly
61
+ ? "index.db has no entries table; dry-run uses an empty snapshot and does not migrate it"
62
+ : "index.db has no entries table; the selector uses an empty snapshot",
63
+ };
64
+ }
65
+ function describeUnavailableSnapshot(error) {
66
+ return {
67
+ status: "incompatible",
68
+ reason: `index.db cannot provide a stable non-mutating snapshot (${error.message}); dry-run uses an empty snapshot`,
69
+ };
70
+ }
42
71
  export function resolveImproveScope(scope) {
43
72
  const trimmed = scope?.trim();
44
73
  if (!trimmed)
@@ -95,17 +124,33 @@ export async function collectEligibleRefsReadOnly(scope, stashDir, improveProfil
95
124
  }
96
125
  async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, readOnly, configOverride) {
97
126
  const config = configOverride ?? loadConfig();
127
+ let sources;
128
+ try {
129
+ sources = resolveSourceEntries(stashDir, config);
130
+ }
131
+ catch {
132
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
133
+ }
134
+ if (sources.length === 0) {
135
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
136
+ }
137
+ const installations = deriveInstallations(sources);
138
+ const writableBundleIds = deriveWritableBundleIds(sources);
98
139
  if (scope.mode === "ref" && scope.value) {
99
- const writableDirs = new Set(getWritableStashDirs(stashDir, config).map((dir) => path.resolve(dir)));
100
140
  let db;
101
141
  try {
102
142
  db = openEligibilityDb(readOnly);
103
143
  if (!db) {
104
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
144
+ return {
145
+ plannedRefs: [],
146
+ memorySummary: { eligible: 0, derived: 0 },
147
+ strategyFilteredRefs: [],
148
+ indexSnapshot: describeIndexSnapshot(readOnly, "missing"),
149
+ };
105
150
  }
151
+ const indexSnapshot = describeIndexSnapshot(readOnly, "ready");
106
152
  const entries = getAllEntries(db);
107
- const entriesByItemRef = new Map(entries.flatMap((entry) => entry.bundleId && entry.conceptId ? [[`${entry.bundleId}//${entry.conceptId}`, entry]] : []));
108
- const installations = deriveInstallations(resolveSourceEntries(stashDir, config));
153
+ const entriesByItemRef = new Map(entries.map((entry) => [entry.itemRef, entry]));
109
154
  const resolved = resolveRef(scope.value, {
110
155
  defaultBundle: config.defaultBundle,
111
156
  bundles: installations.map((installation) => ({
@@ -116,27 +161,46 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
116
161
  const indexed = entriesByItemRef.get(`${resolved.bundle}//${resolved.conceptId}`);
117
162
  if (!indexed?.bundleId || !indexed.conceptId || !fs.existsSync(indexed.filePath)) {
118
163
  if (await findAssetFilePath(scope.value, stashDir)) {
119
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
164
+ return {
165
+ plannedRefs: [],
166
+ memorySummary: { eligible: 0, derived: 0 },
167
+ strategyFilteredRefs: [],
168
+ indexSnapshot,
169
+ };
120
170
  }
121
171
  throw new NotFoundError(`Asset not found in the selected writable source: ${scope.value}`, "ASSET_NOT_FOUND");
122
172
  }
123
- if (!isEntryInScope(indexed.stashDir, indexed.filePath, stashDir) ||
124
- !isEntryInWritableSource(indexed.stashDir, indexed.filePath, writableDirs)) {
125
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
173
+ if (!writableBundleIds.has(indexed.bundleId)) {
174
+ return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [], indexSnapshot };
126
175
  }
127
- const itemRef = `${indexed.bundleId}//${indexed.conceptId}`;
128
176
  return {
129
- plannedRefs: [{ ref: indexed.conceptId, itemRef, reason: "scope-ref", filePath: indexed.filePath }],
177
+ plannedRefs: [
178
+ { ref: indexed.conceptId, itemRef: indexed.itemRef, reason: "scope-ref", filePath: indexed.filePath },
179
+ ],
130
180
  memorySummary: {
131
181
  eligible: indexed.entry.type === "memory" ? 1 : 0,
132
182
  derived: indexed.entry.type === "memory" && indexed.entry.name.endsWith(".derived") ? 1 : 0,
133
183
  },
134
184
  strategyFilteredRefs: [],
185
+ indexSnapshot,
135
186
  };
136
187
  }
137
188
  catch (error) {
189
+ if (readOnly && error instanceof SqliteReadSnapshotUnavailableError) {
190
+ return {
191
+ plannedRefs: [],
192
+ memorySummary: { eligible: 0, derived: 0 },
193
+ strategyFilteredRefs: [],
194
+ indexSnapshot: describeUnavailableSnapshot(error),
195
+ };
196
+ }
138
197
  if (error instanceof Error && /no such table:\s*entries/i.test(error.message)) {
139
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
198
+ return {
199
+ plannedRefs: [],
200
+ memorySummary: { eligible: 0, derived: 0 },
201
+ strategyFilteredRefs: [],
202
+ indexSnapshot: describeIndexSnapshot(readOnly, "incompatible"),
203
+ };
140
204
  }
141
205
  throw error;
142
206
  }
@@ -145,39 +209,19 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
145
209
  closeDatabase(db);
146
210
  }
147
211
  }
148
- let sources;
149
- try {
150
- sources = resolveSourceEntries(stashDir, config);
151
- }
152
- catch {
153
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
154
- }
155
- if (sources.length === 0) {
156
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
157
- }
158
- // Only operate on writable sources — never mutate read-only registry caches
159
- // or remote stashes that the user did not mark writable.
160
- let writableDirs;
161
- try {
162
- writableDirs = getWritableStashDirs(stashDir, config);
163
- }
164
- catch {
165
- writableDirs = sources.slice(0, 1).map((s) => s.path); // fallback: primary only
166
- }
167
- const writableDirSet = new Set(writableDirs.map((d) => path.resolve(d)));
168
212
  let db;
169
213
  try {
170
214
  db = openEligibilityDb(readOnly);
171
215
  if (!db) {
172
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
216
+ return {
217
+ plannedRefs: [],
218
+ memorySummary: { eligible: 0, derived: 0 },
219
+ strategyFilteredRefs: [],
220
+ indexSnapshot: describeIndexSnapshot(readOnly, "missing"),
221
+ };
173
222
  }
174
- const entries = getAllEntries(db, scope.mode === "type" ? scope.value : undefined).filter((indexed) => {
175
- // First apply the existing stashDir-scope filter (no-op when stashDir is unset).
176
- if (!isEntryInScope(indexed.stashDir, indexed.filePath, stashDir))
177
- return false;
178
- // Then restrict to writable sources only.
179
- return isEntryInWritableSource(indexed.stashDir, indexed.filePath, writableDirSet);
180
- });
223
+ const indexSnapshot = describeIndexSnapshot(readOnly, "ready");
224
+ const entries = getAllEntries(db, scope.mode === "type" ? scope.value : undefined).filter((indexed) => writableBundleIds.has(indexed.bundleId));
181
225
  const planned = new Map();
182
226
  const strategyFiltered = new Map();
183
227
  let memoryEligible = 0;
@@ -198,7 +242,7 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
198
242
  }
199
243
  // Derive the durable item_ref from indexed provenance without another query.
200
244
  // A provenance-free row uses the short conceptId ref for improve state.
201
- const itemRef = indexed.bundleId && indexed.conceptId ? `${indexed.bundleId}//${indexed.conceptId}` : undefined;
245
+ const itemRef = indexed.itemRef;
202
246
  const isDerived = indexed.entry.name.endsWith(".derived");
203
247
  // `.derived` memories are LLM-inferred and intentionally skip reflect
204
248
  // (see the synthetic `derived-memory-reflect-skipped` branch in the
@@ -242,13 +286,27 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
242
286
  plannedRefs: [...planned.values()],
243
287
  memorySummary: { eligible: memoryEligible, derived: memoryDerived },
244
288
  strategyFilteredRefs: [...strategyFiltered.values()],
289
+ indexSnapshot,
245
290
  };
246
291
  }
247
292
  catch (error) {
248
293
  // Empty-stash setup paths can open index.db before its schema exists.
249
294
  rethrowIfTestIsolationError(error);
295
+ if (readOnly && error instanceof SqliteReadSnapshotUnavailableError) {
296
+ return {
297
+ plannedRefs: [],
298
+ memorySummary: { eligible: 0, derived: 0 },
299
+ strategyFilteredRefs: [],
300
+ indexSnapshot: describeUnavailableSnapshot(error),
301
+ };
302
+ }
250
303
  if (error instanceof Error && /no such table:\s*entries/i.test(error.message)) {
251
- return { plannedRefs: [], memorySummary: { eligible: 0, derived: 0 }, strategyFilteredRefs: [] };
304
+ return {
305
+ plannedRefs: [],
306
+ memorySummary: { eligible: 0, derived: 0 },
307
+ strategyFilteredRefs: [],
308
+ indexSnapshot: describeIndexSnapshot(readOnly, "incompatible"),
309
+ };
252
310
  }
253
311
  throw error;
254
312
  }
@@ -257,33 +315,6 @@ async function collectEligibleRefsFromIndex(scope, stashDir, improveProfile, rea
257
315
  closeDatabase(db);
258
316
  }
259
317
  }
260
- export function isEntryInScope(entryStashDir, filePath, stashDir) {
261
- if (!stashDir)
262
- return true;
263
- const resolvedEntryStashDir = path.resolve(entryStashDir);
264
- const resolvedFilePath = path.resolve(filePath);
265
- const resolvedScopeStashDir = path.resolve(stashDir);
266
- return (resolvedEntryStashDir === resolvedScopeStashDir ||
267
- resolvedEntryStashDir.startsWith(`${resolvedScopeStashDir}${path.sep}`) ||
268
- resolvedFilePath.startsWith(`${resolvedScopeStashDir}${path.sep}`));
269
- }
270
- /**
271
- * Return true when the indexed entry belongs to one of the writable source
272
- * directories. Entries from read-only registry caches or remote stashes that
273
- * the user has not marked writable must never enter the improve/distill loop.
274
- */
275
- export function isEntryInWritableSource(entryStashDir, filePath, writableDirSet) {
276
- const resolvedEntryStashDir = path.resolve(entryStashDir);
277
- const resolvedFilePath = path.resolve(filePath);
278
- for (const writableDir of writableDirSet) {
279
- if (resolvedEntryStashDir === writableDir ||
280
- resolvedEntryStashDir.startsWith(`${writableDir}${path.sep}`) ||
281
- resolvedFilePath.startsWith(`${writableDir}${path.sep}`)) {
282
- return true;
283
- }
284
- }
285
- return false;
286
- }
287
318
  export function memoryCleanupParentRef(scope, stashDir) {
288
319
  if (scope.mode !== "ref" || !scope.value)
289
320
  return undefined;
@@ -381,13 +412,13 @@ export function shouldDistillMemoryRef(ref, stashDir) {
381
412
  * metadata events are ignored so a stray `akm feedback <ref>` invocation
382
413
  * without a flag doesn't trigger downstream re-processing.
383
414
  */
384
- export function buildLatestFeedbackTsMap(refs, sinceIso, itemRefByRef) {
415
+ export function buildLatestFeedbackTsMap(refs, sinceIso, itemRefByRef, eventsCtx) {
385
416
  const out = new Map();
386
417
  if (refs.length === 0)
387
418
  return out;
388
419
  // Correlate item_ref-keyed events and conceptId-keyed direct invocations.
389
420
  const refByDurableKey = new Map(refs.flatMap((ref) => improveStateReadRefs(ref, itemRefByRef?.get(ref)).map((key) => [key, ref])));
390
- const { events } = readEvents({ type: "feedback", since: sinceIso });
421
+ const { events } = readEvents({ type: "feedback", since: sinceIso }, eventsCtx);
391
422
  for (const e of events) {
392
423
  const ref = e.ref ? refByDurableKey.get(e.ref) : undefined;
393
424
  if (!ref)
@@ -411,14 +442,14 @@ export function buildLatestFeedbackTsMap(refs, sinceIso, itemRefByRef) {
411
442
  * lesson/knowledge ref, not the source memory — joining back through the
412
443
  * payload would be fragile.
413
444
  */
414
- export function buildLatestProposalTsMap(refs, source, itemRefByRef) {
445
+ export function buildLatestProposalTsMap(refs, source, itemRefByRef, eventsCtx) {
415
446
  const out = new Map();
416
447
  if (refs.length === 0)
417
448
  return out;
418
449
  // Correlate item_ref-keyed events and conceptId-keyed direct invocations.
419
450
  const refByDurableKey = new Map(refs.flatMap((ref) => improveStateReadRefs(ref, itemRefByRef?.get(ref)).map((key) => [key, ref])));
420
451
  const eventType = source === "reflect" ? "reflect_invoked" : "distill_invoked";
421
- const { events } = readEvents({ type: eventType });
452
+ const { events } = readEvents({ type: eventType }, eventsCtx);
422
453
  for (const e of events) {
423
454
  const ref = e.ref ? refByDurableKey.get(e.ref) : undefined;
424
455
  if (!ref)
@@ -489,14 +520,16 @@ export function shouldAnalyzeMemoryCleanup(scope, eligibleMemories, primaryStash
489
520
  return false;
490
521
  return parseRefInput(scope.value).type === "memory";
491
522
  }
492
- export function buildUtilityMap(refs) {
523
+ export function buildUtilityMap(refs, readOnly = false) {
493
524
  const map = new Map();
494
525
  if (refs.length === 0)
495
526
  return map;
496
527
  const refSet = new Set(refs.map((r) => r.ref));
497
528
  let db;
498
529
  try {
499
- db = openExistingDatabase();
530
+ db = readOnly ? openReadonlyExistingDatabase(undefined, { isolatedSnapshot: true }) : openExistingDatabase();
531
+ if (!db)
532
+ return map;
500
533
  const allDbEntries = getAllEntries(db);
501
534
  const idToRef = new Map();
502
535
  for (const indexed of allDbEntries) {
@@ -526,11 +559,11 @@ export function buildUtilityMap(refs) {
526
559
  }
527
560
  return map;
528
561
  }
529
- export async function findAssetFilePath(ref, stashDir, writableDirSet) {
562
+ export async function findAssetFilePath(ref, stashDir, writableBundleIds) {
530
563
  return resolveAssetPath(ref, {
531
564
  stashDir,
532
565
  mode: "disk-only",
533
- writableDirSet,
566
+ writableBundleIds,
534
567
  directoryIndexNames: ["SKILL.md"],
535
568
  preserveDirectNameFallback: true,
536
569
  honorOrigin: true,
@@ -0,0 +1,57 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import { deepMergeConfig } from "../../core/config/deep-merge.js";
5
+ import { ConfigError } from "../../core/errors.js";
6
+ import { lowerResolvedExecutionRequest } from "../../integrations/agent/execution-lowering.js";
7
+ import { prepareInlineExecution } from "../../integrations/agent/inline-execution.js";
8
+ function own(value, key) {
9
+ return value !== undefined && Object.hasOwn(value, key);
10
+ }
11
+ function cascadeDefaults(layer) {
12
+ if (!layer)
13
+ return {};
14
+ return {
15
+ ...(own(layer, "engine") ? { engine: layer.engine } : {}),
16
+ ...(own(layer, "model") ? { model: layer.model } : {}),
17
+ ...(own(layer, "llm") ? { inference: layer.llm } : {}),
18
+ ...(own(layer, "timeoutMs") ? { timeout: layer.timeoutMs ?? null } : {}),
19
+ };
20
+ }
21
+ function mergeDefaults(farther, nearer) {
22
+ return deepMergeConfig(farther, nearer);
23
+ }
24
+ /**
25
+ * Resolve improve-owned model work through the canonical execution cascade.
26
+ * The legacy improve precedence is preserved exactly:
27
+ * defaults.llmEngine -> strategy -> process -> current invocation.
28
+ */
29
+ export function resolveImproveExecution(options) {
30
+ const defaultEngine = options.config.defaults?.llmEngine;
31
+ const profileDefaults = cascadeDefaults(options.profile);
32
+ const processDefaults = cascadeDefaults(options.process);
33
+ const currentDefaults = cascadeDefaults(options.current);
34
+ const selectedEngine = currentDefaults.engine ?? processDefaults.engine ?? profileDefaults.engine ?? defaultEngine;
35
+ if (selectedEngine === undefined || selectedEngine === null)
36
+ return null;
37
+ const invocationDefaults = mergeDefaults(defaultEngine ? { engine: defaultEngine } : {}, profileDefaults);
38
+ const current = mergeDefaults(processDefaults, currentDefaults);
39
+ const prepared = prepareInlineExecution({
40
+ content: `improve ${options.processName} execution selection`,
41
+ config: options.config,
42
+ invocationKind: "direct",
43
+ invocationDefaults,
44
+ current,
45
+ });
46
+ const lowered = lowerResolvedExecutionRequest(prepared.request, prepared.config);
47
+ return Object.freeze({ runner: lowered.runner, notices: lowered.notices });
48
+ }
49
+ export function resolveImproveLlmExecution(options) {
50
+ const resolved = resolveImproveExecution(options);
51
+ if (!resolved)
52
+ return null;
53
+ if (resolved.runner.kind !== "llm") {
54
+ throw new ConfigError(`Engine ${JSON.stringify(resolved.runner.engine ?? "unknown")} is not an LLM engine.`, "INVALID_CONFIG_FILE");
55
+ }
56
+ return { runner: resolved.runner, notices: resolved.notices };
57
+ }
@@ -7,11 +7,11 @@
7
7
  * see src/commands/proposal/proposal-cli.ts for its registration).
8
8
  *
9
9
  * Examples:
10
- * akm proposal extract --type claude-code --session-id <id>
11
- * akm proposal extract --type claude-code --since 24h
10
+ * akm proposal extract --type claude --session-id <id>
11
+ * akm proposal extract --type claude --since 24h
12
12
  * akm proposal extract --type opencode --since 7d --dry-run
13
13
  * akm proposal extract --auto # iterate all available harnesses
14
- * akm proposal extract --type claude-code --location /custom/path --session-id <id>
14
+ * akm proposal extract --type claude --location /custom/path --session-id <id>
15
15
  *
16
16
  * Output is the AkmExtractResult JSON envelope (or an aggregated one when
17
17
  * `--auto` runs multiple harnesses).
@@ -25,12 +25,12 @@ import { akmExtract, resolveStandaloneExtractPlan } from "./extract.js";
25
25
  export const extractCommand = defineJsonCommand({
26
26
  meta: {
27
27
  name: "extract",
28
- description: "Extract durable insights from native session files (claude-code, opencode) and queue them as proposals.",
28
+ description: "Extract durable insights from native session files (claude, opencode) and queue them as proposals.",
29
29
  },
30
30
  args: {
31
31
  type: {
32
32
  type: "string",
33
- description: "Harness name (claude-code, opencode). Required unless --auto.",
33
+ description: "Harness name (claude, opencode). Required unless --auto.",
34
34
  },
35
35
  "session-id": {
36
36
  type: "string",
@@ -15,6 +15,7 @@
15
15
  * the model emits get dropped before we parse.
16
16
  */
17
17
  import promptTemplate from "../../assets/prompts/extract-session.md" with { type: "text" };
18
+ import { escapeJsonStringControls, stripCodeFences, stripThinkBlocks } from "../../core/parse.js";
18
19
  const EXTRACT_CANDIDATE_NAME_PATTERN = "^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)?$";
19
20
  const EXTRACT_CANDIDATE_NAME_RE = new RegExp(EXTRACT_CANDIDATE_NAME_PATTERN);
20
21
  /**
@@ -163,6 +164,57 @@ export function buildExtractPrompt(input) {
163
164
  .replace("{{STANDARDS}}", standards)
164
165
  .replace("{{TRANSCRIPT}}", `${TRANSCRIPT_FENCE_BEGIN}\n${formatTranscript(input.events)}\n${TRANSCRIPT_FENCE_END}`);
165
166
  }
167
+ function failedExtractPayload(code, message) {
168
+ return { candidates: [], rationale_if_empty: message, parseFailure: { code, message } };
169
+ }
170
+ function parseFirstJsonObject(stdout) {
171
+ const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(stdout)));
172
+ try {
173
+ return { objectFound: text.startsWith("{"), value: JSON.parse(text) };
174
+ }
175
+ catch {
176
+ // Continue with the first object embedded in prose. Once an opening brace
177
+ // is found, never descend into a nested object if that outer object is
178
+ // malformed or truncated: doing so can turn a broken candidate into a
179
+ // superficially valid top-level payload.
180
+ }
181
+ const start = text.indexOf("{");
182
+ if (start < 0)
183
+ return { objectFound: false };
184
+ let depth = 0;
185
+ let inString = false;
186
+ let escaped = false;
187
+ for (let index = start; index < text.length; index++) {
188
+ const char = text[index];
189
+ if (inString) {
190
+ if (escaped)
191
+ escaped = false;
192
+ else if (char === "\\")
193
+ escaped = true;
194
+ else if (char === '"')
195
+ inString = false;
196
+ continue;
197
+ }
198
+ if (char === '"') {
199
+ inString = true;
200
+ continue;
201
+ }
202
+ if (char === "{")
203
+ depth += 1;
204
+ else if (char === "}") {
205
+ depth -= 1;
206
+ if (depth !== 0)
207
+ continue;
208
+ try {
209
+ return { objectFound: true, value: JSON.parse(text.slice(start, index + 1)) };
210
+ }
211
+ catch {
212
+ return { objectFound: true };
213
+ }
214
+ }
215
+ }
216
+ return { objectFound: true };
217
+ }
166
218
  /**
167
219
  * Parse the LLM's JSON response into a structured {@link ExtractPayload}.
168
220
  * Defensive — drops candidates that violate the shape rather than failing
@@ -170,32 +222,22 @@ export function buildExtractPrompt(input) {
170
222
  */
171
223
  export function parseExtractPayload(stdout) {
172
224
  if (!stdout || stdout.trim().length === 0) {
173
- return { candidates: [], rationale_if_empty: "LLM returned empty response" };
225
+ return failedExtractPayload("empty_response", "LLM returned an empty response");
174
226
  }
175
- let parsed;
176
- try {
177
- parsed = JSON.parse(stdout);
227
+ const parsed = parseFirstJsonObject(stdout);
228
+ if (parsed.value === undefined) {
229
+ return parsed.objectFound
230
+ ? failedExtractPayload("invalid_json_object", "JSON object was found but could not be parsed")
231
+ : failedExtractPayload("no_json_object", "LLM response: no JSON object found");
178
232
  }
179
- catch {
180
- // Tolerate prose preamble/postamble by extracting the first balanced
181
- // top-level JSON object.
182
- const start = stdout.indexOf("{");
183
- const end = stdout.lastIndexOf("}");
184
- if (start === -1 || end <= start) {
185
- return { candidates: [], rationale_if_empty: `LLM response was not parseable JSON` };
186
- }
187
- try {
188
- parsed = JSON.parse(stdout.slice(start, end + 1));
189
- }
190
- catch {
191
- return { candidates: [], rationale_if_empty: `LLM response was not parseable JSON` };
192
- }
233
+ if (!parsed.value || typeof parsed.value !== "object" || Array.isArray(parsed.value)) {
234
+ return failedExtractPayload("invalid_payload", "LLM response JSON was not an object");
193
235
  }
194
- if (!parsed || typeof parsed !== "object") {
195
- return { candidates: [], rationale_if_empty: "LLM response was not an object" };
236
+ const obj = parsed.value;
237
+ if (!Array.isArray(obj.candidates)) {
238
+ return failedExtractPayload("invalid_payload", "LLM response JSON did not contain a candidates array");
196
239
  }
197
- const obj = parsed;
198
- const rawCandidates = Array.isArray(obj.candidates) ? obj.candidates : [];
240
+ const rawCandidates = obj.candidates;
199
241
  const candidates = [];
200
242
  for (const raw of rawCandidates) {
201
243
  if (!raw || typeof raw !== "object")