akm-cli 0.9.0-rc.8 → 0.9.0

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 (450) hide show
  1. package/CHANGELOG.md +1063 -44
  2. package/README.md +51 -25
  3. package/SECURITY.md +14 -1
  4. package/STABILITY.md +497 -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 +73 -88
  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 +141 -87
  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/md-report.js +5 -9
  57. package/dist/commands/health/metrics.js +62 -20
  58. package/dist/commands/health/renderers.js +47 -0
  59. package/dist/commands/health/report-view-model.js +4 -5
  60. package/dist/commands/health/stash-exposure.js +1 -1
  61. package/dist/commands/health/surfaces.js +3 -48
  62. package/dist/commands/health/task-runs.js +3 -67
  63. package/dist/commands/health/types-improve.js +7 -0
  64. package/dist/commands/health.js +99 -28
  65. package/dist/commands/improve/anti-collapse.js +2 -2
  66. package/dist/commands/improve/autonomy-gate.js +68 -0
  67. package/dist/commands/improve/collapse-detector.js +41 -40
  68. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  69. package/dist/commands/improve/consolidate/merge.js +4 -0
  70. package/dist/commands/improve/consolidate.js +140 -1000
  71. package/dist/commands/improve/distill/promote-memory.js +12 -12
  72. package/dist/commands/improve/distill/quality-gate.js +6 -6
  73. package/dist/commands/improve/distill.js +58 -69
  74. package/dist/commands/improve/eligibility.js +105 -57
  75. package/dist/commands/improve/extract-cli.js +14 -133
  76. package/dist/commands/improve/improve-cli.js +98 -114
  77. package/dist/commands/improve/improve-result-file.js +1 -28
  78. package/dist/commands/improve/improve-strategies.js +8 -5
  79. package/dist/commands/improve/improve.js +128 -91
  80. package/dist/commands/improve/loop-stages.js +182 -20
  81. package/dist/commands/improve/memory/derived-ref.js +45 -43
  82. package/dist/commands/improve/memory/memory-belief.js +1 -1
  83. package/dist/commands/improve/memory/memory-contradiction-detect.js +4 -12
  84. package/dist/commands/improve/memory/memory-improve.js +6 -5
  85. package/dist/commands/improve/outcome-loop.js +22 -65
  86. package/dist/commands/improve/preparation.js +114 -123
  87. package/dist/commands/improve/proactive-maintenance.js +2 -5
  88. package/dist/commands/improve/reflect.js +56 -160
  89. package/dist/commands/improve/salience.js +11 -122
  90. package/dist/commands/improve/source-identity.js +10 -38
  91. package/dist/commands/lint/base-linter.js +20 -124
  92. package/dist/commands/lint/env-key-rules.js +31 -47
  93. package/dist/commands/lint/index.js +249 -43
  94. package/dist/commands/{events.js → log.js} +33 -38
  95. package/dist/commands/migrate-cli.js +92 -12
  96. package/dist/commands/migration-tool.js +46 -0
  97. package/dist/commands/observability-cli.js +70 -209
  98. package/dist/commands/proposal/drain.js +101 -29
  99. package/dist/commands/proposal/proposal-cli.js +76 -48
  100. package/dist/commands/proposal/proposal.js +54 -18
  101. package/dist/commands/proposal/propose-cli.js +88 -0
  102. package/dist/commands/proposal/propose.js +23 -15
  103. package/dist/commands/proposal/repository.js +701 -278
  104. package/dist/commands/proposal/validators/proposal-quality-validators.js +2 -8
  105. package/dist/commands/proposal/validators/proposal-validators.js +55 -7
  106. package/dist/commands/proposal/validators/proposals.js +4 -7
  107. package/dist/commands/read/curate.js +34 -53
  108. package/dist/commands/read/knowledge.js +150 -95
  109. package/dist/commands/read/registry-search.js +2 -2
  110. package/dist/commands/read/remember-cli.js +42 -15
  111. package/dist/commands/read/search-cli.js +180 -78
  112. package/dist/commands/read/search.js +58 -43
  113. package/dist/commands/read/show.js +197 -141
  114. package/dist/commands/registry-cli.js +12 -51
  115. package/dist/commands/remember.js +14 -57
  116. package/dist/commands/sources/add-cli.js +100 -31
  117. package/dist/commands/sources/bundle-cli.js +166 -0
  118. package/dist/commands/sources/bundle-config-ops.js +7 -2
  119. package/dist/commands/sources/info.js +18 -5
  120. package/dist/commands/sources/init.js +12 -12
  121. package/dist/commands/sources/installed-stashes.js +382 -98
  122. package/dist/commands/sources/schema-repair.js +3 -2
  123. package/dist/commands/sources/self-update.js +131 -38
  124. package/dist/commands/sources/source-add.js +72 -17
  125. package/dist/commands/sources/source-clone.js +129 -45
  126. package/dist/commands/sources/source-manage.js +43 -23
  127. package/dist/commands/sources/sources-cli.js +57 -208
  128. package/dist/commands/sources/stash-cli.js +46 -53
  129. package/dist/commands/tasks/tasks-cli.js +91 -97
  130. package/dist/commands/tasks/tasks.js +276 -421
  131. package/dist/commands/workflow-cli.js +175 -450
  132. package/dist/core/adapter/adapters/akm-adapter.js +47 -28
  133. package/dist/core/adapter/adapters/akm-lint.js +42 -27
  134. package/dist/core/adapter/adapters/akm-metadata.js +15 -44
  135. package/dist/core/adapter/adapters/akm-task-adapter.js +15 -13
  136. package/dist/core/adapter/adapters/akm-workflow-adapter.js +55 -71
  137. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  138. package/dist/core/adapter/adapters/generic-files-adapter.js +2 -0
  139. package/dist/core/adapter/adapters/index.js +6 -6
  140. package/dist/core/adapter/adapters/llm-wiki-adapter.js +14 -8
  141. package/dist/core/adapter/adapters/okf-adapter.js +187 -19
  142. package/dist/core/adapter/adapters/shared.js +3 -19
  143. package/dist/core/adapter/adapters/tool-dir-shared.js +8 -3
  144. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -0
  145. package/dist/core/adapter/detect-adapter.js +17 -0
  146. package/dist/core/adapter/recognize-match.js +6 -4
  147. package/dist/core/adapter/validate-context.js +214 -0
  148. package/dist/core/asset/akm-markdown.js +63 -0
  149. package/dist/core/asset/asset-placement.js +20 -6
  150. package/dist/core/asset/asset-ref.js +11 -9
  151. package/dist/core/asset/frontmatter-lint.js +30 -0
  152. package/dist/core/asset/frontmatter.js +37 -9
  153. package/dist/core/asset/markdown.js +40 -51
  154. package/dist/core/asset/resolve-ref.js +89 -18
  155. package/dist/core/asset/stash-meta.js +1 -1
  156. package/dist/core/bundle-id.js +51 -0
  157. package/dist/core/common.js +152 -38
  158. package/dist/core/config/config-io.js +12 -1
  159. package/dist/core/config/config-schema.js +35 -8
  160. package/dist/core/config/config-sources.js +55 -11
  161. package/dist/core/config/config-walker.js +25 -9
  162. package/dist/core/config/config.js +9 -48
  163. package/dist/core/config/experimental.js +21 -0
  164. package/dist/core/config/schema/embedding.js +5 -1
  165. package/dist/core/config/schema/experimental.js +30 -0
  166. package/dist/core/config/schema/improve-processes.js +0 -6
  167. package/dist/core/config/schema/improve.js +21 -3
  168. package/dist/core/config/schema/index-config.js +8 -15
  169. package/dist/core/config/schema/output.js +4 -1
  170. package/dist/core/config/schema/setup.js +9 -18
  171. package/dist/core/config/schema/sources-bundles.js +49 -33
  172. package/dist/core/config/schema/workflow.js +3 -3
  173. package/dist/core/env-secret-ref.js +76 -46
  174. package/dist/core/errors.js +18 -12
  175. package/dist/core/events.js +46 -128
  176. package/dist/core/file-change.js +6 -5
  177. package/dist/core/fs-txn.js +83 -7
  178. package/dist/core/git-message.js +2 -2
  179. package/dist/core/improve-result.js +1 -100
  180. package/dist/core/lesson-lint.js +1 -17
  181. package/dist/core/logs-db.js +2 -1
  182. package/dist/core/migration-operation.js +16 -0
  183. package/dist/core/mutation-target.js +78 -0
  184. package/dist/core/parse.js +4 -1
  185. package/dist/core/paths.js +17 -20
  186. package/dist/core/recognition-util.js +12 -14
  187. package/dist/core/redaction.js +34 -0
  188. package/dist/core/standards/resolve-standards-context.js +2 -14
  189. package/dist/core/standards/resolve-stash-standards.js +2 -2
  190. package/dist/core/standards/resolve-type-conventions.js +2 -2
  191. package/dist/core/state/migrations.js +41 -18
  192. package/dist/core/state-db.js +5 -14
  193. package/dist/core/structured.js +1 -1
  194. package/dist/core/subprocess.js +6 -4
  195. package/dist/core/text-truncation.js +9 -5
  196. package/dist/core/type-presentation.js +3 -3
  197. package/dist/core/warn.js +0 -3
  198. package/dist/core/write-source.js +771 -95
  199. package/dist/indexer/bundle-identity-guard.js +3 -2
  200. package/dist/indexer/db/graph-db.js +0 -24
  201. package/dist/indexer/ensure-index.js +1 -0
  202. package/dist/indexer/graph/graph-boost.js +9 -34
  203. package/dist/indexer/graph/graph-extraction.js +8 -5
  204. package/dist/indexer/index-writer-lock.js +53 -17
  205. package/dist/indexer/index-written-assets.js +16 -22
  206. package/dist/indexer/indexer.js +497 -239
  207. package/dist/indexer/installations.js +14 -96
  208. package/dist/indexer/passes/dir-staleness.js +16 -9
  209. package/dist/indexer/passes/memory-inference.js +11 -9
  210. package/dist/indexer/passes/metadata.js +113 -47
  211. package/dist/indexer/scan/doc-to-entry.js +38 -1
  212. package/dist/indexer/scan/drain-dir.js +13 -23
  213. package/dist/indexer/search/db-search.js +99 -54
  214. package/dist/indexer/search/fts-query.js +47 -24
  215. package/dist/indexer/search/ranking-contributors.js +42 -20
  216. package/dist/indexer/search/ranking.js +18 -99
  217. package/dist/indexer/search/search-fields.js +7 -2
  218. package/dist/indexer/search/search-source.js +82 -93
  219. package/dist/indexer/usage/usage-events.js +0 -89
  220. package/dist/indexer/walk/file-context.js +2 -1
  221. package/dist/indexer/walk/matchers.js +30 -43
  222. package/dist/indexer/walk/path-resolver.js +7 -2
  223. package/dist/indexer/walk/walker.js +38 -12
  224. package/dist/integrations/agent/builders.js +0 -6
  225. package/dist/integrations/agent/config.js +2 -2
  226. package/dist/integrations/agent/detect.js +49 -19
  227. package/dist/integrations/agent/engine-fallback.js +76 -0
  228. package/dist/integrations/agent/profiles.js +14 -0
  229. package/dist/integrations/agent/prompts.js +12 -8
  230. package/dist/integrations/agent/runner-dispatch.js +4 -2
  231. package/dist/integrations/agent/runner.js +0 -1
  232. package/dist/integrations/agent/spawn.js +5 -6
  233. package/dist/integrations/github.js +1 -1
  234. package/dist/integrations/harnesses/aider/agent-builder.js +6 -4
  235. package/dist/integrations/harnesses/amazonq/agent-builder.js +7 -4
  236. package/dist/integrations/harnesses/claude/session-log.js +0 -10
  237. package/dist/integrations/harnesses/codex/agent-builder.js +5 -2
  238. package/dist/integrations/harnesses/copilot/agent-builder.js +5 -3
  239. package/dist/integrations/harnesses/gemini/agent-builder.js +5 -3
  240. package/dist/integrations/harnesses/index.js +3 -7
  241. package/dist/integrations/harnesses/opencode/agent-builder.js +21 -2
  242. package/dist/integrations/harnesses/opencode/session-log.js +0 -15
  243. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +13 -4
  244. package/dist/integrations/harnesses/openhands/agent-builder.js +9 -6
  245. package/dist/integrations/harnesses/pi/agent-builder.js +6 -4
  246. package/dist/integrations/lockfile.js +101 -6
  247. package/dist/integrations/session-logs/index.js +3 -28
  248. package/dist/llm/client.js +136 -100
  249. package/dist/llm/embedders/remote.js +13 -5
  250. package/dist/llm/feature-gate.js +4 -12
  251. package/dist/llm/graph-extract.js +5 -11
  252. package/dist/llm/memory-infer.js +144 -1
  253. package/dist/llm/metadata-enhance.js +5 -7
  254. package/dist/llm/structured-call.js +1 -1
  255. package/dist/llm/usage-persist.js +26 -5
  256. package/dist/llm/usage-telemetry.js +25 -2
  257. package/dist/output/cli-hints.js +1 -2
  258. package/dist/output/context.js +22 -7
  259. package/dist/output/format-exempt.js +80 -0
  260. package/dist/output/generic-render.js +259 -0
  261. package/dist/output/render-registry.js +57 -0
  262. package/dist/output/renderers.js +14 -36
  263. package/dist/output/shapes/curate.js +10 -1
  264. package/dist/output/shapes/events.js +12 -7
  265. package/dist/output/shapes/helpers.js +56 -83
  266. package/dist/output/shapes/migrate.js +8 -0
  267. package/dist/output/shapes/passthrough.js +7 -41
  268. package/dist/output/shapes/proposal/producer.js +15 -7
  269. package/dist/output/shapes.js +2 -9
  270. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  271. package/dist/output/text/bundle-show.js +7 -0
  272. package/dist/output/text/command-format.js +164 -96
  273. package/dist/output/text/env.js +1 -3
  274. package/dist/output/text/events.js +8 -7
  275. package/dist/output/text/health-format.js +103 -0
  276. package/dist/output/text/health.js +7 -0
  277. package/dist/output/text/helpers.js +10 -8
  278. package/dist/output/text/lint-format.js +43 -0
  279. package/dist/output/text/{save.js → lint.js} +2 -2
  280. package/dist/output/text/migrate.js +88 -0
  281. package/dist/output/text/proposal/producer.js +4 -2
  282. package/dist/output/text/proposal-format.js +44 -72
  283. package/dist/output/text/registry-commands.js +1 -2
  284. package/dist/output/text/show-directives.js +15 -7
  285. package/dist/output/text/status-list.js +32 -0
  286. package/dist/output/text/sync.js +5 -0
  287. package/dist/output/text/workflow-format.js +24 -203
  288. package/dist/output/text/workflow.js +1 -7
  289. package/dist/output/text.js +16 -17
  290. package/dist/registry/factory.js +4 -6
  291. package/dist/registry/origin-resolve.js +16 -27
  292. package/dist/registry/providers/skills-sh.js +3 -3
  293. package/dist/registry/providers/static-index.js +13 -23
  294. package/dist/registry/resolve.js +42 -7
  295. package/dist/registry/semver.js +34 -84
  296. package/dist/runtime.js +2 -23
  297. package/dist/scripts/akm-migrate-node.js +60290 -0
  298. package/dist/scripts/akm-migrate.js +59628 -0
  299. package/dist/setup/detect.js +42 -15
  300. package/dist/setup/registry-stash-loader.js +2 -2
  301. package/dist/setup/setup.js +236 -136
  302. package/dist/setup/steps/connection.js +7 -9
  303. package/dist/setup/steps/platforms.js +9 -9
  304. package/dist/setup/steps/semantic.js +15 -3
  305. package/dist/setup/steps/sources.js +12 -13
  306. package/dist/setup/steps/stashdir.js +2 -3
  307. package/dist/setup/steps/tasks.js +237 -120
  308. package/dist/sources/freshness.js +1 -1
  309. package/dist/sources/provider-factory.js +11 -17
  310. package/dist/sources/providers/filesystem.js +2 -3
  311. package/dist/sources/providers/git-install.js +278 -34
  312. package/dist/sources/providers/git-provider.js +25 -23
  313. package/dist/sources/providers/git-stash.js +395 -106
  314. package/dist/sources/providers/git.js +2 -2
  315. package/dist/sources/providers/npm.js +16 -19
  316. package/dist/sources/providers/provider-utils.js +7 -4
  317. package/dist/sources/providers/sync-from-ref.js +3 -9
  318. package/dist/sources/providers/website.js +6 -1
  319. package/dist/sources/resolve.js +6 -5
  320. package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
  321. package/dist/sources/snapshot-fetchers/content-extract.js +566 -0
  322. package/dist/sources/snapshot-fetchers/fetcher-util.js +41 -0
  323. package/dist/sources/snapshot-fetchers/github.js +100 -0
  324. package/dist/sources/snapshot-fetchers/host-guard.js +291 -0
  325. package/dist/sources/snapshot-fetchers/registry.js +17 -1
  326. package/dist/sources/snapshot-fetchers/robots.js +348 -0
  327. package/dist/sources/snapshot-fetchers/rss.js +282 -0
  328. package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
  329. package/dist/sources/snapshot-fetchers/website-ingest.js +566 -268
  330. package/dist/sources/snapshot-fetchers/x.js +910 -0
  331. package/dist/storage/database.js +7 -0
  332. package/dist/storage/engines/sqlite-migrations.js +23 -111
  333. package/dist/storage/managed-db.js +2 -2
  334. package/dist/storage/repositories/canaries-repository.js +1 -1
  335. package/dist/storage/repositories/events-repository.js +27 -11
  336. package/dist/storage/repositories/improve-runs-repository.js +6 -12
  337. package/dist/storage/repositories/index-connection.js +17 -6
  338. package/dist/storage/repositories/index-entries-repository.js +151 -240
  339. package/dist/storage/repositories/index-entry-mapper.js +15 -11
  340. package/dist/storage/repositories/index-fts-repository.js +5 -2
  341. package/dist/storage/repositories/index-llm-cache-repository.js +0 -1
  342. package/dist/storage/repositories/index-meta-repository.js +2 -3
  343. package/dist/storage/repositories/index-schema.js +10 -25
  344. package/dist/storage/repositories/index-utility-repository.js +15 -28
  345. package/dist/storage/repositories/index-vec-repository.js +6 -1
  346. package/dist/storage/repositories/outcome-repository.js +119 -0
  347. package/dist/storage/repositories/proposals-repository.js +296 -59
  348. package/dist/storage/repositories/registry-cache.js +19 -0
  349. package/dist/storage/repositories/salience-repository.js +172 -0
  350. package/dist/storage/repositories/task-history-repository.js +15 -13
  351. package/dist/storage/repositories/workflow-runs-repository.js +52 -40
  352. package/dist/tasks/backends/cron.js +105 -15
  353. package/dist/tasks/backends/index.js +1 -1
  354. package/dist/tasks/backends/launchd.js +85 -38
  355. package/dist/tasks/backends/schtasks.js +135 -15
  356. package/dist/tasks/embedded.js +56 -40
  357. package/dist/tasks/parser.js +7 -157
  358. package/dist/tasks/resolve-akm-bin.js +137 -59
  359. package/dist/tasks/runner.js +79 -42
  360. package/dist/tasks/scheduler-invocation.js +220 -10
  361. package/dist/tasks/schema.js +24 -1
  362. package/dist/tasks/task-id.js +1 -3
  363. package/dist/tasks/validator.js +20 -6
  364. package/dist/workflows/authoring/authoring.js +94 -143
  365. package/dist/workflows/authoring/scope-key.js +1 -1
  366. package/dist/workflows/exec/frozen-judge.js +28 -2
  367. package/dist/workflows/exec/native-executor.js +77 -57
  368. package/dist/workflows/exec/param-secrets.js +9 -9
  369. package/dist/workflows/exec/run-workflow.js +133 -79
  370. package/dist/workflows/exec/step-work.js +219 -346
  371. package/dist/{migrate-storage-node.mjs → workflows/exec/unit-dispatch.js} +1 -5
  372. package/dist/workflows/ir/compile.js +141 -270
  373. package/dist/workflows/ir/freeze.js +40 -30
  374. package/dist/workflows/ir/params.js +135 -11
  375. package/dist/workflows/ir/plan-hash.js +1 -1
  376. package/dist/workflows/ir/schema.js +25 -26
  377. package/dist/workflows/parser.js +872 -307
  378. package/dist/workflows/program/expressions.js +20 -208
  379. package/dist/workflows/program/schema.js +7 -10
  380. package/dist/workflows/renderer.js +95 -68
  381. package/dist/workflows/resource-limits.js +2 -0
  382. package/dist/workflows/runtime/checkin.js +3 -3
  383. package/dist/workflows/runtime/plan-classifier.js +16 -75
  384. package/dist/workflows/runtime/runs.js +186 -127
  385. package/dist/workflows/runtime/unit-checkin.js +1 -1
  386. package/dist/workflows/runtime/unit-phases.js +2 -2
  387. package/dist/workflows/runtime/workflow-asset-loader.js +232 -83
  388. package/dist/workflows/schema.js +1 -11
  389. package/dist/workflows/validate-summary.js +30 -36
  390. package/dist/workflows/validator.js +21 -62
  391. package/docs/README.md +68 -0
  392. package/docs/migration/README.md +8 -0
  393. package/docs/migration/release-notes/0.7.0.md +11 -11
  394. package/docs/migration/release-notes/0.9.0.md +208 -27
  395. package/docs/migration/v0.7-to-v0.8.md +46 -47
  396. package/docs/migration/v0.8-to-v0.9.md +564 -208
  397. package/docs/migration/v0.9.0-troubleshooting.md +561 -0
  398. package/docs/reference/README.md +12 -0
  399. package/docs/reference/cli.md +2253 -0
  400. package/docs/reference/configuration.md +358 -0
  401. package/docs/reference/data-and-telemetry.md +105 -42
  402. package/docs/reference/workflows.md +647 -0
  403. package/package.json +22 -11
  404. package/schemas/akm-asset-envelope.json +93 -0
  405. package/schemas/akm-config.json +81 -128
  406. package/schemas/akm-workflow.json +74 -73
  407. package/dist/assets/tasks/core/backup.yml +0 -5
  408. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  409. package/dist/cli/config-migrate.js +0 -1806
  410. package/dist/cli/config-validate.js +0 -41
  411. package/dist/commands/backup-cli.js +0 -56
  412. package/dist/commands/bundle/bundle-cli.js +0 -68
  413. package/dist/commands/bundle/bundle.js +0 -219
  414. package/dist/commands/graph/graph-cli.js +0 -124
  415. package/dist/commands/graph/graph.js +0 -489
  416. package/dist/commands/improve/extract-watch.js +0 -140
  417. package/dist/commands/mv-cli.js +0 -1221
  418. package/dist/commands/sources/history.js +0 -201
  419. package/dist/commands/tasks/default-tasks.js +0 -186
  420. package/dist/core/migration-backup.js +0 -1234
  421. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -95
  422. package/dist/llm/memory-infer-impl.js +0 -138
  423. package/dist/migrate/legacy/config-source-migration.js +0 -223
  424. package/dist/migrate/legacy/content-migration.js +0 -305
  425. package/dist/migrate/legacy/legacy-layout.js +0 -779
  426. package/dist/migrate/legacy/legacy-paths.js +0 -25
  427. package/dist/migrate/legacy/legacy-stash-json.js +0 -72
  428. package/dist/migrate/legacy/proposal-fs-import.js +0 -168
  429. package/dist/migrate/legacy/task-target-ref-migration.js +0 -272
  430. package/dist/migrate/legacy/three-db-cutover.js +0 -841
  431. package/dist/migrate/legacy/workflow-migrations-bodies.js +0 -52
  432. package/dist/migrate/legacy/workflow-migrations-frozen.js +0 -21
  433. package/dist/migrate/legacy-ref-grammar.js +0 -214
  434. package/dist/output/shapes/distill.js +0 -14
  435. package/dist/output/shapes/history.js +0 -11
  436. package/dist/output/text/distill.js +0 -6
  437. package/dist/output/text/enable-disable.js +0 -8
  438. package/dist/output/text/history.js +0 -6
  439. package/dist/registry/build-index.js +0 -382
  440. package/dist/schemas/akm-config.json +0 -4704
  441. package/dist/schemas/akm-task.json +0 -87
  442. package/dist/schemas/akm-workflow.json +0 -372
  443. package/dist/scripts/migrate-storage.js +0 -3816
  444. package/dist/workflows/authoring/workflow-program-template.yaml +0 -31
  445. package/dist/workflows/cli.js +0 -53
  446. package/dist/workflows/exec/brief.js +0 -481
  447. package/dist/workflows/exec/report.js +0 -1460
  448. package/dist/workflows/exec/watch.js +0 -116
  449. package/dist/workflows/program/parser.js +0 -813
  450. package/dist/workflows/program/project.js +0 -104
@@ -13,8 +13,8 @@
13
13
  * commit behaviour — they only ever touch the filesystem. Git-backed targets
14
14
  * are committed in a SINGLE batch at the operation boundary via
15
15
  * {@link commitWriteTargetBoundary} (which delegates to `saveGitStash`). This
16
- * stages only operation-owned paths that still have a Git status entry as one
17
- * complete commit instead of one noisy, incomplete commit per asset.
16
+ * commits only operation-owned exact paths as one complete commit instead of
17
+ * one noisy, incomplete commit per asset.
18
18
  *
19
19
  * This module is still the **single dispatch point** for write/delete: callers
20
20
  * (remember, import, source-add, etc.) MUST go through `writeAssetToSource` /
@@ -24,49 +24,230 @@
24
24
  */
25
25
  import fs from "node:fs";
26
26
  import path from "node:path";
27
+ import { withAssetMutationLeaseSync } from "../indexer/index-writer-lock.js";
27
28
  import { lockContentRootFor } from "../integrations/lockfile.js";
28
- import { getCachePaths, listGitChangedPaths, parseGitRepoUrl, saveGitStash } from "../sources/providers/git.js";
29
+ import { GitStashPushError, getCachePaths, inspectGitUpstream, isGitBackedStash, parseGitRepoUrl, runGit, saveGitStash, } from "../sources/providers/git.js";
30
+ import { assertGitExactPathsClean, assertNoIgnoredExactPaths, listIgnoredExactPaths, reconcileGitExactPathIndex, } from "../sources/providers/git-stash.js";
31
+ import { detectAdapterId } from "./adapter/detect-adapter.js";
32
+ import { ensureAkmMarkdownType } from "./asset/akm-markdown.js";
29
33
  import { assetPathForName, stashDirFor } from "./asset/asset-placement.js";
30
- import { displayRef } from "./asset/resolve-ref.js";
34
+ import { conceptIdFromTypeName, displayRef } from "./asset/resolve-ref.js";
35
+ import { deriveBundleId } from "./bundle-id.js";
31
36
  import { isWithin, resolveStashDir } from "./common.js";
32
37
  import { resolveConfiguredSources } from "./config/config.js";
33
38
  import { ConfigError, UsageError } from "./errors.js";
34
39
  import { sanitizeCommitMessage } from "./git-message.js";
35
40
  import { warn } from "./warn.js";
36
- // Re-exported so existing `import { sanitizeCommitMessage } from
37
- // "./core/write-source"` sites are unaffected by the KILL 6 sever (the
38
- // helper moved to git-message.ts to break the write-source.ts → git.ts →
39
- // git-stash.ts → write-source.ts 3-file import cycle).
40
- export { sanitizeCommitMessage };
41
41
  /**
42
42
  * Source kinds that the loader is allowed to mark `writable: true`. Anything
43
43
  * else is rejected at config load (per locked decision 4) — see
44
44
  * {@link assertWritableAllowedForKind}.
45
45
  */
46
46
  const REJECTED_WRITABLE_KINDS = new Set(["website", "npm"]);
47
- const pendingGitPaths = new Map();
47
+ const pendingGitMutations = new Map();
48
+ const EMPTY_GIT_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
49
+ const GIT_PUSH_TIMEOUT_MS = 120_000;
48
50
  function gitTargetKey(source) {
49
51
  return `${path.resolve(source.repoPath ?? source.path)}\0${path.resolve(source.path)}`;
50
52
  }
51
- /** Register a successful direct mutation for the target's exact-path boundary commit. */
53
+ /** Record the exact post-mutation blob for the target's boundary commit. */
52
54
  export function recordWriteTargetPath(source, filePath) {
53
55
  if (source.kind !== "git")
54
56
  return;
57
+ const repoDir = path.resolve(source.repoPath ?? source.path);
58
+ if (!isGitBackedStash(repoDir))
59
+ return;
55
60
  const key = gitTargetKey(source);
56
- const paths = pendingGitPaths.get(key) ?? new Set();
57
- paths.add(path.resolve(filePath));
58
- pendingGitPaths.set(key, paths);
61
+ const pending = pendingGitMutations.get(key) ?? {
62
+ baseHead: readOptionalGitHead(repoDir),
63
+ snapshots: {},
64
+ };
65
+ const snapshot = captureGitPathSnapshot({ source, config: { type: "git" } }, filePath);
66
+ pending.snapshots[snapshot.path] = snapshot.state;
67
+ pendingGitMutations.set(key, pending);
59
68
  }
60
- function takeGitTargetPaths(source) {
69
+ function repoRelativeGitPath(source, filePath) {
70
+ const repoDir = path.resolve(source.repoPath ?? source.path);
71
+ const absolutePath = path.resolve(filePath);
72
+ const [relativePath] = normalizePublicationPaths([path.relative(repoDir, absolutePath).replaceAll(path.sep, "/")]);
73
+ return relativePath;
74
+ }
75
+ /** Reject dirty or ignored exact paths before a direct transaction mutation. */
76
+ export function assertWriteTargetPathsClean(source, filePaths) {
77
+ if (source.kind !== "git")
78
+ return;
79
+ const repoDir = path.resolve(source.repoPath ?? source.path);
80
+ if (!isGitBackedStash(repoDir))
81
+ return;
61
82
  const key = gitTargetKey(source);
62
- const absolutePaths = pendingGitPaths.get(key);
63
- pendingGitPaths.delete(key);
64
- if (!absolutePaths)
65
- return [];
83
+ const pending = pendingGitMutations.get(key);
84
+ for (const filePath of filePaths) {
85
+ const relativePath = repoRelativeGitPath(source, filePath);
86
+ if (pending && Object.hasOwn(pending.snapshots, relativePath)) {
87
+ preflightGitPathMutation(source, filePath);
88
+ }
89
+ else {
90
+ assertGitExactPathsClean(repoDir, [relativePath]);
91
+ }
92
+ }
93
+ }
94
+ /** Preflight one operation's complete exact-path set before its first mutation. */
95
+ export function planWriteTargetPublication(target, filePaths, options) {
96
+ const paths = [...new Set(filePaths.map((filePath) => path.resolve(filePath)))];
97
+ assertNoWritePathDescendantSymlinks(target.source, paths);
98
+ if (target.source.kind !== "git")
99
+ return { target, paths, publish: false };
100
+ const repoDir = path.resolve(target.source.repoPath ?? target.source.path);
101
+ if (!isGitBackedStash(repoDir))
102
+ return { target, paths, publish: false };
103
+ const expectedBaseHead = readOptionalGitHead(repoDir);
104
+ const relativePaths = paths.map((filePath) => repoRelativeGitPath(target.source, filePath));
105
+ const ignored = new Set(listIgnoredExactPaths(repoDir, relativePaths));
106
+ if (ignored.size > 0) {
107
+ if (options.ignored === "reject") {
108
+ throw new UsageError(`Exact Git publication path is ignored: ${[...ignored][0]}. Update .gitignore or choose a tracked destination before writing.`);
109
+ }
110
+ }
111
+ const unignoredPaths = paths.filter((_, index) => !ignored.has(relativePaths[index]));
112
+ assertWriteTargetPathsClean(target.source, unignoredPaths);
113
+ assertWriteTargetPlanBase(target, expectedBaseHead);
114
+ return { target, paths, publish: ignored.size === 0, expectedBaseHead };
115
+ }
116
+ function assertNoWritePathDescendantSymlinks(source, filePaths) {
117
+ const lexicalRoot = path.resolve(source.path);
118
+ const canonicalRoot = fs.realpathSync(lexicalRoot);
119
+ for (const filePath of filePaths) {
120
+ const relative = path.relative(lexicalRoot, filePath);
121
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
122
+ throw new UsageError(`Write path resolves outside source "${source.name}".`, "PATH_ESCAPE_VIOLATION");
123
+ }
124
+ let current = canonicalRoot;
125
+ for (const segment of relative.split(path.sep)) {
126
+ current = path.join(current, segment);
127
+ try {
128
+ if (fs.lstatSync(current).isSymbolicLink()) {
129
+ throw new UsageError(`Write path contains a symbolic link below source "${source.name}": ${relative}`);
130
+ }
131
+ }
132
+ catch (error) {
133
+ if (error.code === "ENOENT")
134
+ break;
135
+ throw error;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ function assertWriteTargetPlanBase(target, expectedBaseHead) {
141
+ const repoDir = path.resolve(target.source.repoPath ?? target.source.path);
142
+ if (readOptionalGitHead(repoDir) !== expectedBaseHead) {
143
+ throw new UsageError(`Git target "${target.source.name}" advanced after exact-path preflight.`);
144
+ }
145
+ }
146
+ /** Revalidate a publication plan immediately before its first mutation. */
147
+ export function beginWriteTargetMutation(plan) {
148
+ if (plan.expectedBaseHead === undefined)
149
+ return;
150
+ assertWriteTargetPlanBase(plan.target, plan.expectedBaseHead);
151
+ }
152
+ /** Publish exactly the paths bound by {@link planWriteTargetPublication}. */
153
+ export function publishWriteTargetPlan(plan, message, expectedSnapshots) {
154
+ if (!plan.publish)
155
+ return;
156
+ if (plan.expectedBaseHead === undefined) {
157
+ throw new UsageError(`Git publication plan for "${plan.target.source.name}" has no preflight base.`);
158
+ }
159
+ assertWriteTargetPlanBase(plan.target, plan.expectedBaseHead);
160
+ for (const filePath of plan.paths)
161
+ recordWriteTargetPath(plan.target.source, filePath);
162
+ commitWriteTargetBoundary(plan.target, message, {
163
+ paths: [...plan.paths],
164
+ expectedBaseHead: plan.expectedBaseHead,
165
+ ...(expectedSnapshots ? { expectedSnapshots } : {}),
166
+ });
167
+ }
168
+ /** Hold the shared asset lease from exact-path preflight through publication. */
169
+ export function withWriteTargetMutation(target, paths, options, mutate) {
170
+ return withAssetMutationLeaseSync(options.purpose, () => {
171
+ const plan = planWriteTargetPublication(target, paths, { ignored: options.ignored });
172
+ beginWriteTargetMutation(plan);
173
+ const result = mutate();
174
+ const expectedSnapshots = captureIntendedWriteTargetState(plan);
175
+ publishWriteTargetPlan(plan, options.message, expectedSnapshots);
176
+ return result;
177
+ });
178
+ }
179
+ function captureIntendedWriteTargetState(plan) {
180
+ if (plan.expectedBaseHead === undefined)
181
+ return undefined;
182
+ const snapshots = {};
183
+ for (const filePath of plan.paths) {
184
+ const snapshot = captureGitPathSnapshot(plan.target, filePath);
185
+ snapshots[snapshot.path] = snapshot.state;
186
+ }
187
+ return snapshots;
188
+ }
189
+ function readOptionalGitHead(repoDir) {
190
+ const result = runGit(["-C", repoDir, "rev-parse", "--verify", "HEAD"]);
191
+ return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : null;
192
+ }
193
+ function preflightGitPathMutation(source, filePath) {
194
+ if (source.kind !== "git")
195
+ return undefined;
66
196
  const repoDir = path.resolve(source.repoPath ?? source.path);
67
- return [...absolutePaths]
68
- .map((filePath) => path.relative(repoDir, filePath).replaceAll(path.sep, "/"))
69
- .filter((filePath) => filePath && filePath !== ".." && !filePath.startsWith("../"));
197
+ if (!isGitBackedStash(repoDir))
198
+ return undefined;
199
+ const relativePath = repoRelativeGitPath(source, filePath);
200
+ const key = gitTargetKey(source);
201
+ let pending = pendingGitMutations.get(key);
202
+ const created = pending === undefined;
203
+ if (!pending) {
204
+ pending = { baseHead: readOptionalGitHead(repoDir), snapshots: {} };
205
+ pendingGitMutations.set(key, pending);
206
+ }
207
+ try {
208
+ if (readOptionalGitHead(repoDir) !== pending.baseHead) {
209
+ throw new UsageError(`Git target "${source.name}" advanced before its exact-path mutation.`);
210
+ }
211
+ if (!Object.hasOwn(pending.snapshots, relativePath)) {
212
+ assertGitExactPathsClean(repoDir, [relativePath]);
213
+ return { key, created };
214
+ }
215
+ assertNoIgnoredExactPaths(repoDir, [relativePath]);
216
+ const index = runGit([
217
+ "--literal-pathspecs",
218
+ "-C",
219
+ repoDir,
220
+ "diff",
221
+ "--cached",
222
+ "--quiet",
223
+ pending.baseHead ?? EMPTY_GIT_TREE,
224
+ "--",
225
+ relativePath,
226
+ ]);
227
+ if (index.status === 1) {
228
+ throw new UsageError(`Exact Git operation path has staged work: ${relativePath}. Commit, stash, or discard that path before retrying.`);
229
+ }
230
+ if (index.status !== 0) {
231
+ throw new Error(`Cannot inspect Git index for exact operation path ${relativePath}: ${index.stderr.trim()}`);
232
+ }
233
+ const current = captureGitPathSnapshot({ source, config: { type: "git" } }, filePath);
234
+ if (!sameGitPathState(current.state, pending.snapshots[relativePath] ?? null)) {
235
+ throw new UsageError(`Exact Git operation path changed after AKM wrote it: ${relativePath}. Commit, stash, or discard that path before retrying.`);
236
+ }
237
+ return { key, created };
238
+ }
239
+ catch (error) {
240
+ if (created && Object.keys(pending.snapshots).length === 0)
241
+ pendingGitMutations.delete(key);
242
+ throw error;
243
+ }
244
+ }
245
+ function discardEmptyGitPreflight(preflight) {
246
+ if (!preflight?.created)
247
+ return;
248
+ const pending = pendingGitMutations.get(preflight.key);
249
+ if (pending && Object.keys(pending.snapshots).length === 0)
250
+ pendingGitMutations.delete(preflight.key);
70
251
  }
71
252
  // ── Portability advisory (review 13, D1) ────────────────────────────────────
72
253
  /**
@@ -144,11 +325,20 @@ export function assertWritableAllowedForKind(entry) {
144
325
  export async function writeAssetToSource(source, config, ref, content) {
145
326
  ensureWritable(source, config);
146
327
  assertSupportedKind(source);
328
+ assertAkmAssetWrite(source);
147
329
  const filePath = resolveAssetFilePath(source, ref);
148
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
149
- const normalized = content.endsWith("\n") ? content : `${content}\n`;
150
- fs.writeFileSync(filePath, normalized, "utf8");
151
- recordWriteTargetPath(source, filePath);
330
+ const authored = filePath.toLowerCase().endsWith(".md") ? ensureAkmMarkdownType(content, ref.type) : content;
331
+ const normalized = authored.endsWith("\n") ? authored : `${authored}\n`;
332
+ const preflight = preflightGitPathMutation(source, filePath);
333
+ try {
334
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
335
+ fs.writeFileSync(filePath, normalized, "utf8");
336
+ recordWriteTargetPath(source, filePath);
337
+ }
338
+ catch (error) {
339
+ discardEmptyGitPreflight(preflight);
340
+ throw error;
341
+ }
152
342
  // Non-fatal portability advisory (review 13, D1): flag absolute host home
153
343
  // paths in the written content. These make the stash non-portable and leak
154
344
  // the local username. We warn AFTER the write so the advisory never blocks it.
@@ -168,12 +358,20 @@ export async function writeAssetToSource(source, config, ref, content) {
168
358
  export async function deleteAssetFromSource(source, config, ref) {
169
359
  ensureWritable(source, config);
170
360
  assertSupportedKind(source);
361
+ assertAkmAssetWrite(source);
171
362
  const filePath = resolveAssetFilePath(source, ref);
172
363
  if (!fs.existsSync(filePath)) {
173
364
  throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
174
365
  }
175
- fs.unlinkSync(filePath);
176
- recordWriteTargetPath(source, filePath);
366
+ const preflight = preflightGitPathMutation(source, filePath);
367
+ try {
368
+ fs.unlinkSync(filePath);
369
+ recordWriteTargetPath(source, filePath);
370
+ }
371
+ catch (error) {
372
+ discardEmptyGitPreflight(preflight);
373
+ throw error;
374
+ }
177
375
  return { path: filePath, ref: displayRef({ type: ref.type, name: ref.name, bundleId: ref.origin }) };
178
376
  }
179
377
  /**
@@ -190,48 +388,499 @@ export async function deleteAssetFromSource(source, config, ref) {
190
388
  * caller paths), commits once, and pushes when the target is writable, has a
191
389
  * remote, and `push !== false`.
192
390
  *
193
- * The deprecated `options.pushOnCommit` on the source config is now fully
194
- * IGNORED (Decision 6, WI-9.6b): it neither sets nor suppresses `push`. Only a
195
- * one-time deprecation warning remains (see {@link warnIfPushOnCommit}).
196
391
  */
197
392
  export function commitWriteTargetBoundary(target, message, options) {
198
393
  if (target.source.kind !== "git")
199
394
  return;
200
- warnIfPushOnCommit(target.config);
201
395
  const push = options?.push;
202
396
  const writable = resolveWritable(target.config);
203
- const repoDir = target.source.repoPath ?? target.source.path;
204
- const changedPaths = new Set(listGitChangedPaths(repoDir));
205
- const paths = [...new Set([...(options?.paths ?? []), ...takeGitTargetPaths(target.source)])]
206
- .map((filePath) => filePath.replaceAll(path.sep, "/"))
207
- .filter((filePath) => changedPaths.has(filePath));
397
+ const repoDir = path.resolve(target.source.repoPath ?? target.source.path);
398
+ const key = gitTargetKey(target.source);
399
+ const pending = pendingGitMutations.get(key);
400
+ const expectedBaseHead = options?.expectedBaseHead !== undefined
401
+ ? options.expectedBaseHead
402
+ : pending
403
+ ? pending.baseHead
404
+ : readOptionalGitHead(repoDir);
405
+ const normalizeBoundaryPath = (filePath) => {
406
+ const relativePath = path.isAbsolute(filePath)
407
+ ? path.relative(repoDir, path.resolve(filePath)).replaceAll(path.sep, "/")
408
+ : filePath.replaceAll(path.sep, "/");
409
+ return normalizePublicationPaths([relativePath])[0];
410
+ };
411
+ const providedSnapshots = new Map(Object.entries(options?.expectedSnapshots ?? {}).map(([filePath, state]) => [
412
+ normalizeBoundaryPath(filePath),
413
+ state,
414
+ ]));
415
+ const paths = normalizePublicationPaths([
416
+ ...(options?.paths ?? []).map(normalizeBoundaryPath),
417
+ ...Object.keys(pending?.snapshots ?? {}),
418
+ ]);
419
+ const expectedSnapshots = {};
420
+ for (const filePath of paths) {
421
+ if (providedSnapshots.has(filePath)) {
422
+ expectedSnapshots[filePath] = providedSnapshots.get(filePath) ?? null;
423
+ }
424
+ else if (pending && Object.hasOwn(pending.snapshots, filePath)) {
425
+ expectedSnapshots[filePath] = pending.snapshots[filePath] ?? null;
426
+ }
427
+ else {
428
+ expectedSnapshots[filePath] = captureGitPathSnapshot(target, path.join(repoDir, filePath)).state;
429
+ }
430
+ }
431
+ if (options?.expectedBaseHead !== undefined &&
432
+ pending !== undefined &&
433
+ options.expectedBaseHead !== pending.baseHead) {
434
+ throw new Error(`Git boundary base does not match the recorded exact-path mutation base.`);
435
+ }
208
436
  // Assets may live under <repo>/content, but git synchronization always runs
209
437
  // against the repository root.
210
- saveGitStash(undefined, message, writable, {
211
- repoDir,
438
+ try {
439
+ saveGitStash(undefined, message, writable, {
440
+ repoDir,
441
+ paths,
442
+ expectedSnapshots,
443
+ expectedBaseHead,
444
+ ...(push === undefined ? {} : { push }),
445
+ ...(options?.transactionId === undefined ? {} : { transactionId: options.transactionId }),
446
+ });
447
+ pendingGitMutations.delete(key);
448
+ }
449
+ catch (error) {
450
+ const currentHead = readOptionalGitHead(repoDir);
451
+ if (pending && currentHead !== pending.baseHead)
452
+ pendingGitMutations.delete(key);
453
+ if (error instanceof GitStashPushError) {
454
+ throw new Error(`Changes were committed as ${error.commit}, but publication failed: ${error.message}`, {
455
+ cause: error,
456
+ });
457
+ }
458
+ throw error;
459
+ }
460
+ }
461
+ function sameGitPathState(left, right) {
462
+ return left === null ? right === null : right !== null && left.oid === right.oid && left.mode === right.mode;
463
+ }
464
+ /** Capture the exact checkout/upstream identity before a durable mutation starts. */
465
+ export function captureGitPublication(target) {
466
+ if (target.source.kind !== "git")
467
+ return undefined;
468
+ const identity = readGitPublicationIdentity(target);
469
+ const head = runGit(["-C", identity.repoPath, "rev-parse", "HEAD"]);
470
+ if (head.status !== 0 || !head.stdout.trim()) {
471
+ throw new Error(`Cannot read Git HEAD for target "${target.source.name}".`);
472
+ }
473
+ const baseHead = head.stdout.trim();
474
+ let upstreamHead;
475
+ if (identity.upstream) {
476
+ const upstream = runGit(["-C", identity.repoPath, "rev-parse", identity.upstream]);
477
+ if (upstream.status !== 0 || !upstream.stdout.trim()) {
478
+ throw new Error(`Cannot read Git upstream for target "${target.source.name}".`);
479
+ }
480
+ upstreamHead = upstream.stdout.trim();
481
+ if (baseHead !== upstreamHead) {
482
+ throw new Error(`Writable Git target "${target.source.name}" changed after mutation preflight.`);
483
+ }
484
+ }
485
+ return { ...identity, baseHead, ...(upstreamHead ? { upstreamHead } : {}) };
486
+ }
487
+ /** Capture one repo-relative path exactly as Git will stage it. */
488
+ export function captureGitPathSnapshot(target, filePath) {
489
+ if (target.source.kind !== "git")
490
+ throw new Error(`Target "${target.source.name}" is not Git-backed.`);
491
+ const repoPath = path.resolve(target.source.repoPath ?? target.source.path);
492
+ const absolutePath = path.resolve(filePath);
493
+ const relativePath = path.relative(repoPath, absolutePath).replaceAll(path.sep, "/");
494
+ const [normalizedPath] = normalizePublicationPaths([relativePath]);
495
+ return {
496
+ path: normalizedPath,
497
+ state: captureGitPathState(repoPath, absolutePath, normalizedPath),
498
+ };
499
+ }
500
+ function captureGitPathState(repoPath, absolutePath, relativePath) {
501
+ let stat;
502
+ try {
503
+ stat = fs.lstatSync(absolutePath);
504
+ }
505
+ catch (error) {
506
+ if (error.code === "ENOENT")
507
+ return null;
508
+ throw error;
509
+ }
510
+ let mode;
511
+ let oidResult;
512
+ if (stat.isSymbolicLink()) {
513
+ mode = "120000";
514
+ oidResult = runGit(["-C", repoPath, "hash-object", "--stdin"], { input: fs.readlinkSync(absolutePath) });
515
+ }
516
+ else if (stat.isFile()) {
517
+ mode = stat.mode & 0o111 ? "100755" : "100644";
518
+ oidResult = runGit(["-C", repoPath, "hash-object", `--path=${relativePath}`, "--", absolutePath]);
519
+ }
520
+ else {
521
+ throw new Error(`Git publication path is not a file: ${relativePath}`);
522
+ }
523
+ if (oidResult.status !== 0 || !oidResult.stdout.trim()) {
524
+ throw new Error(`Cannot snapshot Git publication path: ${relativePath}`);
525
+ }
526
+ return { oid: oidResult.stdout.trim(), mode };
527
+ }
528
+ /** Create or recover the one transaction-owned commit without pushing it. */
529
+ export function ensureGitTransactionCommit(target, publication, options) {
530
+ assertGitPublicationIdentity(target, publication);
531
+ const paths = normalizePublicationPaths(options.paths);
532
+ validateGitWorktreeSnapshots(target, paths, options.snapshots);
533
+ if (publication.commit !== undefined) {
534
+ if (publication.commit !== null) {
535
+ validateGitTransactionCommit(target, publication, publication.commit, options.transactionId, paths, options.snapshots);
536
+ reconcileGitExactPathIndex(publication.repoPath, publication.baseHead, publication.commit, paths);
537
+ }
538
+ return publication.commit;
539
+ }
540
+ const existing = findGitTransactionCommit(publication.repoPath, publication.baseHead, options.transactionId);
541
+ if (existing) {
542
+ validateGitTransactionCommit(target, publication, existing, options.transactionId, paths, options.snapshots);
543
+ reconcileGitExactPathIndex(publication.repoPath, publication.baseHead, existing, paths);
544
+ return existing;
545
+ }
546
+ const head = readGitHead(publication.repoPath, target.source.name);
547
+ if (head !== publication.baseHead) {
548
+ throw new Error(`Cannot publish Git transaction ${options.transactionId}: target "${target.source.name}" advanced before its commit was recorded.`);
549
+ }
550
+ commitWriteTargetBoundary(target, options.message, {
212
551
  paths,
213
- ...(push === undefined ? {} : { push }),
552
+ push: false,
553
+ transactionId: options.transactionId,
554
+ expectedBaseHead: publication.baseHead,
555
+ expectedSnapshots: options.snapshots,
214
556
  });
557
+ const committed = findGitTransactionCommit(publication.repoPath, publication.baseHead, options.transactionId);
558
+ if (!committed) {
559
+ if (readGitHead(publication.repoPath, target.source.name) === publication.baseHead) {
560
+ validateGitCommitSnapshots(publication.repoPath, publication.baseHead, paths, options.snapshots);
561
+ return null;
562
+ }
563
+ throw new Error(`Cannot identify the Git commit for transaction ${options.transactionId}.`);
564
+ }
565
+ validateGitTransactionCommit(target, publication, committed, options.transactionId, paths, options.snapshots);
566
+ reconcileGitExactPathIndex(publication.repoPath, publication.baseHead, committed, paths);
567
+ return committed;
568
+ }
569
+ /**
570
+ * Kind-neutral snapshot capture for one mutated path.
571
+ *
572
+ * Returns `undefined` for kinds with no publication model, so command layers
573
+ * never branch on `source.kind` themselves — the same fail-soft contract
574
+ * {@link captureGitPublication} and {@link commitWriteTargetBoundary} already
575
+ * use. `captureGitPathSnapshot` throws for non-git targets, which is what
576
+ * forced callers to guard; this absorbs that guard.
577
+ */
578
+ export function captureWriteTargetPathSnapshot(target, filePath) {
579
+ if (target.source.kind !== "git")
580
+ return undefined;
581
+ return captureGitPathSnapshot(target, filePath);
215
582
  }
216
583
  /**
217
- * Emit a one-time deprecation warning the first time a source config carrying
218
- * `options.pushOnCommit` is encountered. The field still parses (for old
219
- * configs) but is now FULLY IGNORED (Decision 6, WI-9.6b): it no longer maps
220
- * onto the batch push gate in any way (neither opts in nor opts out). The
221
- * field will be REMOVED in 0.10.
584
+ * Kind-neutral commit + publish for one transaction boundary.
585
+ *
586
+ * A no-op (returns `undefined`) for kinds with no publication model. For a
587
+ * publication-backed target this absorbs BOTH the kind test and the
588
+ * "transaction lacks durable publication identity" invariant, so callers hold
589
+ * no provider knowledge. `onCommitRecorded` fires between the ensure and the
590
+ * push, letting a caller persist the commit and advance its own journal phase
591
+ * without inspecting the target.
592
+ *
593
+ * `missingPublicationError` lets a caller keep its own error CLASS for the
594
+ * missing-identity case. The two call sites disagreed historically —
595
+ * consolidate threw `ConfigError` (exit 78), proposal a plain `Error`
596
+ * (exit 70) — and collapsing them here would silently change one command's
597
+ * exit code. The guard moves; the classification stays with the caller.
222
598
  */
223
- let pushOnCommitWarned = false;
224
- function warnIfPushOnCommit(config) {
225
- if (config.options?.pushOnCommit === undefined)
599
+ export function publishWriteTargetTransaction(target, publication, options) {
600
+ if (target.source.kind !== "git")
601
+ return undefined;
602
+ if (!publication) {
603
+ throw (options.missingPublicationError?.(target.source.name) ??
604
+ new Error(`Proposal transaction ${options.transactionId} has no Git publication identity.`));
605
+ }
606
+ const commit = ensureGitTransactionCommit(target, publication, {
607
+ transactionId: options.transactionId,
608
+ message: options.message,
609
+ paths: options.paths,
610
+ snapshots: options.snapshots,
611
+ });
612
+ options.onCommitRecorded?.(commit);
613
+ publishGitTransactionCommit(target, publication, options.transactionId, options.paths, options.snapshots);
614
+ return { commit };
615
+ }
616
+ /** Push only the recorded transaction commit, never later local descendants. */
617
+ export function publishGitTransactionCommit(target, publication, transactionId, paths, snapshots) {
618
+ assertGitPublicationIdentity(target, publication);
619
+ if (publication.commit === undefined) {
620
+ throw new Error(`Git transaction ${transactionId} has no recorded publication decision.`);
621
+ }
622
+ if (publication.commit === null)
623
+ return;
624
+ validateGitTransactionCommit(target, publication, publication.commit, transactionId, normalizePublicationPaths(paths), snapshots);
625
+ if (!publication.remote || !publication.mergeRef)
226
626
  return;
227
- if (pushOnCommitWarned)
627
+ if (!publication.upstream)
628
+ throw new Error(`Git transaction ${transactionId} has no recorded upstream.`);
629
+ if (runGit(["-C", publication.repoPath, "merge-base", "--is-ancestor", publication.commit, publication.upstream])
630
+ .status === 0) {
228
631
  return;
229
- pushOnCommitWarned = true;
230
- const label = config.name ? ` on source "${config.name}"` : "";
231
- process.stderr.write(`warning: \`options.pushOnCommit\`${label} is deprecated (0.9.0) and now fully ignored — it no longer ` +
232
- "affects push behaviour in any way. akm commits writes in a single batch at the operation boundary and " +
233
- "pushes when the target is writable with a remote and push isn't explicitly disabled. Remove the option " +
234
- "or rely on sync push instead; it will be REMOVED in 0.10.\n");
632
+ }
633
+ if (runGit(["-C", publication.repoPath, "merge-base", "--is-ancestor", publication.upstream, publication.commit])
634
+ .status !== 0) {
635
+ throw new Error(`Cannot publish Git transaction ${transactionId}: upstream history diverged.`);
636
+ }
637
+ if (!publication.upstreamHead) {
638
+ throw new Error(`Git transaction ${transactionId} has no recorded upstream lease.`);
639
+ }
640
+ const pushed = runGit([
641
+ "-C",
642
+ publication.repoPath,
643
+ "push",
644
+ `--force-with-lease=${publication.mergeRef}:${publication.upstreamHead}`,
645
+ publication.remote,
646
+ `${publication.commit}:${publication.mergeRef}`,
647
+ ], { timeout: GIT_PUSH_TIMEOUT_MS });
648
+ if (pushed.status !== 0) {
649
+ throw new Error(`git push failed for target "${target.source.name}": ${pushed.stderr.trim()}`);
650
+ }
651
+ }
652
+ function readGitPublicationIdentity(target) {
653
+ const repoPath = path.resolve(target.source.repoPath ?? target.source.path);
654
+ const branchResult = runGit(["-C", repoPath, "symbolic-ref", "--quiet", "--short", "HEAD"]);
655
+ const branch = branchResult.status === 0 ? branchResult.stdout.trim() : undefined;
656
+ const remotes = runGit(["-C", repoPath, "remote"]);
657
+ if (remotes.status !== 0)
658
+ throw new Error(`Cannot inspect Git remotes for target "${target.source.name}".`);
659
+ if (!remotes.stdout.trim())
660
+ return { repoPath, ...(branch ? { branch } : {}) };
661
+ if (!branch)
662
+ throw new Error(`Writable Git target "${target.source.name}" is detached from a branch.`);
663
+ const remoteResult = runGit(["-C", repoPath, "config", "--get", `branch.${branch}.remote`]);
664
+ const mergeResult = runGit(["-C", repoPath, "config", "--get", `branch.${branch}.merge`]);
665
+ if (remoteResult.status !== 0 || mergeResult.status !== 0) {
666
+ throw new Error(`Writable Git target "${target.source.name}" has no configured upstream branch.`);
667
+ }
668
+ const remote = remoteResult.stdout.trim();
669
+ const mergeRef = mergeResult.stdout.trim();
670
+ const upstreamResult = runGit(["-C", repoPath, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
671
+ if (upstreamResult.status !== 0 || !upstreamResult.stdout.trim()) {
672
+ throw new Error(`Cannot read Git upstream for target "${target.source.name}".`);
673
+ }
674
+ const urlResult = runGit(["-C", repoPath, "remote", "get-url", remote]);
675
+ if (urlResult.status !== 0 || !urlResult.stdout.trim()) {
676
+ throw new Error(`Cannot read Git remote "${remote}" for target "${target.source.name}".`);
677
+ }
678
+ const pushUrlsResult = runGit(["-C", repoPath, "remote", "get-url", "--push", "--all", remote]);
679
+ if (pushUrlsResult.status !== 0 || !pushUrlsResult.stdout.trim()) {
680
+ throw new Error(`Cannot read Git push URL for target "${target.source.name}".`);
681
+ }
682
+ return {
683
+ repoPath,
684
+ branch,
685
+ remote,
686
+ mergeRef,
687
+ remoteUrl: urlResult.stdout.trim(),
688
+ pushUrls: pushUrlsResult.stdout
689
+ .split("\n")
690
+ .map((value) => value.trim())
691
+ .filter(Boolean),
692
+ upstream: upstreamResult.stdout.trim(),
693
+ };
694
+ }
695
+ function assertGitPublicationIdentity(target, publication) {
696
+ if (target.source.kind !== "git") {
697
+ throw new Error(`Git publication is bound to non-Git target "${target.source.name}".`);
698
+ }
699
+ const current = readGitPublicationIdentity(target);
700
+ for (const key of ["repoPath", "branch", "remote", "mergeRef", "remoteUrl", "upstream"]) {
701
+ if (current[key] !== publication[key]) {
702
+ throw new Error(`Git publication target identity changed at ${key} for "${target.source.name}".`);
703
+ }
704
+ }
705
+ if (JSON.stringify(current.pushUrls ?? []) !== JSON.stringify(publication.pushUrls ?? [])) {
706
+ throw new Error(`Git publication target identity changed at pushUrls for "${target.source.name}".`);
707
+ }
708
+ }
709
+ function normalizePublicationPaths(paths) {
710
+ const normalized = new Set();
711
+ for (const filePath of paths) {
712
+ const candidate = filePath.replaceAll(path.sep, "/");
713
+ if (!candidate ||
714
+ candidate.includes("\0") ||
715
+ path.isAbsolute(filePath) ||
716
+ path.posix.isAbsolute(candidate) ||
717
+ candidate === "." ||
718
+ candidate === ".." ||
719
+ candidate.startsWith("../") ||
720
+ path.posix.normalize(candidate) !== candidate) {
721
+ throw new Error("Git publication contains an unsafe path.");
722
+ }
723
+ normalized.add(candidate);
724
+ }
725
+ return [...normalized];
726
+ }
727
+ function readGitHead(repoPath, targetName) {
728
+ const result = runGit(["-C", repoPath, "rev-parse", "HEAD"]);
729
+ if (result.status !== 0 || !result.stdout.trim())
730
+ throw new Error(`Cannot read Git HEAD for target "${targetName}".`);
731
+ return result.stdout.trim();
732
+ }
733
+ function findGitTransactionCommit(repoPath, baseHead, transactionId) {
734
+ const result = runGit([
735
+ "-C",
736
+ repoPath,
737
+ "log",
738
+ "--format=%H",
739
+ "--fixed-strings",
740
+ `--grep=AKM-Transaction: ${transactionId}`,
741
+ `${baseHead}..HEAD`,
742
+ ]);
743
+ if (result.status !== 0)
744
+ throw new Error(`Cannot inspect Git transaction ${transactionId}.`);
745
+ const matches = result.stdout
746
+ .split("\n")
747
+ .map((value) => value.trim())
748
+ .filter(Boolean);
749
+ if (matches.length > 1)
750
+ throw new Error(`Git transaction ${transactionId} has multiple candidate commits.`);
751
+ return matches[0];
752
+ }
753
+ function validateGitTransactionCommit(target, publication, commit, transactionId, expectedPaths, snapshots) {
754
+ const parent = runGit(["-C", publication.repoPath, "rev-list", "--parents", "-n", "1", commit]);
755
+ const ancestry = parent.stdout.trim().split(/\s+/);
756
+ if (parent.status !== 0 || ancestry.length !== 2 || ancestry[1] !== publication.baseHead) {
757
+ throw new Error(`Git commit ${commit} is not the direct transaction commit for "${target.source.name}".`);
758
+ }
759
+ const message = runGit(["-C", publication.repoPath, "show", "-s", "--format=%B", commit]);
760
+ if (message.status !== 0 ||
761
+ !message.stdout.split(/\r?\n/).some((line) => line.trim() === `AKM-Transaction: ${transactionId}`)) {
762
+ throw new Error(`Git commit ${commit} is not owned by transaction ${transactionId}.`);
763
+ }
764
+ const changed = runGit([
765
+ "-C",
766
+ publication.repoPath,
767
+ "diff-tree",
768
+ "--no-commit-id",
769
+ "--name-only",
770
+ "-z",
771
+ "--no-renames",
772
+ "-r",
773
+ commit,
774
+ ]);
775
+ const expected = new Set(expectedPaths);
776
+ const changedPaths = changed.stdout.split("\0").filter(Boolean);
777
+ if (changed.status !== 0 || changedPaths.length === 0 || changedPaths.some((filePath) => !expected.has(filePath))) {
778
+ throw new Error(`Git commit ${commit} contains paths outside transaction ${transactionId}.`);
779
+ }
780
+ validateGitCommitSnapshots(publication.repoPath, commit, expectedPaths, snapshots);
781
+ if (runGit(["-C", publication.repoPath, "merge-base", "--is-ancestor", commit, "HEAD"]).status !== 0) {
782
+ throw new Error(`Git commit ${commit} is no longer on the target branch.`);
783
+ }
784
+ }
785
+ function validateGitWorktreeSnapshots(target, expectedPaths, snapshots) {
786
+ for (const expectedPath of expectedPaths) {
787
+ if (!Object.hasOwn(snapshots, expectedPath)) {
788
+ throw new Error(`Git publication lacks a snapshot for ${expectedPath}.`);
789
+ }
790
+ const current = captureGitPathSnapshot(target, path.join(target.source.repoPath ?? target.source.path, expectedPath));
791
+ if (!sameGitPathState(current.state, snapshots[expectedPath] ?? null)) {
792
+ throw new Error(`Git publication path diverged after mutation: ${expectedPath}.`);
793
+ }
794
+ }
795
+ }
796
+ function validateGitCommitSnapshots(repoPath, commit, expectedPaths, snapshots) {
797
+ for (const expectedPath of expectedPaths) {
798
+ if (!Object.hasOwn(snapshots, expectedPath)) {
799
+ throw new Error(`Git publication lacks a snapshot for ${expectedPath}.`);
800
+ }
801
+ const expected = snapshots[expectedPath];
802
+ const tree = runGit(["-C", repoPath, "ls-tree", commit, "--", expectedPath]);
803
+ if (tree.status !== 0)
804
+ throw new Error(`Cannot inspect committed Git path: ${expectedPath}.`);
805
+ const match = tree.stdout.trim().match(/^(\d+)\s+blob\s+([0-9a-f]+)\t/);
806
+ if (expected === null) {
807
+ if (tree.stdout.trim())
808
+ throw new Error(`Git transaction unexpectedly retained ${expectedPath}.`);
809
+ }
810
+ else if (expected === undefined || !match || match[1] !== expected.mode || match[2] !== expected.oid) {
811
+ throw new Error(`Git transaction committed unexpected content for ${expectedPath}.`);
812
+ }
813
+ }
814
+ }
815
+ /**
816
+ * Validate and normalize a write target before a command mutates it.
817
+ *
818
+ * Git bundle locks record the materialized content root, which can be a
819
+ * subdirectory of the checkout. Resolve the actual repository boundary from
820
+ * that root so scoped commits use repository-relative paths. Extracted or
821
+ * unmaterialized Git caches are not writable checkouts and must fail before a
822
+ * command writes files into them.
823
+ */
824
+ export function prepareWriteTargetForMutation(target, options = {}) {
825
+ assertAkmAssetWrite(target.source, options.allowedAdapters);
826
+ if (target.source.kind !== "git")
827
+ return target;
828
+ const contentRoot = path.resolve(target.source.path);
829
+ let stat;
830
+ try {
831
+ stat = fs.statSync(contentRoot);
832
+ }
833
+ catch {
834
+ throw gitTargetNotMaterialized(target, contentRoot);
835
+ }
836
+ if (!stat.isDirectory())
837
+ throw gitTargetNotMaterialized(target, contentRoot);
838
+ const rootResult = runGit(["-C", contentRoot, "rev-parse", "--show-toplevel"]);
839
+ if (rootResult.status !== 0 || !rootResult.stdout.trim()) {
840
+ throw gitTargetNotMaterialized(target, contentRoot);
841
+ }
842
+ const repoPath = path.resolve(rootResult.stdout.trim());
843
+ const gitDirResult = runGit(["-C", repoPath, "rev-parse", "--git-dir"]);
844
+ if (gitDirResult.status !== 0 || !gitDirResult.stdout.trim()) {
845
+ throw gitTargetNotMaterialized(target, contentRoot);
846
+ }
847
+ const gitDir = path.resolve(repoPath, gitDirResult.stdout.trim());
848
+ let realContentRoot;
849
+ let realRepoPath;
850
+ try {
851
+ realContentRoot = fs.realpathSync(contentRoot);
852
+ realRepoPath = fs.realpathSync(repoPath);
853
+ fs.accessSync(realContentRoot, fs.constants.W_OK);
854
+ fs.accessSync(gitDir, fs.constants.W_OK);
855
+ }
856
+ catch {
857
+ throw new ConfigError(`Writable Git target "${target.source.name}" is not writable at ${contentRoot}.`, "INVALID_CONFIG_FILE", `Fix the checkout permissions or choose a different --target.`);
858
+ }
859
+ if (!isWithin(realContentRoot, realRepoPath)) {
860
+ throw new ConfigError(`Writable Git target "${target.source.name}" resolves outside its checkout: ${contentRoot}.`, "INVALID_CONFIG_FILE");
861
+ }
862
+ const statusResult = runGit(["-C", repoPath, "status", "--porcelain"]);
863
+ if (statusResult.status !== 0 || statusResult.error) {
864
+ throw gitTargetNotMaterialized(target, contentRoot);
865
+ }
866
+ const branchResult = runGit(["-C", repoPath, "symbolic-ref", "--quiet", "HEAD"]);
867
+ if (branchResult.status !== 0 || !branchResult.stdout.trim()) {
868
+ throw new UsageError(`Writable Git target "${target.source.name}" is detached from a branch.`, "INVALID_FLAG_VALUE");
869
+ }
870
+ const upstream = inspectGitUpstream(repoPath);
871
+ if (upstream.behind > 0) {
872
+ throw new UsageError(`Writable Git target "${target.source.name}" is behind ${upstream.upstream}; run \`akm bundle update ${target.source.name}\` before writing.`, "INVALID_FLAG_VALUE");
873
+ }
874
+ if (upstream.ahead > 0 && options.allowAhead !== true) {
875
+ throw new UsageError(`Writable Git target "${target.source.name}" has unpushed commits; push or reconcile them before AKM writes another commit.`, "INVALID_FLAG_VALUE");
876
+ }
877
+ return {
878
+ ...target,
879
+ source: { ...target.source, path: contentRoot, repoPath },
880
+ };
881
+ }
882
+ function gitTargetNotMaterialized(target, contentRoot) {
883
+ return new ConfigError(`Writable Git target "${target.source.name}" is not materialized as a Git checkout at ${contentRoot}; refusing to write without a commit boundary.`, "INVALID_CONFIG_FILE", `Run \`akm bundle update ${target.source.name}\` to materialize it, or point the bundle at a writable Git checkout.`);
235
884
  }
236
885
  /** Enumerate enabled writable targets, deduplicated by materialized content root. */
237
886
  export function resolveWritableTargets(akmConfig) {
@@ -245,17 +894,9 @@ export function resolveWritableTargets(akmConfig) {
245
894
  if (!existing || target.source.name === akmConfig.defaultWriteTarget)
246
895
  byRoot.set(root, target);
247
896
  }
248
- if (byRoot.size === 0) {
249
- try {
250
- const stashDir = resolveStashDir({ readOnly: true });
251
- byRoot.set(path.resolve(stashDir), {
252
- source: { kind: "filesystem", name: "stash", path: stashDir },
253
- config: { type: "filesystem", path: stashDir, name: "stash", writable: true },
254
- });
255
- }
256
- catch {
257
- // No active working stash; configured writable targets above are complete.
258
- }
897
+ if (process.env.AKM_BUNDLE_DIR?.trim()) {
898
+ const target = resolveWorkingStashTarget(akmConfig);
899
+ byRoot.set(path.resolve(target.source.path), target);
259
900
  }
260
901
  return [...byRoot.values()];
261
902
  }
@@ -264,8 +905,8 @@ export function resolveWritableTargets(akmConfig) {
264
905
  *
265
906
  * 1. Explicit `--target <name>` (when supplied)
266
907
  * 2. `config.defaultWriteTarget`
267
- * 3. `config.stashDir` (the working stash created by `akm init`)
268
- * 4. `ConfigError("no writable source configured; run `akm init`")`
908
+ * 3. `config.defaultBundle`'s path (the working stash created by `akm bundle create`)
909
+ * 4. `ConfigError("no writable source configured; run `akm bundle create`")`
269
910
  *
270
911
  * The legacy `first-writable-in-source-array-order` fallback is *not* used —
271
912
  * see plan §6 decision 3 for the rationale.
@@ -277,7 +918,7 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
277
918
  if (explicitTarget) {
278
919
  const match = configuredSources.find((s) => s.name === explicitTarget);
279
920
  if (!match) {
280
- throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm list\` to see available sources.`, "INVALID_FLAG_VALUE");
921
+ throw new UsageError(`--target must reference a source name from your config. No source named "${explicitTarget}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
281
922
  }
282
923
  // Up-front writable check so an explicit --target fails fast with a
283
924
  // ConfigError (rather than the generic UsageError ensureWritable would
@@ -306,28 +947,45 @@ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
306
947
  return adaptConfiguredSource(match);
307
948
  }
308
949
  // Fall through if the named target no longer exists — surface a clear error.
309
- throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" does not match any configured source.`, "INVALID_CONFIG_FILE", "Update `defaultWriteTarget` in your config (run `akm config get defaultWriteTarget`) or run `akm list` to see configured sources.");
310
- }
311
- // 3. Working stash (config.stashDir / resolveStashDir()).
312
- //
313
- // The primary stash stays `kind: "filesystem"` on purpose, even when it is a
314
- // git repo on disk (recognized elsewhere via isGitBackedStash). Returning
315
- // `kind: "git"` here would fire the boundary commit on every write through
316
- // this resolver, double-committing the primary stash which is already
317
- // committed in a single batch at operation boundaries (e.g. the end-of-run
318
- // improve auto-sync via saveGitStash). Per-write stays non-committing.
319
- try {
320
- const stashDir = resolveStashDir({ readOnly: true });
950
+ throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" does not match any configured source.`, "INVALID_CONFIG_FILE", "Update `defaultWriteTarget` in your config (run `akm config get defaultWriteTarget`) or run `akm bundle list` to see configured sources.");
951
+ }
952
+ // 3. Configured default bundle.
953
+ return resolveWorkingStashTarget(akmConfig, options);
954
+ }
955
+ /** Resolve the implicit working stash without consulting `defaultWriteTarget`. */
956
+ export function resolveWorkingStashTarget(akmConfig, options = {}) {
957
+ const configuredSources = resolveConfiguredSources(akmConfig);
958
+ const requireWritable = options.requireWritable !== false;
959
+ if (process.env.AKM_BUNDLE_DIR?.trim()) {
960
+ const stashDir = resolveStashDir();
961
+ const configured = configuredSources.find((source) => {
962
+ const sourcePath = source.source.type === "filesystem" ? source.source.path : undefined;
963
+ return sourcePath !== undefined && path.resolve(sourcePath) === path.resolve(stashDir);
964
+ });
965
+ if (configured) {
966
+ const target = adaptConfiguredSource(configured);
967
+ if (requireWritable && !resolveWritable(target.config)) {
968
+ throw new ConfigError(`Bundle "${configured.name}" is not writable.`, "INVALID_CONFIG_FILE");
969
+ }
970
+ return { ...target, selector: undefined };
971
+ }
972
+ const bundleId = deriveBundleId(undefined, stashDir, new Set(Object.keys(akmConfig.bundles ?? {})));
321
973
  return {
322
- source: { kind: "filesystem", name: "stash", path: stashDir },
323
- config: { type: "filesystem", path: stashDir, name: "stash", writable: true },
974
+ source: { kind: "filesystem", name: bundleId, path: stashDir, adapterId: detectAdapterId(stashDir) },
975
+ config: { type: "filesystem", name: bundleId, path: stashDir, writable: true },
324
976
  };
325
977
  }
326
- catch {
327
- // Fall through to the final ConfigError below.
978
+ const defaultBundleSource = akmConfig.defaultBundle
979
+ ? configuredSources.find((source) => source.name === akmConfig.defaultBundle)
980
+ : undefined;
981
+ if (!defaultBundleSource || !akmConfig.defaultBundle) {
982
+ throw new ConfigError("No default bundle is configured.", "INVALID_CONFIG_FILE");
328
983
  }
329
- // 4. Nothing usable.
330
- throw new ConfigError("no writable source configured; run `akm init`", "STASH_DIR_NOT_FOUND", "Run `akm init` to create a working stash, or set `defaultWriteTarget` in your config.");
984
+ const target = adaptConfiguredSource(defaultBundleSource);
985
+ if (requireWritable && !resolveWritable(target.config)) {
986
+ throw new ConfigError(`defaultBundle "${akmConfig.defaultBundle}" is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${akmConfig.defaultBundle}" bundle, or set \`defaultWriteTarget\` to a writable source.`);
987
+ }
988
+ return { ...target, selector: undefined };
331
989
  }
332
990
  // ── Internals ───────────────────────────────────────────────────────────────
333
991
  function ensureWritable(source, config) {
@@ -339,6 +997,10 @@ function ensureWritable(source, config) {
339
997
  }
340
998
  }
341
999
  function resolveAssetFilePath(source, ref) {
1000
+ const basename = path.posix.basename(ref.name.replaceAll("\\", "/")).replace(/\.md$/i, "").toLowerCase();
1001
+ if (basename === "index" || basename === "log") {
1002
+ throw new UsageError(`Reserved concept name "${basename}" cannot be written.`, "INVALID_FLAG_VALUE");
1003
+ }
342
1004
  const typeDir = stashDirFor(ref.type);
343
1005
  if (!typeDir) {
344
1006
  throw new UsageError(`Unknown asset type "${ref.type}". Cannot resolve a write path.`, "INVALID_FLAG_VALUE");
@@ -350,6 +1012,11 @@ function resolveAssetFilePath(source, ref) {
350
1012
  }
351
1013
  return assetPath;
352
1014
  }
1015
+ export function assertAkmAssetWrite(source, allowedAdapters = ["akm"]) {
1016
+ if (!source.adapterId || allowedAdapters.includes(source.adapterId))
1017
+ return;
1018
+ throw new UsageError(`Bundle "${source.name}" uses adapter "${source.adapterId}", which does not support AKM asset writes.`, "INVALID_FLAG_VALUE");
1019
+ }
353
1020
  /**
354
1021
  * Reject any kind reaching the write/delete helpers other than the two
355
1022
  * supported writable kinds. The config loader is the first line of defence
@@ -372,7 +1039,12 @@ export function formatRefForMessage(ref) {
372
1039
  const origin = ref.origin ? sanitizeCommitMessage(ref.origin) : "";
373
1040
  const type = sanitizeCommitMessage(ref.type);
374
1041
  const name = sanitizeCommitMessage(ref.name);
375
- return origin ? `${origin}//${type}:${name}` : `${type}:${name}`;
1042
+ // 0.9.0 (Q-02): the retired `type:name` colon grammar is gone — emit the
1043
+ // slash conceptId (`workflows/name`), qualified with `origin//` when the
1044
+ // ref carries one. Mirrors the `displayRef`/`conceptIdFromTypeName` rule
1045
+ // used elsewhere in this file (see `resolveAssetFilePath` callers above).
1046
+ const conceptId = conceptIdFromTypeName(type, name);
1047
+ return origin ? `${origin}//${conceptId}` : conceptId;
376
1048
  }
377
1049
  /**
378
1050
  * Derive a {@link WriteTargetSource} + persisted {@link SourceConfigEntry}
@@ -383,8 +1055,6 @@ export function formatRefForMessage(ref) {
383
1055
  * ConfiguredSource.name → WriteTargetSource.name
384
1056
  * ConfiguredSource.source.* → WriteTargetSource.path (via parseSourceSpec)
385
1057
  *
386
- * Legacy aliases (`context-hub`, `github`) have already been normalised to
387
- * `git` by the config loader, so this mapping is straightforward.
388
1058
  */
389
1059
  function adaptConfiguredSource(runtime) {
390
1060
  // Map the runtime kind to the write helper's `kind` discriminator. Only
@@ -401,18 +1071,23 @@ function adaptConfiguredSource(runtime) {
401
1071
  // root lives in the lock (`localRoot`), NOT the desired config. Resolve there
402
1072
  // FIRST — via the SAME shared resolver the indexer READ path uses — so a write
403
1073
  // lands in exactly the directory a read walks; git sync/commit then runs
404
- // against that same root. When no lock row records a localRoot (a git bundle
405
- // migrated from a `sources[]` url), fall back to the derived cache repoDir +
406
- // content/-subdir convention — the identical chain the read path applies.
1074
+ // against that same root. Before the first lock row exists, fall back to the
1075
+ // derived cache repoDir + content/-subdir convention used by the read path.
407
1076
  const lockRoot = kind === "git" ? lockContentRootFor(runtime.name, runtime.type) : undefined;
408
1077
  const repoPath = lockRoot ?? pathFromConfiguredSource(runtime);
409
1078
  if (!repoPath) {
410
1079
  throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
411
1080
  }
1081
+ const contentRoot = kind === "git" ? (lockRoot ?? resolveGitContentRoot(repoPath)) : repoPath;
1082
+ const componentRoot = path.resolve(contentRoot, runtime.componentRoot ?? ".");
1083
+ if (!isWithin(componentRoot, contentRoot)) {
1084
+ throw new ConfigError(`Component root "${runtime.componentRoot}" escapes bundle "${runtime.name}".`, "INVALID_CONFIG_FILE");
1085
+ }
1086
+ const adapterId = runtime.adapterId ?? detectAdapterId(componentRoot);
412
1087
  const config = {
413
1088
  type: runtime.type,
414
1089
  name: runtime.name,
415
- path: repoPath,
1090
+ path: componentRoot,
416
1091
  ...(runtime.writable !== undefined ? { writable: runtime.writable } : {}),
417
1092
  ...(runtime.options ? { options: runtime.options } : {}),
418
1093
  };
@@ -421,7 +1096,8 @@ function adaptConfiguredSource(runtime) {
421
1096
  source: {
422
1097
  kind,
423
1098
  name: runtime.name,
424
- path: kind === "git" ? (lockRoot ?? resolveGitContentRoot(repoPath)) : repoPath,
1099
+ path: componentRoot,
1100
+ adapterId,
425
1101
  ...(kind === "git" ? { repoPath } : {}),
426
1102
  },
427
1103
  config,