akm-cli 0.9.0-rc.0 → 0.9.0-rc.13

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -13,7 +13,7 @@
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 `.akm/` + sibling assets together as one complete commit instead of
16
+ * commits only operation-owned exact paths as one complete commit instead of
17
17
  * one noisy, incomplete commit per asset.
18
18
  *
19
19
  * This module is still the **single dispatch point** for write/delete: callers
@@ -24,12 +24,19 @@
24
24
  */
25
25
  import fs from "node:fs";
26
26
  import path from "node:path";
27
- import { getCachePaths, parseGitRepoUrl, saveGitStash } from "../sources/providers/git.js";
28
- import { makeAssetRef } from "./asset/asset-ref.js";
29
- import { resolveAssetPathFromName, TYPE_DIRS } from "./asset/asset-spec.js";
27
+ import { withAssetMutationLeaseSync } from "../indexer/index-writer-lock.js";
28
+ import { lockContentRootFor } from "../integrations/lockfile.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";
33
+ import { assetPathForName, stashDirFor } from "./asset/asset-placement.js";
34
+ import { conceptIdFromTypeName, displayRef } from "./asset/resolve-ref.js";
35
+ import { deriveBundleId } from "./bundle-id.js";
30
36
  import { isWithin, resolveStashDir } from "./common.js";
31
37
  import { resolveConfiguredSources } from "./config/config.js";
32
38
  import { ConfigError, UsageError } from "./errors.js";
39
+ import { sanitizeCommitMessage } from "./git-message.js";
33
40
  import { warn } from "./warn.js";
34
41
  /**
35
42
  * Source kinds that the loader is allowed to mark `writable: true`. Anything
@@ -37,49 +44,210 @@ import { warn } from "./warn.js";
37
44
  * {@link assertWritableAllowedForKind}.
38
45
  */
39
46
  const REJECTED_WRITABLE_KINDS = new Set(["website", "npm"]);
40
- /**
41
- * Maximum length of a sanitized git commit message. Git itself imposes no
42
- * fixed limit, but message strings come from refs and `--message` flags that
43
- * can be supplied by users or upstream config. A 4096-char clamp keeps audit
44
- * trails readable and prevents pathological payloads from bloating the log
45
- * stream a downstream consumer parses.
46
- */
47
- const COMMIT_MESSAGE_MAX_LENGTH = 4096;
48
- /**
49
- * Sanitize a string before passing it as `git commit -m <message>`.
50
- *
51
- * Defenses, in order:
52
- * 1. Strip NUL bytes (`\0`) — git rejects them anyway, but we never want
53
- * them in argv.
54
- * 2. Replace any CR/LF (`\r`, `\n`) and other ASCII control chars with a
55
- * single space. This collapses newline-injection attempts that would
56
- * otherwise turn a single-line commit subject into a forged trailer
57
- * block.
58
- * 3. Collapse runs of whitespace into a single space and trim.
59
- * 4. Clamp to {@link COMMIT_MESSAGE_MAX_LENGTH} characters.
60
- *
61
- * If the result is empty after sanitization the caller should substitute a
62
- * default — this helper returns `""` rather than throwing because not every
63
- * callsite has a sensible "invalid input" exit code, and "empty" is a
64
- * recoverable signal.
65
- */
66
- export function sanitizeCommitMessage(input) {
67
- if (typeof input !== "string")
68
- return "";
69
- // 1. Strip NULs outright.
70
- let out = input.replace(/\0/g, "");
71
- // 2. Replace CR/LF + other C0 control characters (0x00-0x1F, 0x7F) with a
72
- // space. Tab (0x09) is included intentionally — commit subjects should
73
- // be a single visual line.
74
- // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional sanitization
75
- out = out.replace(/[\x00-\x1F\x7F]/g, " ");
76
- // 3. Collapse whitespace runs and trim.
77
- out = out.replace(/\s+/g, " ").trim();
78
- // 4. Clamp length.
79
- if (out.length > COMMIT_MESSAGE_MAX_LENGTH) {
80
- out = out.slice(0, COMMIT_MESSAGE_MAX_LENGTH).trimEnd();
81
- }
82
- return out;
47
+ const pendingGitMutations = new Map();
48
+ const EMPTY_GIT_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
49
+ const GIT_PUSH_TIMEOUT_MS = 120_000;
50
+ function gitTargetKey(source) {
51
+ return `${path.resolve(source.repoPath ?? source.path)}\0${path.resolve(source.path)}`;
52
+ }
53
+ /** Record the exact post-mutation blob for the target's boundary commit. */
54
+ export function recordWriteTargetPath(source, filePath) {
55
+ if (source.kind !== "git")
56
+ return;
57
+ const repoDir = path.resolve(source.repoPath ?? source.path);
58
+ if (!isGitBackedStash(repoDir))
59
+ return;
60
+ const key = gitTargetKey(source);
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);
68
+ }
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;
82
+ const key = gitTargetKey(source);
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;
196
+ const repoDir = path.resolve(source.repoPath ?? source.path);
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);
83
251
  }
84
252
  // ── Portability advisory (review 13, D1) ────────────────────────────────────
85
253
  /**
@@ -157,10 +325,20 @@ export function assertWritableAllowedForKind(entry) {
157
325
  export async function writeAssetToSource(source, config, ref, content) {
158
326
  ensureWritable(source, config);
159
327
  assertSupportedKind(source);
328
+ assertAkmAssetWrite(source);
160
329
  const filePath = resolveAssetFilePath(source, ref);
161
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
162
- const normalized = content.endsWith("\n") ? content : `${content}\n`;
163
- fs.writeFileSync(filePath, normalized, "utf8");
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
+ }
164
342
  // Non-fatal portability advisory (review 13, D1): flag absolute host home
165
343
  // paths in the written content. These make the stash non-portable and leak
166
344
  // the local username. We warn AFTER the write so the advisory never blocks it.
@@ -169,7 +347,7 @@ export async function writeAssetToSource(source, config, ref, content) {
169
347
  warn(`warning: asset "${formatRefForMessage(ref)}" embeds absolute host path(s): ${hostPaths.join(", ")}. ` +
170
348
  "These make the stash non-portable and leak the local username — prefer $HOME or ~ relative references.");
171
349
  }
172
- return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
350
+ return { path: filePath, ref: displayRef({ type: ref.type, name: ref.name, bundleId: ref.origin }) };
173
351
  }
174
352
  /**
175
353
  * Delete the asset at `ref` from `source`. Symmetric to
@@ -180,12 +358,21 @@ export async function writeAssetToSource(source, config, ref, content) {
180
358
  export async function deleteAssetFromSource(source, config, ref) {
181
359
  ensureWritable(source, config);
182
360
  assertSupportedKind(source);
361
+ assertAkmAssetWrite(source);
183
362
  const filePath = resolveAssetFilePath(source, ref);
184
363
  if (!fs.existsSync(filePath)) {
185
364
  throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
186
365
  }
187
- fs.unlinkSync(filePath);
188
- return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
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
+ }
375
+ return { path: filePath, ref: displayRef({ type: ref.type, name: ref.name, bundleId: ref.origin }) };
189
376
  }
190
377
  /**
191
378
  * Fire the one-shot batch-at-boundary commit for a resolved write target.
@@ -196,67 +383,542 @@ export async function deleteAssetFromSource(source, config, ref) {
196
383
  * primary stash stay non-committing here — the primary stash is committed by
197
384
  * the existing improve auto-sync boundary).
198
385
  *
199
- * For a git target it delegates to `saveGitStash(name, message, writable, …)`,
200
- * which stages `.akm/` + sibling assets together (`git add -A`), commits once,
201
- * and pushes when the target is writable, has a remote, and `push !== false`.
386
+ * For a git target it delegates to `saveGitStash(name, message, writable, …)`
387
+ * with the exact paths recorded by the write/delete helpers (plus any explicit
388
+ * caller paths), commits once, and pushes when the target is writable, has a
389
+ * remote, and `push !== false`.
202
390
  *
203
- * The push intent honours a deprecated `options.pushOnCommit` on the source
204
- * config (mapped onto the batch push gate) when `push` is not explicitly set.
205
391
  */
206
392
  export function commitWriteTargetBoundary(target, message, options) {
207
393
  if (target.source.kind !== "git")
208
394
  return;
209
- warnIfPushOnCommit(target.config);
210
- // Map the deprecated per-asset `pushOnCommit` intent onto the batch push gate
211
- // when the caller did not pass an explicit push toggle. `saveGitStash` still
212
- // gates the actual push on writable + remote, so this only ever opts *in*.
213
- const push = options?.push ?? (target.config.options?.pushOnCommit === true ? true : undefined);
395
+ const push = options?.push;
214
396
  const writable = resolveWritable(target.config);
215
- // Commit against the already-resolved repo directory (target.source.path)
216
- // rather than re-resolving the stash by name through config. The write helper
217
- // resolved this exact path; the boundary commit must operate on the SAME
218
- // directory so the staged batch matches what was just written.
219
- saveGitStash(undefined, message, writable, {
220
- repoDir: target.source.path,
221
- ...(push === undefined ? {} : { push }),
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
+ }
436
+ // Assets may live under <repo>/content, but git synchronization always runs
437
+ // against the repository root.
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, {
551
+ paths,
552
+ push: false,
553
+ transactionId: options.transactionId,
554
+ expectedBaseHead: publication.baseHead,
555
+ expectedSnapshots: options.snapshots,
222
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);
223
582
  }
224
583
  /**
225
- * Emit a one-time deprecation warning the first time a source config carrying
226
- * `options.pushOnCommit` is encountered. The field still parses (for old
227
- * configs) but its per-asset push-on-commit behaviour is retired; its intent is
228
- * now honoured via the batch push gate (writable + remote + push toggle).
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.
229
598
  */
230
- let pushOnCommitWarned = false;
231
- function warnIfPushOnCommit(config) {
232
- 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)
233
626
  return;
234
- 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) {
235
631
  return;
236
- pushOnCommitWarned = true;
237
- const label = config.name ? ` on source "${config.name}"` : "";
238
- process.stderr.write(`warning: \`options.pushOnCommit\`${label} is deprecated (0.9.0) and no longer commits per asset. ` +
239
- "akm now commits writes in a single batch at the operation boundary and pushes when the target is " +
240
- "writable with a remote. Remove the option or rely on sync push instead.\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.`);
884
+ }
885
+ /** Enumerate enabled writable targets, deduplicated by materialized content root. */
886
+ export function resolveWritableTargets(akmConfig) {
887
+ const byRoot = new Map();
888
+ for (const runtime of resolveConfiguredSources(akmConfig)) {
889
+ if (runtime.enabled === false || !resolveWritable({ type: runtime.type, writable: runtime.writable }))
890
+ continue;
891
+ const target = adaptConfiguredSource(runtime);
892
+ const root = path.resolve(target.source.path);
893
+ const existing = byRoot.get(root);
894
+ if (!existing || target.source.name === akmConfig.defaultWriteTarget)
895
+ byRoot.set(root, target);
896
+ }
897
+ if (process.env.AKM_BUNDLE_DIR?.trim()) {
898
+ const target = resolveWorkingStashTarget(akmConfig);
899
+ byRoot.set(path.resolve(target.source.path), target);
900
+ }
901
+ return [...byRoot.values()];
241
902
  }
242
903
  /**
243
904
  * Resolve the destination for a write per locked decision 3:
244
905
  *
245
906
  * 1. Explicit `--target <name>` (when supplied)
246
907
  * 2. `config.defaultWriteTarget`
247
- * 3. `config.stashDir` (the working stash created by `akm init`)
248
- * 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`")`
249
910
  *
250
911
  * The legacy `first-writable-in-source-array-order` fallback is *not* used —
251
912
  * see plan §6 decision 3 for the rationale.
252
913
  */
253
- export function resolveWriteTarget(akmConfig, explicitTarget) {
914
+ export function resolveWriteTarget(akmConfig, explicitTarget, options = {}) {
254
915
  const configuredSources = resolveConfiguredSources(akmConfig);
916
+ const requireWritable = options.requireWritable !== false;
255
917
  // 1. Explicit --target wins.
256
918
  if (explicitTarget) {
257
919
  const match = configuredSources.find((s) => s.name === explicitTarget);
258
920
  if (!match) {
259
- 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");
260
922
  }
261
923
  // Up-front writable check so an explicit --target fails fast with a
262
924
  // ConfigError (rather than the generic UsageError ensureWritable would
@@ -264,7 +926,7 @@ export function resolveWriteTarget(akmConfig, explicitTarget) {
264
926
  // effective writable flag (filesystem defaults to true; everything else
265
927
  // defaults to false) so unset values are interpreted correctly.
266
928
  const effectiveWritable = resolveWritable({ type: match.type, writable: match.writable });
267
- if (!effectiveWritable) {
929
+ if (requireWritable && !effectiveWritable) {
268
930
  throw new ConfigError(`source ${explicitTarget} is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${explicitTarget}" source in your config, or pass --target to a different source.`);
269
931
  }
270
932
  return adaptConfiguredSource(match);
@@ -279,34 +941,51 @@ export function resolveWriteTarget(akmConfig, explicitTarget) {
279
941
  // ConfigError, rather than surfacing as a generic UsageError after
280
942
  // path-building has already begun.
281
943
  const effectiveWritable = resolveWritable({ type: match.type, writable: match.writable });
282
- if (!effectiveWritable) {
944
+ if (requireWritable && !effectiveWritable) {
283
945
  throw new ConfigError(`defaultWriteTarget "${akmConfig.defaultWriteTarget}" is not writable`, "INVALID_CONFIG_FILE", `Set \`writable: true\` on the "${akmConfig.defaultWriteTarget}" source in your config, or change \`defaultWriteTarget\` to a writable source.`);
284
946
  }
285
947
  return adaptConfiguredSource(match);
286
948
  }
287
949
  // Fall through if the named target no longer exists — surface a clear error.
288
- 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.");
289
- }
290
- // 3. Working stash (config.stashDir / resolveStashDir()).
291
- //
292
- // The primary stash stays `kind: "filesystem"` on purpose, even when it is a
293
- // git repo on disk (recognized elsewhere via isGitBackedStash). Returning
294
- // `kind: "git"` here would fire the boundary commit on every write through
295
- // this resolver, double-committing the primary stash which is already
296
- // committed in a single batch at operation boundaries (e.g. the end-of-run
297
- // improve auto-sync via saveGitStash). Per-write stays non-committing.
298
- try {
299
- 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 ?? {})));
300
973
  return {
301
- source: { kind: "filesystem", name: "stash", path: stashDir },
302
- 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 },
303
976
  };
304
977
  }
305
- catch {
306
- // 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");
983
+ }
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.`);
307
987
  }
308
- // 4. Nothing usable.
309
- 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.");
988
+ return { ...target, selector: undefined };
310
989
  }
311
990
  // ── Internals ───────────────────────────────────────────────────────────────
312
991
  function ensureWritable(source, config) {
@@ -318,17 +997,26 @@ function ensureWritable(source, config) {
318
997
  }
319
998
  }
320
999
  function resolveAssetFilePath(source, ref) {
321
- const typeDir = TYPE_DIRS[ref.type];
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
+ }
1004
+ const typeDir = stashDirFor(ref.type);
322
1005
  if (!typeDir) {
323
1006
  throw new UsageError(`Unknown asset type "${ref.type}". Cannot resolve a write path.`, "INVALID_FLAG_VALUE");
324
1007
  }
325
1008
  const typeRoot = path.join(source.path, typeDir);
326
- const assetPath = resolveAssetPathFromName(ref.type, typeRoot, ref.name);
1009
+ const assetPath = assetPathForName(ref.type, typeRoot, ref.name);
327
1010
  if (!isWithin(assetPath, typeRoot)) {
328
1011
  throw new UsageError(`Resolved asset path escapes its source: "${ref.name}" in source "${source.name}".`, "PATH_ESCAPE_VIOLATION");
329
1012
  }
330
1013
  return assetPath;
331
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
+ }
332
1020
  /**
333
1021
  * Reject any kind reaching the write/delete helpers other than the two
334
1022
  * supported writable kinds. The config loader is the first line of defence
@@ -351,7 +1039,12 @@ export function formatRefForMessage(ref) {
351
1039
  const origin = ref.origin ? sanitizeCommitMessage(ref.origin) : "";
352
1040
  const type = sanitizeCommitMessage(ref.type);
353
1041
  const name = sanitizeCommitMessage(ref.name);
354
- 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;
355
1048
  }
356
1049
  /**
357
1050
  * Derive a {@link WriteTargetSource} + persisted {@link SourceConfigEntry}
@@ -362,14 +1055,8 @@ export function formatRefForMessage(ref) {
362
1055
  * ConfiguredSource.name → WriteTargetSource.name
363
1056
  * ConfiguredSource.source.* → WriteTargetSource.path (via parseSourceSpec)
364
1057
  *
365
- * Legacy aliases (`context-hub`, `github`) have already been normalised to
366
- * `git` by the config loader, so this mapping is straightforward.
367
1058
  */
368
1059
  function adaptConfiguredSource(runtime) {
369
- const writePath = pathFromConfiguredSource(runtime);
370
- if (!writePath) {
371
- throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
372
- }
373
1060
  // Map the runtime kind to the write helper's `kind` discriminator. Only
374
1061
  // filesystem and git produce writable sources at v1; any other kind
375
1062
  // reaching this point is a config-loader bug (assertWritableAllowedForKind
@@ -380,18 +1067,47 @@ function adaptConfiguredSource(runtime) {
380
1067
  "Writes are only defined for `filesystem` and `git` sources.", "INVALID_CONFIG_FILE", 'Use `kind: "filesystem"` or `kind: "git"` for writable sources.');
381
1068
  }
382
1069
  const kind = runtime.type;
1070
+ // §10.2 lock-first (BEHAVIOR FIX): a managed git bundle's resolved content
1071
+ // root lives in the lock (`localRoot`), NOT the desired config. Resolve there
1072
+ // FIRST — via the SAME shared resolver the indexer READ path uses — so a write
1073
+ // lands in exactly the directory a read walks; git sync/commit then runs
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.
1076
+ const lockRoot = kind === "git" ? lockContentRootFor(runtime.name, runtime.type) : undefined;
1077
+ const repoPath = lockRoot ?? pathFromConfiguredSource(runtime);
1078
+ if (!repoPath) {
1079
+ throw new ConfigError(`Source "${runtime.name}" has no resolvable on-disk path; writes are unsupported for this entry.`, "INVALID_CONFIG_FILE");
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);
383
1087
  const config = {
384
1088
  type: runtime.type,
385
1089
  name: runtime.name,
386
- ...(writePath !== undefined ? { path: writePath } : {}),
1090
+ path: componentRoot,
387
1091
  ...(runtime.writable !== undefined ? { writable: runtime.writable } : {}),
388
1092
  ...(runtime.options ? { options: runtime.options } : {}),
389
1093
  };
390
1094
  return {
391
- source: { kind, name: runtime.name, path: writePath },
1095
+ selector: runtime.name,
1096
+ source: {
1097
+ kind,
1098
+ name: runtime.name,
1099
+ path: componentRoot,
1100
+ adapterId,
1101
+ ...(kind === "git" ? { repoPath } : {}),
1102
+ },
392
1103
  config,
393
1104
  };
394
1105
  }
1106
+ /** Resolve the asset root inside a git checkout while preserving root-layout repos. */
1107
+ export function resolveGitContentRoot(repoPath) {
1108
+ const contentPath = path.join(repoPath, "content");
1109
+ return fs.existsSync(contentPath) && fs.statSync(contentPath).isDirectory() ? contentPath : repoPath;
1110
+ }
395
1111
  function pathFromConfiguredSource(runtime) {
396
1112
  // ConfiguredSource.source is the parsed SourceSpec (filesystem|git|website|npm).
397
1113
  // For writable kinds we only ever care about a local on-disk path: filesystem