akm-cli 0.9.0-rc.9 → 0.9.1-beta.1

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 (477) hide show
  1. package/CHANGELOG.md +1770 -44
  2. package/README.md +38 -47
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +499 -0
  5. package/dist/akm +148 -35
  6. package/dist/{akm-migrate-storage → akm-migrate} +6 -9
  7. package/dist/assets/hints/cli-hints-full.md +223 -95
  8. package/dist/assets/hints/cli-hints-short.md +85 -22
  9. package/dist/assets/improve-strategies/default.json +1 -1
  10. package/dist/assets/improve-strategies/reflect-distill.json +1 -1
  11. package/dist/assets/prompts/memory-infer-user.md +2 -3
  12. package/dist/assets/stash-skeleton/README.md +6 -5
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +2 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +2 -0
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +2 -0
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +2 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +2 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +2 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +2 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +2 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +2 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -0
  24. package/dist/assets/stash-skeleton/facts/conventions/organization.md +20 -9
  25. package/dist/assets/tasks/core/extract.yml +1 -1
  26. package/dist/assets/tasks/core/version-check.yml +1 -1
  27. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  28. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  29. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  30. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  31. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  32. package/dist/assets/templates/html/health.html +1 -3
  33. package/dist/assets/workflows/workflow-template.md +32 -15
  34. package/dist/cli/invocation.js +40 -15
  35. package/dist/cli/parse-args.js +0 -22
  36. package/dist/cli/retired-commands.js +121 -0
  37. package/dist/cli/shared.js +154 -22
  38. package/dist/cli/unknown-flags.js +236 -0
  39. package/dist/cli-node.mjs +2 -1
  40. package/dist/cli.js +696 -258
  41. package/dist/commands/agent/agent-dispatch.js +14 -3
  42. package/dist/commands/agent/contribute-cli.js +74 -89
  43. package/dist/commands/completions.js +79 -22
  44. package/dist/commands/config-cli.js +17 -150
  45. package/dist/commands/env/env-cli.js +59 -143
  46. package/dist/commands/env/env.js +12 -163
  47. package/dist/commands/env/marker-path.js +6 -0
  48. package/dist/commands/env/secret-cli.js +36 -66
  49. package/dist/commands/env/secret.js +24 -57
  50. package/dist/commands/feedback-cli.js +148 -88
  51. package/dist/commands/health/accept-rate.js +58 -0
  52. package/dist/commands/health/advisories.js +3 -4
  53. package/dist/commands/health/checks.js +85 -23
  54. package/dist/commands/health/html-report.js +7 -10
  55. package/dist/commands/health/improve-metrics.js +25 -83
  56. package/dist/commands/health/llm-usage.js +2 -1
  57. package/dist/commands/health/md-report.js +5 -9
  58. package/dist/commands/health/metrics.js +62 -20
  59. package/dist/commands/health/renderers.js +47 -0
  60. package/dist/commands/health/report-view-model.js +4 -5
  61. package/dist/commands/health/stash-exposure.js +1 -1
  62. package/dist/commands/health/surfaces.js +4 -122
  63. package/dist/commands/health/task-runs.js +3 -67
  64. package/dist/commands/health/types-improve.js +7 -0
  65. package/dist/commands/health.js +160 -35
  66. package/dist/commands/improve/anti-collapse.js +2 -2
  67. package/dist/commands/improve/autonomy-gate.js +68 -0
  68. package/dist/commands/improve/collapse-detector.js +41 -40
  69. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  70. package/dist/commands/improve/consolidate/merge.js +4 -0
  71. package/dist/commands/improve/consolidate.js +140 -1000
  72. package/dist/commands/improve/distill/promote-memory.js +12 -12
  73. package/dist/commands/improve/distill/quality-gate.js +11 -6
  74. package/dist/commands/improve/distill.js +58 -69
  75. package/dist/commands/improve/eligibility.js +101 -55
  76. package/dist/commands/improve/extract-cli.js +14 -133
  77. package/dist/commands/improve/improve-cli.js +98 -114
  78. package/dist/commands/improve/improve-result-file.js +1 -28
  79. package/dist/commands/improve/improve-strategies.js +8 -5
  80. package/dist/commands/improve/improve.js +254 -101
  81. package/dist/commands/improve/locks.js +7 -0
  82. package/dist/commands/improve/loop-stages.js +182 -20
  83. package/dist/commands/improve/memory/derived-ref.js +45 -43
  84. package/dist/commands/improve/memory/memory-belief.js +1 -1
  85. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  86. package/dist/commands/improve/memory/memory-improve.js +14 -5
  87. package/dist/commands/improve/outcome-loop.js +22 -65
  88. package/dist/commands/improve/preparation.js +114 -123
  89. package/dist/commands/improve/proactive-maintenance.js +2 -5
  90. package/dist/commands/improve/reflect.js +56 -160
  91. package/dist/commands/improve/run-context.js +5 -0
  92. package/dist/commands/improve/salience.js +11 -122
  93. package/dist/commands/improve/session-asset.js +4 -0
  94. package/dist/commands/improve/source-identity.js +10 -38
  95. package/dist/commands/lint/base-linter.js +51 -131
  96. package/dist/commands/lint/env-key-rules.js +31 -47
  97. package/dist/commands/lint/index.js +444 -88
  98. package/dist/commands/lint/types.js +22 -1
  99. package/dist/commands/{events.js → log.js} +33 -38
  100. package/dist/commands/migrate-cli.js +92 -12
  101. package/dist/commands/migration-tool.js +46 -0
  102. package/dist/commands/observability-cli.js +70 -209
  103. package/dist/commands/proposal/drain.js +101 -29
  104. package/dist/commands/proposal/proposal-cli.js +76 -48
  105. package/dist/commands/proposal/proposal.js +54 -18
  106. package/dist/commands/proposal/propose-cli.js +88 -0
  107. package/dist/commands/proposal/propose.js +23 -15
  108. package/dist/commands/proposal/repository.js +718 -279
  109. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  110. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  111. package/dist/commands/proposal/validators/proposals.js +4 -7
  112. package/dist/commands/read/curate.js +34 -53
  113. package/dist/commands/read/knowledge.js +150 -95
  114. package/dist/commands/read/registry-search.js +2 -2
  115. package/dist/commands/read/remember-cli.js +42 -15
  116. package/dist/commands/read/search-cli.js +180 -78
  117. package/dist/commands/read/search.js +58 -43
  118. package/dist/commands/read/show.js +197 -141
  119. package/dist/commands/registry-cli.js +12 -51
  120. package/dist/commands/remember.js +14 -57
  121. package/dist/commands/sources/add-cli.js +100 -31
  122. package/dist/commands/sources/bundle-cli.js +166 -0
  123. package/dist/commands/sources/bundle-config-ops.js +7 -2
  124. package/dist/commands/sources/info.js +30 -7
  125. package/dist/commands/sources/init.js +12 -12
  126. package/dist/commands/sources/installed-stashes.js +387 -98
  127. package/dist/commands/sources/schema-repair.js +3 -2
  128. package/dist/commands/sources/self-update.js +131 -38
  129. package/dist/commands/sources/source-add.js +72 -17
  130. package/dist/commands/sources/source-clone.js +129 -45
  131. package/dist/commands/sources/source-manage.js +43 -23
  132. package/dist/commands/sources/sources-cli.js +57 -208
  133. package/dist/commands/sources/stash-cli.js +46 -53
  134. package/dist/commands/tasks/tasks-cli.js +91 -97
  135. package/dist/commands/tasks/tasks.js +284 -423
  136. package/dist/commands/workflow-cli.js +181 -450
  137. package/dist/core/abort-deadline.js +28 -0
  138. package/dist/core/adapter/adapters/agent-skills-adapter.js +67 -5
  139. package/dist/core/adapter/adapters/akm-adapter.js +60 -38
  140. package/dist/core/adapter/adapters/akm-lint.js +109 -42
  141. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  142. package/dist/core/adapter/adapters/akm-task-adapter.js +54 -31
  143. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  144. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  145. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  146. package/dist/core/adapter/adapters/index.js +6 -6
  147. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  148. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  149. package/dist/core/adapter/adapters/shared.js +3 -19
  150. package/dist/core/adapter/adapters/tool-dir-shared.js +13 -6
  151. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  152. package/dist/core/adapter/detect-adapter.js +17 -0
  153. package/dist/core/adapter/recognize-match.js +6 -4
  154. package/dist/core/adapter/validate-context.js +214 -0
  155. package/dist/core/asset/akm-markdown.js +63 -0
  156. package/dist/core/asset/asset-placement.js +20 -6
  157. package/dist/core/asset/asset-ref.js +11 -9
  158. package/dist/core/asset/frontmatter-lint.js +30 -0
  159. package/dist/core/asset/frontmatter.js +29 -9
  160. package/dist/core/asset/markdown.js +40 -51
  161. package/dist/core/asset/resolve-ref.js +89 -18
  162. package/dist/core/asset/stash-meta.js +1 -1
  163. package/dist/core/bundle-id.js +51 -0
  164. package/dist/core/common.js +218 -44
  165. package/dist/core/concurrent.js +32 -0
  166. package/dist/core/config/config-io.js +12 -1
  167. package/dist/core/config/config-schema.js +35 -8
  168. package/dist/core/config/config-sources.js +55 -11
  169. package/dist/core/config/config-walker.js +25 -9
  170. package/dist/core/config/config.js +9 -48
  171. package/dist/core/config/experimental.js +21 -0
  172. package/dist/core/config/schema/embedding.js +5 -1
  173. package/dist/core/config/schema/experimental.js +30 -0
  174. package/dist/core/config/schema/improve-processes.js +0 -6
  175. package/dist/core/config/schema/improve.js +21 -3
  176. package/dist/core/config/schema/index-config.js +8 -15
  177. package/dist/core/config/schema/output.js +4 -1
  178. package/dist/core/config/schema/setup.js +9 -18
  179. package/dist/core/config/schema/sources-bundles.js +49 -33
  180. package/dist/core/config/schema/workflow.js +14 -3
  181. package/dist/core/env-secret-ref.js +76 -46
  182. package/dist/core/errors.js +43 -12
  183. package/dist/core/events.js +76 -152
  184. package/dist/core/file-change.js +6 -5
  185. package/dist/core/file-lock.js +7 -1
  186. package/dist/core/fs-txn.js +83 -7
  187. package/dist/core/git-message.js +2 -2
  188. package/dist/core/improve-result.js +6 -100
  189. package/dist/core/json-schema.js +327 -9
  190. package/dist/core/lesson-lint.js +1 -17
  191. package/dist/core/logs-db.js +2 -1
  192. package/dist/core/loopback.js +89 -0
  193. package/dist/core/migration-operation.js +33 -2
  194. package/dist/core/mutation-target.js +78 -0
  195. package/dist/core/parse.js +4 -1
  196. package/dist/core/path-access.js +107 -0
  197. package/dist/core/paths.js +17 -20
  198. package/dist/core/recognition-util.js +12 -14
  199. package/dist/core/redaction.js +98 -17
  200. package/dist/core/spawn-env.js +234 -0
  201. package/dist/core/standards/resolve-standards-context.js +2 -14
  202. package/dist/core/standards/resolve-stash-standards.js +2 -2
  203. package/dist/core/standards/resolve-type-conventions.js +2 -2
  204. package/dist/core/state/migrations.js +41 -18
  205. package/dist/core/state-db-scope.js +134 -0
  206. package/dist/core/state-db.js +5 -14
  207. package/dist/core/structured.js +1 -1
  208. package/dist/core/subprocess.js +182 -36
  209. package/dist/core/text-truncation.js +9 -5
  210. package/dist/core/type-presentation.js +3 -3
  211. package/dist/core/warn.js +0 -3
  212. package/dist/core/write-provenance.js +85 -0
  213. package/dist/core/write-source.js +778 -95
  214. package/dist/indexer/bundle-identity-guard.js +3 -2
  215. package/dist/indexer/db/graph-db.js +17 -30
  216. package/dist/indexer/ensure-index.js +11 -3
  217. package/dist/indexer/graph/graph-boost.js +9 -34
  218. package/dist/indexer/graph/graph-extraction.js +8 -5
  219. package/dist/indexer/index-writer-lock.js +53 -17
  220. package/dist/indexer/index-written-assets.js +33 -24
  221. package/dist/indexer/indexer.js +551 -253
  222. package/dist/indexer/installations.js +14 -96
  223. package/dist/indexer/passes/dir-staleness.js +16 -9
  224. package/dist/indexer/passes/memory-inference.js +15 -9
  225. package/dist/indexer/passes/metadata.js +113 -47
  226. package/dist/indexer/scan/doc-to-entry.js +38 -1
  227. package/dist/indexer/scan/drain-dir.js +13 -23
  228. package/dist/indexer/search/db-search.js +107 -69
  229. package/dist/indexer/search/fts-query.js +47 -24
  230. package/dist/indexer/search/ranking-contributors.js +42 -20
  231. package/dist/indexer/search/ranking.js +18 -99
  232. package/dist/indexer/search/search-fields.js +7 -2
  233. package/dist/indexer/search/search-source.js +82 -93
  234. package/dist/indexer/usage/usage-events.js +0 -89
  235. package/dist/indexer/walk/file-context.js +2 -1
  236. package/dist/indexer/walk/matchers.js +30 -43
  237. package/dist/indexer/walk/path-resolver.js +7 -2
  238. package/dist/indexer/walk/walker.js +38 -12
  239. package/dist/integrations/agent/builders.js +0 -6
  240. package/dist/integrations/agent/config.js +2 -2
  241. package/dist/integrations/agent/detect.js +49 -19
  242. package/dist/integrations/agent/engine-fallback.js +76 -0
  243. package/dist/integrations/agent/engine-resolution.js +24 -11
  244. package/dist/integrations/agent/model-aliases.js +1 -1
  245. package/dist/integrations/agent/profiles.js +23 -1
  246. package/dist/integrations/agent/prompts.js +12 -8
  247. package/dist/integrations/agent/runner-dispatch.js +4 -2
  248. package/dist/integrations/agent/runner.js +0 -1
  249. package/dist/integrations/agent/spawn.js +20 -93
  250. package/dist/integrations/github.js +1 -1
  251. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  252. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  253. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  254. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  255. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  256. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  257. package/dist/integrations/harnesses/index.js +3 -7
  258. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  259. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  260. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  261. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  262. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  263. package/dist/integrations/lockfile.js +132 -8
  264. package/dist/integrations/session-logs/index.js +3 -28
  265. package/dist/llm/client.js +136 -100
  266. package/dist/llm/embedders/remote.js +13 -5
  267. package/dist/llm/feature-gate.js +4 -12
  268. package/dist/llm/graph-extract.js +5 -11
  269. package/dist/llm/memory-infer.js +144 -1
  270. package/dist/llm/metadata-enhance.js +5 -7
  271. package/dist/llm/structured-call.js +1 -1
  272. package/dist/llm/usage-persist.js +26 -5
  273. package/dist/llm/usage-telemetry.js +25 -2
  274. package/dist/output/cli-hints.js +1 -2
  275. package/dist/output/context.js +22 -7
  276. package/dist/output/format-exempt.js +80 -0
  277. package/dist/output/generic-render.js +259 -0
  278. package/dist/output/render-registry.js +57 -0
  279. package/dist/output/renderers.js +14 -36
  280. package/dist/output/shapes/curate.js +10 -1
  281. package/dist/output/shapes/events.js +12 -7
  282. package/dist/output/shapes/helpers.js +56 -83
  283. package/dist/output/shapes/migrate.js +8 -0
  284. package/dist/output/shapes/passthrough.js +7 -41
  285. package/dist/output/shapes/proposal/producer.js +15 -7
  286. package/dist/output/shapes.js +2 -9
  287. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  288. package/dist/output/text/bundle-show.js +7 -0
  289. package/dist/output/text/command-format.js +164 -96
  290. package/dist/output/text/env.js +1 -3
  291. package/dist/output/text/events.js +8 -7
  292. package/dist/output/text/health-format.js +103 -0
  293. package/dist/output/text/health.js +7 -0
  294. package/dist/output/text/helpers.js +10 -8
  295. package/dist/output/text/lint-format.js +56 -0
  296. package/dist/{migrate-storage-node.mjs → output/text/lint.js} +2 -5
  297. package/dist/output/text/migrate.js +88 -0
  298. package/dist/output/text/proposal/producer.js +4 -2
  299. package/dist/output/text/proposal-format.js +44 -72
  300. package/dist/output/text/registry-commands.js +1 -2
  301. package/dist/output/text/show-directives.js +15 -7
  302. package/dist/output/text/status-list.js +32 -0
  303. package/dist/output/text/{save.js → sync.js} +2 -2
  304. package/dist/output/text/workflow-format.js +24 -203
  305. package/dist/output/text/workflow.js +1 -7
  306. package/dist/output/text.js +16 -17
  307. package/dist/registry/factory.js +4 -6
  308. package/dist/registry/origin-resolve.js +16 -27
  309. package/dist/registry/providers/skills-sh.js +3 -3
  310. package/dist/registry/providers/static-index.js +13 -23
  311. package/dist/registry/resolve.js +42 -7
  312. package/dist/registry/semver.js +34 -84
  313. package/dist/runtime.js +2 -23
  314. package/dist/scripts/akm-migrate-node.js +60972 -0
  315. package/dist/scripts/akm-migrate.js +60310 -0
  316. package/dist/setup/detect.js +42 -15
  317. package/dist/setup/registry-stash-loader.js +2 -2
  318. package/dist/setup/setup.js +236 -136
  319. package/dist/setup/steps/connection.js +7 -9
  320. package/dist/setup/steps/platforms.js +9 -9
  321. package/dist/setup/steps/semantic.js +15 -3
  322. package/dist/setup/steps/sources.js +12 -13
  323. package/dist/setup/steps/stashdir.js +2 -3
  324. package/dist/setup/steps/tasks.js +237 -120
  325. package/dist/sources/freshness.js +1 -1
  326. package/dist/sources/provider-factory.js +11 -17
  327. package/dist/sources/providers/filesystem.js +2 -3
  328. package/dist/sources/providers/git-install.js +278 -34
  329. package/dist/sources/providers/git-provider.js +25 -23
  330. package/dist/sources/providers/git-stash.js +414 -106
  331. package/dist/sources/providers/git.js +2 -2
  332. package/dist/sources/providers/npm.js +16 -19
  333. package/dist/sources/providers/provider-utils.js +7 -4
  334. package/dist/sources/providers/sync-from-ref.js +3 -9
  335. package/dist/sources/providers/website.js +6 -1
  336. package/dist/sources/resolve.js +6 -5
  337. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  338. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  339. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  340. package/dist/sources/snapshot-fetchers/github.js +100 -0
  341. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  342. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  343. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  344. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  345. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  346. package/dist/sources/snapshot-fetchers/website-ingest.js +686 -282
  347. package/dist/sources/snapshot-fetchers/x.js +910 -0
  348. package/dist/storage/database.js +13 -1
  349. package/dist/storage/engines/sqlite-migrations.js +23 -111
  350. package/dist/storage/managed-db.js +21 -2
  351. package/dist/storage/repositories/canaries-repository.js +1 -1
  352. package/dist/storage/repositories/events-repository.js +27 -11
  353. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  354. package/dist/storage/repositories/index-connection.js +44 -8
  355. package/dist/storage/repositories/index-entries-repository.js +157 -241
  356. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  357. package/dist/storage/repositories/index-fts-repository.js +5 -2
  358. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  359. package/dist/storage/repositories/index-meta-repository.js +2 -3
  360. package/dist/storage/repositories/index-schema.js +10 -25
  361. package/dist/storage/repositories/index-utility-repository.js +15 -28
  362. package/dist/storage/repositories/index-vec-repository.js +6 -1
  363. package/dist/storage/repositories/outcome-repository.js +119 -0
  364. package/dist/storage/repositories/proposals-repository.js +296 -59
  365. package/dist/storage/repositories/registry-cache.js +19 -0
  366. package/dist/storage/repositories/salience-repository.js +172 -0
  367. package/dist/storage/repositories/task-history-repository.js +15 -13
  368. package/dist/storage/repositories/workflow-runs-repository.js +118 -53
  369. package/dist/tasks/backends/cron.js +105 -15
  370. package/dist/tasks/backends/index.js +1 -1
  371. package/dist/tasks/backends/launchd.js +85 -38
  372. package/dist/tasks/backends/schtasks.js +135 -15
  373. package/dist/tasks/embedded.js +56 -40
  374. package/dist/tasks/log-redaction.js +156 -0
  375. package/dist/tasks/parser.js +87 -160
  376. package/dist/tasks/resolve-akm-bin.js +137 -59
  377. package/dist/tasks/runner.js +209 -44
  378. package/dist/tasks/scheduler-invocation.js +220 -10
  379. package/dist/tasks/schema.js +89 -1
  380. package/dist/tasks/task-id.js +1 -3
  381. package/dist/tasks/validator.js +20 -6
  382. package/dist/workflows/authoring/authoring.js +94 -143
  383. package/dist/workflows/authoring/scope-key.js +1 -1
  384. package/dist/workflows/concurrency-policy.js +95 -1
  385. package/dist/workflows/exec/dispatch-redaction.js +114 -0
  386. package/dist/workflows/exec/exec-unit.js +542 -0
  387. package/dist/workflows/exec/frozen-judge.js +130 -32
  388. package/dist/workflows/exec/native-executor.js +513 -274
  389. package/dist/workflows/exec/param-secrets.js +12 -11
  390. package/dist/workflows/exec/run-workflow.js +496 -237
  391. package/dist/workflows/exec/step-work.js +616 -412
  392. package/dist/workflows/exec/unit-dispatch.js +34 -0
  393. package/dist/workflows/exec/unit-writer.js +53 -13
  394. package/dist/workflows/exec/worktree.js +454 -41
  395. package/dist/workflows/ir/compile.js +166 -271
  396. package/dist/workflows/ir/freeze.js +119 -42
  397. package/dist/workflows/ir/params.js +135 -11
  398. package/dist/workflows/ir/plan-hash.js +1 -1
  399. package/dist/workflows/ir/schema.js +129 -45
  400. package/dist/workflows/parser.js +1070 -307
  401. package/dist/workflows/program/expressions.js +20 -208
  402. package/dist/workflows/program/schema.js +31 -10
  403. package/dist/workflows/renderer.js +123 -68
  404. package/dist/workflows/resource-limits.js +184 -0
  405. package/dist/workflows/runtime/checkin.js +3 -3
  406. package/dist/workflows/runtime/plan-classifier.js +16 -75
  407. package/dist/workflows/runtime/runs.js +331 -132
  408. package/dist/workflows/runtime/unit-checkin.js +1 -1
  409. package/dist/workflows/runtime/unit-phases.js +2 -2
  410. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  411. package/dist/workflows/schema.js +1 -11
  412. package/dist/workflows/validate-summary.js +47 -38
  413. package/dist/workflows/validator.js +21 -62
  414. package/docs/README.md +110 -0
  415. package/docs/migration/README.md +8 -0
  416. package/docs/migration/release-notes/0.7.0.md +11 -11
  417. package/docs/migration/release-notes/0.9.0.md +209 -27
  418. package/docs/migration/v0.7-to-v0.8.md +48 -48
  419. package/docs/migration/v0.8-to-v0.9.md +567 -209
  420. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  421. package/docs/reference/README.md +19 -0
  422. package/docs/reference/bundle-types.md +19 -0
  423. package/docs/reference/cli.md +2342 -0
  424. package/docs/reference/configuration.md +371 -0
  425. package/docs/reference/data-and-telemetry.md +129 -46
  426. package/docs/reference/supported-formats.md +50 -0
  427. package/docs/reference/workflow-schema.md +1014 -0
  428. package/docs/reference/workflows.md +51 -0
  429. package/package.json +31 -13
  430. package/schemas/akm-asset-envelope.json +93 -0
  431. package/schemas/akm-config.json +89 -128
  432. package/schemas/akm-task.json +27 -5
  433. package/schemas/akm-workflow.json +157 -81
  434. package/dist/assets/tasks/core/backup.yml +0 -5
  435. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  436. package/dist/cli/config-migrate.js +0 -1878
  437. package/dist/cli/config-validate.js +0 -41
  438. package/dist/commands/backup-cli.js +0 -56
  439. package/dist/commands/bundle/bundle-cli.js +0 -68
  440. package/dist/commands/bundle/bundle.js +0 -219
  441. package/dist/commands/graph/graph-cli.js +0 -124
  442. package/dist/commands/graph/graph.js +0 -489
  443. package/dist/commands/improve/extract-watch.js +0 -140
  444. package/dist/commands/mv-cli.js +0 -1221
  445. package/dist/commands/sources/history.js +0 -201
  446. package/dist/commands/tasks/default-tasks.js +0 -186
  447. package/dist/core/migration-backup.js +0 -1231
  448. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  449. package/dist/llm/memory-infer-impl.js +0 -138
  450. package/dist/migrate/legacy/config-source-migration.js +0 -257
  451. package/dist/migrate/legacy/content-migration.js +0 -350
  452. package/dist/migrate/legacy/legacy-layout.js +0 -779
  453. package/dist/migrate/legacy/legacy-paths.js +0 -25
  454. package/dist/migrate/legacy/legacy-stash-json.js +0 -80
  455. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  456. package/dist/migrate/legacy/task-target-ref-migration.js +0 -278
  457. package/dist/migrate/legacy/three-db-cutover.js +0 -845
  458. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  459. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  460. package/dist/migrate/legacy-ref-grammar.js +0 -214
  461. package/dist/output/shapes/distill.js +0 -14
  462. package/dist/output/shapes/history.js +0 -11
  463. package/dist/output/text/distill.js +0 -6
  464. package/dist/output/text/enable-disable.js +0 -8
  465. package/dist/output/text/history.js +0 -6
  466. package/dist/registry/build-index.js +0 -382
  467. package/dist/schemas/akm-config.json +0 -4704
  468. package/dist/schemas/akm-task.json +0 -87
  469. package/dist/schemas/akm-workflow.json +0 -372
  470. package/dist/scripts/migrate-storage.js +0 -3816
  471. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  472. package/dist/workflows/cli.js +0 -53
  473. package/dist/workflows/exec/brief.js +0 -481
  474. package/dist/workflows/exec/report.js +0 -1460
  475. package/dist/workflows/exec/watch.js +0 -116
  476. package/dist/workflows/program/parser.js +0 -813
  477. package/dist/workflows/program/project.js +0 -104
@@ -28,24 +28,12 @@ function refType(ref) {
28
28
  const body = ref.includes("//") ? ref.slice(ref.indexOf("//") + 2) : ref;
29
29
  // 0.9.0 conceptId `<stash-subdir>/<name>`: delegate to the D-R2 reverse table
30
30
  // so the leading stash subdir maps back to its asset type (the canonical path).
31
- const conceptType = typeNameFromConceptId(body)?.type;
32
- if (conceptType !== undefined)
33
- return conceptType;
34
- // DOCUMENTED EXCEPTION (ref-grammar decision D-R3 migration window): a tolerant
35
- // legacy `type:name` arm survives ONLY because live callers still hand this
36
- // recognition-only seam the old spelling — `propose.ts` builds
37
- // `${options.type}:${options.name}`, and a pre-migration stored ref may still
38
- // reach here before the 0.10.0 grammar removal. It never crosses a storage
39
- // boundary, so it stays until those feeders flip.
40
- const colon = body.indexOf(":");
41
- if (colon > 0)
42
- return body.slice(0, colon).trim() || undefined;
43
- return undefined;
31
+ return typeNameFromConceptId(body)?.type;
44
32
  }
45
33
  /**
46
34
  * Resolve the standards context for a write target identified by its asset ref.
47
35
  *
48
- * @param ref Canonical asset ref of the write target (e.g. `skill:foo`).
36
+ * @param ref Canonical asset ref of the write target (e.g. `skills/foo`).
49
37
  * When undefined, the target is a general authoring flow.
50
38
  * @param stashRoot Stash root directory.
51
39
  */
@@ -53,14 +53,14 @@ function collectMarkdownFiles(dir) {
53
53
  return results;
54
54
  }
55
55
  /**
56
- * Derive a fact ref (`fact:conventions/naming`) from an absolute markdown path
56
+ * Derive a fact ref (`facts/conventions/naming`) from an absolute markdown path
57
57
  * relative to the facts root. Mirrors the canonical-name derivation in
58
58
  * `asset-spec.ts` (POSIX separators, `.md` stripped).
59
59
  */
60
60
  function toFactRef(factsRoot, absPath) {
61
61
  const rel = path.relative(factsRoot, absPath).split(path.sep).join("/");
62
62
  const name = rel.endsWith(".md") ? rel.slice(0, -3) : rel;
63
- return `fact:${name}`;
63
+ return `${FACTS_SUBDIR}/${name}`;
64
64
  }
65
65
  export function resolveStashStandards(stashRoot) {
66
66
  const factsRoot = path.join(stashRoot, FACTS_SUBDIR);
@@ -28,9 +28,9 @@ import { placementTypes } from "../asset/asset-placement.js";
28
28
  import { parseFrontmatter } from "../asset/frontmatter.js";
29
29
  /** Sub-path (under the stash root) for per-type SOFT convention facts. */
30
30
  export const TYPE_CONVENTIONS_SUBDIR = path.join("facts", "conventions", "assets");
31
- /** The `fact:` ref prefix for a per-type convention, e.g. `fact:conventions/assets/skill`. */
31
+ /** The slash conceptId for a per-type convention, e.g. `facts/conventions/assets/skill`. */
32
32
  export function typeConventionRef(type) {
33
- return `fact:conventions/assets/${type}`;
33
+ return `facts/conventions/assets/${type}`;
34
34
  }
35
35
  /**
36
36
  * Read the SOFT authoring-convention body for asset type `type`, if a stash
@@ -1,7 +1,7 @@
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
- import { runMigrations as runSqliteMigrations } from "../../storage/engines/sqlite-migrations.js";
4
+ import { assertMigrationRegistry, runMigrations as runSqliteMigrations, } from "../../storage/engines/sqlite-migrations.js";
5
5
  export const STATE_MIGRATIONS = [
6
6
  // ── Migration 001 — initial schema ──────────────────────────────────────────
7
7
  {
@@ -59,7 +59,7 @@ export const STATE_MIGRATIONS = [
59
59
  -- id TEXT PK — UUID (crypto.randomUUID()); stable directory name.
60
60
  -- stash_dir TEXT — absolute stash root; multi-stash installs need
61
61
  -- this to partition proposal lists per stash.
62
- -- ref TEXT — target asset ref (e.g. "lesson:alpha");
62
+ -- ref TEXT — target asset ref (e.g. "lessons/alpha");
63
63
  -- indexed for ref-scoped queue views.
64
64
  -- status TEXT — "pending" | "accepted" | "rejected"; indexed
65
65
  -- so pending-queue queries are fast.
@@ -76,7 +76,7 @@ export const STATE_MIGRATIONS = [
76
76
  -- metadata_json TEXT — JSON object for future proposal fields.
77
77
  -- Current fields stored here: sourceRun,
78
78
  -- review, confidence, gateDecision (#577),
79
- -- backupContent, eligibilitySource.
79
+ -- backupContent.
80
80
  --
81
81
  -- ADD COLUMN extension points (future migrations):
82
82
  -- ALTER TABLE proposals ADD COLUMN source_run TEXT DEFAULT NULL;
@@ -350,11 +350,12 @@ export const STATE_MIGRATIONS = [
350
350
  //
351
351
  // VESTIGIAL as of the Chunk-8 fold: the legacy `proposal.json` import moved
352
352
  // OUT of the live per-operation path and INTO the one-time migrator
353
- // (`src/migrate/legacy/proposal-fs-import.ts`, wired through `akm migrate apply`),
353
+ // (`scripts/akm-migrate/migrate/legacy/proposal-fs-import.ts`, wired through
354
+ // `akm-migrate apply`),
354
355
  // whose idempotency is INSERT OR IGNORE on the proposal UUID plus migrate-apply's
355
- // own journal — it no longer reads or writes this ledger. The CREATE TABLE
356
- // stays because the migration registry is APPEND-ONLY and checksum-sealed:
357
- // removing a released fragment would make the schema_migrations ledger of an
356
+ // incomplete sentinel — it no longer reads or writes this ledger. The CREATE TABLE
357
+ // stays because migration IDs are append-only: removing a released migration
358
+ // would make the schema_migrations ledger of an
358
359
  // already-migrated rc database stop being an exact ordered prefix, and the
359
360
  // runner would refuse to open it. The empty table is harmless.
360
361
  //
@@ -832,17 +833,17 @@ export const STATE_MIGRATIONS = [
832
833
  //
833
834
  // The state.db half of the three-DB merge (plan §3.2/§8, normative §11.4,
834
835
  // chunk-8 cutover design §1). This migration is PURE,
835
- // SEALABLE, IDEMPOTENT DDL ONLY — `CREATE TABLE IF NOT EXISTS` (+ indexes),
836
+ // IDEMPOTENT DDL ONLY — `CREATE TABLE IF NOT EXISTS` (+ indexes),
836
837
  // never a DROP or a data move. The actual data movement (the workflow.db merge,
837
838
  // the usage_events rescue from index.db, the full old-ref→item_ref re-key, and
838
- // the workflow.db delete / index.db quarantine) is CODE — a journaled step of
839
- // the migrate-apply flow (`src/cli/config-migrate.ts` `cutover-applied` phase),
840
- // driven by `src/migrate/legacy/three-db-cutover.ts`. See the no-DROP contract
841
- // carve-out note in `src/core/state-db.ts`.
839
+ // the workflow.db delete / index.db quarantine) is idempotent migrate-apply
840
+ // code driven by
841
+ // `scripts/akm-migrate/migrate/legacy/three-db-cutover.ts`. See the no-DROP
842
+ // contract carve-out note in `src/core/state-db.ts`.
842
843
  //
843
844
  // The three workflow tables are the 10 pre-cutover workflow migrations
844
- // (the frozen `src/migrate/legacy/workflow-migrations-bodies.ts` base schema +
845
- // 001–010) folded into one baseline at their FINAL post-010 shape — column
845
+ // (the frozen `scripts/akm-migrate/migrate/legacy/workflow-migrations-bodies.ts`
846
+ // base schema + 001–010) folded into one baseline at their FINAL post-010 shape — column
846
847
  // lists, CHECK constraints, and indexes copied verbatim. `usage_events` mirrors
847
848
  // index.db's former `ensureUsageEventsSchema` (`src/indexer/usage/usage-events.ts`),
848
849
  // its new durable home. `legacy_state` mirrors `ensureLegacyStateTable`
@@ -958,7 +959,32 @@ export const STATE_MIGRATIONS = [
958
959
  );
959
960
  `,
960
961
  },
962
+ // ── Migration 021 — asset_salience / asset_outcome: missing_since (#733) ────
963
+ //
964
+ // Orphan-GC grace clock (Workstream C / #733,
965
+ // docs/architecture/specs/0.9.0-close-out-plan.md). Neither table carries a
966
+ // column that records when a row's ref STOPPED resolving against
967
+ // `entries.item_ref` — `updated_at` is bumped by improve itself on every
968
+ // touch, so it cannot double as a "went missing" timestamp.
969
+ //
970
+ // `missing_since` is stamped by `runOrphanStateGcPass`
971
+ // (src/commands/improve/loop-stages.ts) the first time a row's ref fails to
972
+ // resolve against the live index, cleared the moment the ref resolves
973
+ // again, and used to gate deletion once `STATE_GC_GRACE_MS` (7 days) has
974
+ // elapsed — and ONLY when `improve.stateGc.collect` is true (default
975
+ // false). Mirrors the additive ADD-COLUMN style of migration 011.
976
+ //
977
+ // NULL = never missing (the steady-state default for every existing row —
978
+ // both a fresh row and a pre-migration row read back NULL here).
979
+ {
980
+ id: "021-asset-state-missing-since",
981
+ up: `
982
+ ALTER TABLE asset_salience ADD COLUMN missing_since INTEGER DEFAULT NULL;
983
+ ALTER TABLE asset_outcome ADD COLUMN missing_since INTEGER DEFAULT NULL;
984
+ `,
985
+ },
961
986
  ];
987
+ assertMigrationRegistry(STATE_MIGRATIONS);
962
988
  /**
963
989
  * Apply every pending migration in a single transaction per migration.
964
990
  *
@@ -968,8 +994,5 @@ export const STATE_MIGRATIONS = [
968
994
  * Called automatically by `openStateDatabase()`.
969
995
  */
970
996
  export function runMigrations(db, options) {
971
- runSqliteMigrations(db, STATE_MIGRATIONS, {
972
- applyPending: options?.applyPending,
973
- generationMarker: options?.generationMarker,
974
- });
997
+ runSqliteMigrations(db, STATE_MIGRATIONS, { applyPending: options?.applyPending });
975
998
  }
@@ -0,0 +1,134 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Ambient, run-scoped state.db connection reuse.
6
+ *
7
+ * `openStateDatabase` is NOT cheap: every call registers a `state-db`
8
+ * maintenance activity (a lockfile create under the maintenance barrier), opens
9
+ * a throwaway read-only preflight handle to assert the migration ledger, then
10
+ * opens the real handle and applies pragmas. Paying that per repository call
11
+ * — twice per dispatched workflow unit (insert + finish), plus once per
12
+ * `appendEvent` (two events per unit) — is the dominant cost of a wide `map`
13
+ * fan-out.
14
+ *
15
+ * This module adds the ONE thing the codebase was missing: a way to say "for
16
+ * the duration of this async operation, everything that talks to state.db
17
+ * shares a single handle". It deliberately does NOT introduce a pool, a cache
18
+ * with an eviction policy, or a background keep-alive timer — those all leak
19
+ * handles across the test harness's per-test data-dir swaps. The scope owns
20
+ * exactly one connection and closes it in a `finally`.
21
+ *
22
+ * ## Why this is safe
23
+ *
24
+ * SQLite connections must not be shared across THREADS. This is a single
25
+ * process running a single-threaded JS event loop: `bun:sqlite` statement
26
+ * execution and `withImmediateTransaction`'s `BEGIN IMMEDIATE … COMMIT` bodies
27
+ * are fully synchronous, so two logically concurrent units can never interleave
28
+ * statements on the shared handle — the runtime cannot preempt a synchronous
29
+ * transaction body. Sharing one handle therefore REMOVES in-process writer
30
+ * contention (`SQLITE_BUSY` against ourselves) rather than creating it.
31
+ * Cross-process behaviour is untouched: WAL mode, the 30 s `busy_timeout` and
32
+ * the run-lease protocol all still arbitrate between processes exactly as
33
+ * before.
34
+ *
35
+ * The one hazard of an {@link AsyncLocalStorage}-carried handle is async work
36
+ * that ESCAPES the scope: the context propagates into a promise that settles
37
+ * after the scope's `finally` closed the handle. {@link borrowScopedStateDb}
38
+ * guards that with a `closed` flag — once a scope is torn down it stops
39
+ * lending, and escapee callers transparently fall back to opening their own
40
+ * connection (the pre-existing behaviour). A use-after-close is structurally
41
+ * impossible.
42
+ */
43
+ import { AsyncLocalStorage } from "node:async_hooks";
44
+ import path from "node:path";
45
+ import { getStateDbPath, openStateDatabase } from "./state-db.js";
46
+ const scopeStorage = new AsyncLocalStorage();
47
+ /** Every scope with a live handle, so the exit backstop can close them all. */
48
+ const liveScopes = new Set();
49
+ let exitHookInstalled = false;
50
+ function installExitBackstop() {
51
+ if (exitHookInstalled)
52
+ return;
53
+ exitHookInstalled = true;
54
+ // Idempotent, synchronous backstop: a process that exits mid-scope (a
55
+ // `process.exit()` from a command, an uncaught fatal) still releases the
56
+ // handle and its maintenance-activity lockfile. Normal teardown happens in
57
+ // the scope's own `finally`, which removes the scope from `liveScopes`
58
+ // first, so this never double-closes.
59
+ process.on("exit", closeAllStateDbScopes);
60
+ }
61
+ /**
62
+ * Close every scope-owned handle. Idempotent and safe to call at any time: a
63
+ * closed scope stops lending, so in-flight borrowers fall back to their own
64
+ * connections instead of using a dead handle. Module-private: the per-scope
65
+ * `finally` plus the process-exit backstop above are what guarantee no handle
66
+ * leak — export this the day a caller actually wires deterministic teardown.
67
+ */
68
+ function closeAllStateDbScopes() {
69
+ for (const scope of [...liveScopes])
70
+ closeScope(scope);
71
+ }
72
+ function closeScope(scope) {
73
+ scope.closed = true;
74
+ liveScopes.delete(scope);
75
+ const db = scope.db;
76
+ scope.db = undefined;
77
+ if (!db)
78
+ return;
79
+ try {
80
+ db.close();
81
+ }
82
+ catch {
83
+ // A close failure must never mask the caller's own error (or wedge exit).
84
+ }
85
+ }
86
+ /**
87
+ * The ambient scoped handle for `dbPath` (default: the canonical state.db), or
88
+ * `undefined` when there is no live scope for that exact path.
89
+ *
90
+ * The path comparison matters: the test harness repoints `AKM_DATA_DIR` between
91
+ * tests, and a scope entered against one data dir must never lend its handle to
92
+ * a caller resolving a different one.
93
+ */
94
+ export function borrowScopedStateDb(dbPath) {
95
+ const scope = scopeStorage.getStore();
96
+ if (!scope || scope.closed)
97
+ return undefined;
98
+ if (path.resolve(dbPath ?? getStateDbPath()) !== scope.dbPath)
99
+ return undefined;
100
+ scope.db ??= openStateDatabase(scope.dbPath);
101
+ liveScopes.add(scope);
102
+ return scope.db;
103
+ }
104
+ /**
105
+ * Run `fn` with one shared state.db connection ambient for its whole async
106
+ * extent. The handle is opened on FIRST borrow (never eagerly) and closed once
107
+ * `fn` settles.
108
+ *
109
+ * Nesting is a no-op join: an inner scope for the same path reuses the outer
110
+ * scope's handle and does not close it, so a caller can enter a scope without
111
+ * knowing whether an outer frame already did.
112
+ */
113
+ export async function withStateDbScope(fn, opts) {
114
+ const dbPath = path.resolve(opts?.path ?? getStateDbPath());
115
+ const outer = scopeStorage.getStore();
116
+ if (outer && !outer.closed && outer.dbPath === dbPath)
117
+ return fn();
118
+ const scope = { dbPath, db: undefined, closed: false };
119
+ installExitBackstop();
120
+ try {
121
+ return await scopeStorage.run(scope, fn);
122
+ }
123
+ finally {
124
+ closeScope(scope);
125
+ }
126
+ }
127
+ /** Test seam: how many scope-owned handles are currently open. Must return to 0. */
128
+ export function openScopedStateDbCount() {
129
+ let count = 0;
130
+ for (const scope of liveScopes)
131
+ if (scope.db)
132
+ count++;
133
+ return count;
134
+ }
@@ -45,11 +45,10 @@
45
45
  * merge-target tables at their final shape. The one-time, filesystem-derived,
46
46
  * fail-closed DATA movement (the workflow.db merge, the usage_events rescue, the
47
47
  * full old-ref→item_ref re-key, and the workflow.db unlink / index.db quarantine
48
- * rename) is deliberately NOT a sealed SQL migration body: it is a journaled step
49
- * of the migrate-apply coordinator (`src/cli/config-migrate.ts` `cutover-applied`
50
- * phase → `src/migrate/legacy/three-db-cutover.ts`). So the no-DROP contract here
51
- * is intact — the physical workflow.db deletion happens outside the ledger DDL,
52
- * under the backup-verified-restorable fail-closed gate.
48
+ * rename) runs as idempotent code in the migrate-apply coordinator
49
+ * (`scripts/akm-migrate/migrate/legacy/three-db-cutover.ts`). So the no-DROP
50
+ * contract here is intact: physical workflow.db deletion happens outside the
51
+ * ledger DDL, after a verified backup and committed data transaction.
53
52
  *
54
53
  * ## Schema design: indexed columns vs. metadata_json
55
54
  *
@@ -73,7 +72,7 @@ import fs from "node:fs";
73
72
  import path from "node:path";
74
73
  import { openDatabase } from "../storage/database.js";
75
74
  import { assertCurrentMigrationLedger, assertMigrationLedger } from "../storage/engines/sqlite-migrations.js";
76
- import { openManagedDatabase, withManagedDb, withManagedDbAsync } from "../storage/managed-db.js";
75
+ import { openManagedDatabase, withManagedDb } from "../storage/managed-db.js";
77
76
  import { acquireMaintenanceActivitySync } from "./maintenance-barrier.js";
78
77
  import { assertNoPendingMigrationOperation } from "./migration-operation.js";
79
78
  import { getDataDir } from "./paths.js";
@@ -182,14 +181,6 @@ export function openStateDatabase(dbPath) {
182
181
  export function withStateDb(fn, opts) {
183
182
  return withManagedDb(() => openStateDatabase(opts?.path), fn, opts);
184
183
  }
185
- /**
186
- * Async sibling of {@link withStateDb} — for `fn`s that hold the handle across
187
- * an `await` (the sync version would close it too early). Same borrow/own +
188
- * optional `path` override semantics.
189
- */
190
- export function withStateDbAsync(fn, opts) {
191
- return withManagedDbAsync(() => openStateDatabase(opts?.path), fn, opts);
192
- }
193
184
  /**
194
185
  * Fire-and-forget telemetry write to state.db (Chunk-8 WI-8.3: usage_events'
195
186
  * durable home). Skips entirely when state.db does not exist yet (never
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * Layering: this file lives in `core/` — NOT `llm/` — because the agent path
17
17
  * must be able to reach it and `agent/ ⇏ llm/` is an enforced invariant
18
- * (`tests/architecture/agent-no-llm-sdk-guard.test.ts`). The transport is
18
+ * (`tests/integration/architecture/agent-no-llm-sdk-guard.test.ts`). The transport is
19
19
  * always injected; this module never performs IO of its own.
20
20
  *
21
21
  * Retry semantics: only parse/validation misses are retried (with corrective
@@ -12,8 +12,10 @@
12
12
  * kill reaps the whole descendant tree — no orphaned children.
13
13
  * • A SIGTERM→SIGKILL kill ladder on timeout/abort — a child that ignores
14
14
  * SIGTERM is force-killed after a grace period instead of wedging forever.
15
- * • Bounded output capture ({@link readStream}) that cannot block past the
16
- * wall budget even when the child leaves a pipe endpoint open.
15
+ * • Time-bounded output capture ({@link readStream}) that cannot block past
16
+ * the wall budget even when the child leaves a pipe endpoint open, plus an
17
+ * OPT-IN RETENTION cap (`maxOutputBytes`); see {@link readStream} for what
18
+ * the cap does and does not bound.
17
19
  * • Injectable `spawnFn`/`setTimeoutFn`/`clearTimeoutFn` seams so callers
18
20
  * can drive the machinery deterministically in tests.
19
21
  *
@@ -70,33 +72,115 @@ export function scheduleKillLadder(proc, opts) {
70
72
  if (typeof sigkillTimer !== "number")
71
73
  sigkillTimer.unref?.();
72
74
  }
75
+ /**
76
+ * The joined, human-readable reason ONE managed run's capture is incomplete, or
77
+ * `undefined` when both pipes drained cleanly.
78
+ *
79
+ * Shared so every caller that promotes captured output treats an incomplete
80
+ * capture the same way. A pipe that errored, or that hit the stream-drain
81
+ * timeout because a background descendant kept the fd open after the leader
82
+ * exited 0, yields a PARTIAL string — and a caller that reads only `exitCode`
83
+ * would promote that partial as if it were the command's whole output.
84
+ * `maxBytes` overflow is deliberately NOT reported here: it is a distinct,
85
+ * caller-classified condition (see {@link StreamReadResult.overflowed}), not a
86
+ * drain malfunction.
87
+ */
88
+ export function streamCaptureFailure(stdout, stderr) {
89
+ const failures = [];
90
+ if (stdout.error)
91
+ failures.push(`stdout read failed: ${errorText(stdout.error)}`);
92
+ if (stderr.error)
93
+ failures.push(`stderr read failed: ${errorText(stderr.error)}`);
94
+ if (stdout.timedOut)
95
+ failures.push("stdout drain timed out");
96
+ if (stderr.timedOut)
97
+ failures.push("stderr drain timed out");
98
+ return failures.length === 0 ? undefined : failures.join("; ");
99
+ }
100
+ function errorText(error) {
101
+ return error instanceof Error ? error.message : String(error);
102
+ }
73
103
  const STREAM_READ_TIMEOUT = Symbol("stream-read-timeout");
104
+ /**
105
+ * Index at or before `limit` where a UTF-8 CHARACTER starts, so a retention cut
106
+ * never lands inside a multi-byte sequence. Continuation bytes are `10xxxxxx`;
107
+ * a well-formed sequence has at most three of them, so this walks back at most
108
+ * three positions.
109
+ */
110
+ function utf8BoundaryAtOrBefore(value, limit) {
111
+ let cut = limit;
112
+ while (cut > 0 && (value[cut] & 0xc0) === 0x80)
113
+ cut--;
114
+ return cut;
115
+ }
74
116
  /**
75
117
  * Drain a readable stream to text, optionally racing each read against a
76
118
  * timeout so a process that is killed via SIGTERM/SIGKILL but whose pipe
77
119
  * endpoints stay open (e.g. background threads still holding the fd) cannot
78
120
  * block the caller indefinitely. On timeout we return whatever was decoded
79
121
  * before the pipe stopped draining.
122
+ *
123
+ * ## The `maxBytes` cap bounds MEMORY, not the child
124
+ *
125
+ * The drain ALWAYS runs to the end of the stream. `maxBytes` caps only what is
126
+ * RETAINED: once the cap is reached the loop keeps calling `reader.read()` and
127
+ * throws the bytes away. That is what makes the cap safe to impose on a
128
+ * process that is still running — a reader that stopped pulling would fill the
129
+ * pipe buffer and BLOCK the child on its next write, turning "the output got
130
+ * long" into "the command hangs until the wall timeout". Draining and
131
+ * discarding costs nothing but the reads, and the child finishes normally.
80
132
  */
81
133
  export async function readStream(stream, opts) {
82
134
  if (!stream)
83
- return { text: "", timedOut: false };
135
+ return { text: "", timedOut: false, overflowed: false, bytesRead: 0, retainedBytes: 0 };
84
136
  const reader = stream.getReader();
85
137
  const decoder = new TextDecoder();
138
+ const maxBytes = opts?.maxBytes;
86
139
  let text = "";
140
+ let bytesRead = 0;
141
+ let retainedBytes = 0;
142
+ let overflowed = false;
143
+ /**
144
+ * Common per-chunk accumulate. Uncapped it appends everything. Capped, it
145
+ * appends until the cap and then only COUNTS — the caller's loop keeps reading
146
+ * either way, which is the whole point.
147
+ */
148
+ const absorb = (value) => {
149
+ bytesRead += value.byteLength;
150
+ if (maxBytes === undefined) {
151
+ retainedBytes += value.byteLength;
152
+ text += decoder.decode(value, { stream: true });
153
+ return;
154
+ }
155
+ // Already past the cap: discard. Never resume retaining — a gap in the
156
+ // middle would splice two disjoint regions into one string.
157
+ if (overflowed)
158
+ return;
159
+ if (retainedBytes + value.byteLength <= maxBytes) {
160
+ retainedBytes += value.byteLength;
161
+ text += decoder.decode(value, { stream: true });
162
+ return;
163
+ }
164
+ overflowed = true;
165
+ const keep = utf8BoundaryAtOrBefore(value, maxBytes - retainedBytes);
166
+ if (keep > 0) {
167
+ retainedBytes += keep;
168
+ text += decoder.decode(value.subarray(0, keep), { stream: true });
169
+ }
170
+ };
87
171
  if (!opts?.timeoutMs) {
88
172
  try {
89
173
  while (true) {
90
174
  const chunk = await reader.read();
91
175
  if (chunk.done)
92
176
  break;
93
- text += decoder.decode(chunk.value, { stream: true });
177
+ absorb(chunk.value);
94
178
  }
95
179
  text += decoder.decode();
96
- return { text, timedOut: false };
180
+ return { text, timedOut: false, overflowed, bytesRead, retainedBytes };
97
181
  }
98
182
  catch (error) {
99
- return { text, timedOut: false, error };
183
+ return { text, timedOut: false, error, overflowed, bytesRead, retainedBytes };
100
184
  }
101
185
  finally {
102
186
  try {
@@ -109,33 +193,46 @@ export async function readStream(stream, opts) {
109
193
  }
110
194
  const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
111
195
  const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
196
+ const timeoutMs = opts.timeoutMs;
112
197
  let timer;
198
+ let drained = false;
113
199
  const timeoutPromise = new Promise((resolve) => {
114
- timer = setTimeoutImpl(() => {
115
- timer = undefined;
116
- resolve(STREAM_READ_TIMEOUT);
117
- }, opts.timeoutMs);
118
- if (typeof timer !== "number")
119
- timer.unref?.();
200
+ const arm = () => {
201
+ // The drain already finished while the deadline was still waiting to be
202
+ // armed; a timer started now would belong to nobody.
203
+ if (drained)
204
+ return;
205
+ timer = setTimeoutImpl(() => {
206
+ timer = undefined;
207
+ resolve(STREAM_READ_TIMEOUT);
208
+ }, timeoutMs);
209
+ if (typeof timer !== "number")
210
+ timer.unref?.();
211
+ };
212
+ if (opts.armTimeoutAfter)
213
+ void opts.armTimeoutAfter.then(arm, arm);
214
+ else
215
+ arm();
120
216
  });
121
217
  try {
122
218
  while (true) {
123
219
  const chunk = await Promise.race([reader.read(), timeoutPromise]);
124
220
  if (chunk === STREAM_READ_TIMEOUT) {
125
221
  void reader.cancel().catch(() => { });
126
- return { text, timedOut: true };
222
+ return { text, timedOut: true, overflowed, bytesRead, retainedBytes };
127
223
  }
128
224
  if (chunk.done)
129
225
  break;
130
- text += decoder.decode(chunk.value, { stream: true });
226
+ absorb(chunk.value);
131
227
  }
132
228
  text += decoder.decode();
133
- return { text, timedOut: false };
229
+ return { text, timedOut: false, overflowed, bytesRead, retainedBytes };
134
230
  }
135
231
  catch (error) {
136
- return { text, timedOut: false, error };
232
+ return { text, timedOut: false, error, overflowed, bytesRead, retainedBytes };
137
233
  }
138
234
  finally {
235
+ drained = true;
139
236
  if (timer !== undefined) {
140
237
  clearTimeoutImpl(timer);
141
238
  }
@@ -147,7 +244,29 @@ export async function readStream(stream, opts) {
147
244
  }
148
245
  }
149
246
  }
150
- const EMPTY_READ = { text: "", timedOut: false };
247
+ const EMPTY_READ = {
248
+ text: "",
249
+ timedOut: false,
250
+ overflowed: false,
251
+ bytesRead: 0,
252
+ retainedBytes: 0,
253
+ };
254
+ /**
255
+ * Drain deadline for a run with no wall budget, counted from the child's EXIT
256
+ * (never from capture — see {@link runManagedSubprocess}). It bounds only the
257
+ * window in which a descendant can keep an inherited pipe open after the leader
258
+ * is gone, so it can be generous without ever capping the run itself.
259
+ */
260
+ const UNBOUNDED_STREAM_READ_SAFETY_MS = 60 * 60 * 1000;
261
+ /**
262
+ * Drain deadline for a run WITH a wall budget, counted from the moment the
263
+ * pipe's owner left — the child exited, or (a child that survived its own kill
264
+ * ladder) the budget expired. Anything still holding the fd open then is a
265
+ * background descendant, not the command, so this is the same 2 s the old
266
+ * capture-anchored deadline allowed past the budget; only its starting point
267
+ * moved earlier.
268
+ */
269
+ const POST_EXIT_STREAM_DRAIN_GRACE_MS = 2_000;
151
270
  function toError(err) {
152
271
  return err instanceof Error ? err : new Error(String(err));
153
272
  }
@@ -206,8 +325,18 @@ export async function runManagedSubprocess(cmd, opts) {
206
325
  // Skipped entirely when timeoutMs is null.
207
326
  let timedOut = false;
208
327
  let timer;
328
+ // Settles when a bounded run's wall budget expires — the second way (after
329
+ // the child's own exit) a captured pipe can stop belonging to a live command.
330
+ // See the drain-deadline note below.
331
+ let onBudgetExpiry;
332
+ const budgetExpired = capture && timeoutMs !== null
333
+ ? new Promise((resolve) => {
334
+ onBudgetExpiry = resolve;
335
+ })
336
+ : undefined;
209
337
  if (timeoutMs !== null) {
210
338
  timer = setTimeoutImpl(() => {
339
+ onBudgetExpiry?.();
211
340
  scheduleKillLadder(proc, {
212
341
  onKill: () => {
213
342
  timedOut = true;
@@ -238,24 +367,33 @@ export async function runManagedSubprocess(cmd, opts) {
238
367
  else
239
368
  abortSignal.addEventListener("abort", onAbort, { once: true });
240
369
  }
241
- // Stream-drain timeout: the wall budget plus a 2 s grace, or 30 s when there
242
- // is no kill timer. Ensures the caller never hangs past the budget even if a
243
- // killed child leaves a pipe write-end open in a background thread.
244
- const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : 30_000;
245
- const stdoutPromise = capture
246
- ? readStream(proc.stdout ?? null, {
247
- timeoutMs: streamDrainTimeoutMs,
248
- setTimeoutFn: setTimeoutImpl,
249
- clearTimeoutFn: clearTimeoutImpl,
250
- })
251
- : Promise.resolve(EMPTY_READ);
252
- const stderrPromise = capture
253
- ? readStream(proc.stderr ?? null, {
254
- timeoutMs: streamDrainTimeoutMs,
255
- setTimeoutFn: setTimeoutImpl,
256
- clearTimeoutFn: clearTimeoutImpl,
257
- })
258
- : Promise.resolve(EMPTY_READ);
370
+ // Stream-drain timeout: a short grace once nothing living owns the pipe any
371
+ // more, or a one-hour safety bound when execution itself is unbounded.
372
+ //
373
+ // Neither is armed while the command is still running: every byte up to then
374
+ // is a live process's, and cancelling the reader would discard the output of
375
+ // work that is still going. What ends that ownership is where the two cases
376
+ // differ. Unbounded, the caller asked for NO cap on the run, so only the
377
+ // child's own exit can end it. Bounded, the wall budget ends it too — a child
378
+ // that outlived its own kill ladder is not something akm can keep waiting on
379
+ // — which leaves the SAME budget + grace ceiling as before for a command that
380
+ // really did spend its whole budget, and cuts the common case that used to
381
+ // stall: a leader exiting in milliseconds no longer holds the drain (and with
382
+ // it the unit, and with it a fan-out slot) open for a budget it never came
383
+ // close to spending.
384
+ const streamDrainTimeoutMs = timeoutMs !== null ? POST_EXIT_STREAM_DRAIN_GRACE_MS : UNBOUNDED_STREAM_READ_SAFETY_MS;
385
+ // Settles on a rejected `proc.exited` too: either way the child is gone.
386
+ const childSettled = capture ? proc.exited.catch(() => undefined) : undefined;
387
+ const pipeOwnerGone = childSettled && budgetExpired ? Promise.race([childSettled, budgetExpired]) : childSettled;
388
+ const readOpts = {
389
+ timeoutMs: streamDrainTimeoutMs,
390
+ setTimeoutFn: setTimeoutImpl,
391
+ clearTimeoutFn: clearTimeoutImpl,
392
+ ...(pipeOwnerGone ? { armTimeoutAfter: pipeOwnerGone } : {}),
393
+ ...(opts.maxOutputBytes !== undefined ? { maxBytes: opts.maxOutputBytes } : {}),
394
+ };
395
+ const stdoutPromise = capture ? readStream(proc.stdout ?? null, readOpts) : Promise.resolve(EMPTY_READ);
396
+ const stderrPromise = capture ? readStream(proc.stderr ?? null, readOpts) : Promise.resolve(EMPTY_READ);
259
397
  // Optional stdin payload (captured mode only). Race the write/close against
260
398
  // proc.exited so a child that never drains stdin cannot pin us past the
261
399
  // timeout.
@@ -299,5 +437,13 @@ export async function runManagedSubprocess(cmd, opts) {
299
437
  clearTimeoutImpl(timer);
300
438
  abortSignal?.removeEventListener("abort", onAbort);
301
439
  const [stdoutRead, stderrRead] = await Promise.all([stdoutPromise, stderrPromise]);
302
- return { exitCode, stdout: stdoutRead.text, stderr: stderrRead.text, timedOut, aborted, stdoutRead, stderrRead };
440
+ return {
441
+ exitCode,
442
+ stdout: stdoutRead.text,
443
+ stderr: stderrRead.text,
444
+ timedOut,
445
+ aborted,
446
+ stdoutRead,
447
+ stderrRead,
448
+ };
303
449
  }