akm-cli 0.9.0-beta.9 → 0.9.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/CHANGELOG.md +592 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -21
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +156 -155
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +3 -3
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1091
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1295 -1277
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +228 -605
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +54 -3
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +157 -10
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +28 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +485 -2764
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +37 -35
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +206 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +2 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -895
  138. package/dist/commands/read/curate.js +410 -111
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +19 -39
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +382 -62
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +18 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +132 -1126
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +259 -769
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +36 -92
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +18 -11
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +182 -204
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +34 -11
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2661 -2369
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/website.js +9 -5
  286. package/dist/sources/website-ingest.js +187 -29
  287. package/dist/sources/wiki-fetchers/registry.js +53 -0
  288. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  289. package/dist/storage/database.js +45 -10
  290. package/dist/storage/managed-db.js +82 -0
  291. package/dist/storage/repositories/canaries-repository.js +107 -0
  292. package/dist/storage/repositories/consolidation-repository.js +38 -0
  293. package/dist/storage/repositories/embeddings-repository.js +72 -0
  294. package/dist/storage/repositories/events-repository.js +187 -0
  295. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  296. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  297. package/dist/storage/repositories/index-db.js +14 -8
  298. package/dist/storage/repositories/proposals-repository.js +220 -0
  299. package/dist/storage/repositories/recombine-repository.js +213 -0
  300. package/dist/storage/repositories/registry-cache.js +93 -0
  301. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  302. package/dist/storage/repositories/task-history-repository.js +93 -0
  303. package/dist/storage/sqlite-pragmas.js +146 -0
  304. package/dist/tasks/backends/cron.js +1 -1
  305. package/dist/tasks/backends/index.js +9 -0
  306. package/dist/tasks/backends/launchd.js +1 -1
  307. package/dist/tasks/backends/schtasks.js +1 -1
  308. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  309. package/dist/tasks/runner.js +15 -13
  310. package/dist/text-import-hook.mjs +0 -0
  311. package/dist/wiki/wiki.js +52 -11
  312. package/dist/workflows/cli.js +1 -0
  313. package/dist/workflows/db.js +3 -4
  314. package/dist/workflows/runtime/runs.js +43 -118
  315. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  316. package/dist/workflows/validate-summary.js +2 -7
  317. package/docs/README.md +69 -18
  318. package/docs/data-and-telemetry.md +5 -4
  319. package/docs/migration/release-notes/0.7.0.md +1 -1
  320. package/docs/migration/release-notes/0.9.0.md +39 -0
  321. package/package.json +10 -10
  322. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  323. package/dist/commands/db-cli.js +0 -23
  324. package/dist/indexer/db/db-backup.js +0 -376
  325. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,248 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
7
+ import { resolveStashDir } from "../../core/common.js";
8
+ import { getSources, loadConfig } from "../../core/config/config.js";
9
+ import { UsageError } from "../../core/errors.js";
10
+ import { sanitizeCommitMessage } from "../../core/write-source.js";
11
+ import { runGit } from "./git-install.js";
12
+ import { getCachePaths, parseGitRepoUrl } from "./git-provider.js";
13
+ /**
14
+ * Recognize a stash directory as git-backed by the presence of a `.git` entry.
15
+ *
16
+ * Recognition is deliberately by `.git` presence — NOT by a configured remote.
17
+ * `akm init` git-inits the primary stash (see init.ts `ensureGitRepo`), so a
18
+ * freshly-initialized local stash with no remote is still git-backed. This is
19
+ * the single source of truth used both by `saveGitStash` (below) and by the
20
+ * end-of-run improve auto-sync gate.
21
+ */
22
+ export function isGitBackedStash(stashDir) {
23
+ return fs.existsSync(path.join(stashDir, ".git"));
24
+ }
25
+ /**
26
+ * Resolve the writable-override flag for an end-of-run / `akm sync` commit on
27
+ * the primary stash. Returns `true` when the root config explicitly marks the
28
+ * primary stash writable, otherwise `undefined` (leave the per-stash default
29
+ * untouched). Extracted so `akm sync`, `akm improve`'s end-of-run sync, and the
30
+ * CLI body all derive this identically instead of re-copying the expression.
31
+ */
32
+ export function resolveWritableOverride(config) {
33
+ return config.writable === true ? true : undefined;
34
+ }
35
+ /**
36
+ * Commit (and optionally push) local changes in a git-backed stash.
37
+ *
38
+ * Behaviour:
39
+ * - Not a git repo → skipped (no-op)
40
+ * - Git repo, no remote → commit only
41
+ * - Git repo, has remote, but stash is not writable → commit only
42
+ * - Git repo, has remote, stash is writable → commit + push
43
+ *
44
+ * When `name` is omitted the primary stash directory is used.
45
+ * When `message` is omitted a timestamp is used.
46
+ *
47
+ * `options.repoDir` overrides the primary-stash directory the commit targets
48
+ * (only honoured when `name` is omitted). Callers that already resolved the
49
+ * primary stash dir (e.g. `akm improve`'s end-of-run sync, whose pre-commit
50
+ * gate validates that exact directory) pass it here so the gate and the commit
51
+ * operate on the SAME directory instead of independently calling
52
+ * `resolveStashDir({ readOnly: true })`. When absent, behaviour is unchanged.
53
+ */
54
+ export function saveGitStash(name, message, writableOverride, options) {
55
+ // `push: false` (from `akm sync --no-push`) commits but never pushes, even
56
+ // when the stash is writable with a remote configured.
57
+ const allowPush = options?.push !== false;
58
+ const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
59
+ // Sanitize the user-supplied message: strip CR/LF/NUL, collapse whitespace,
60
+ // clamp length. An attacker can otherwise pass `--message "subject\n\n\
61
+ // Co-Authored-By: someone-else"` and forge trailers in the commit log.
62
+ // Empty result falls back to the timestamped default.
63
+ const sanitized = message ? sanitizeCommitMessage(message) : "";
64
+ const commitMessage = sanitized || `akm save ${timestamp}`;
65
+ let repoDir;
66
+ let writable = false;
67
+ if (name) {
68
+ const config = loadConfig();
69
+ const stash = findGitStashByTarget(getSources(config), name);
70
+ if (!stash)
71
+ throw new UsageError(`No git stash found with name "${name}"`);
72
+ if (stash.type !== "git") {
73
+ throw new UsageError(`Stash "${name}" is not a git stash (type: ${stash.type})`);
74
+ }
75
+ if (!stash.url)
76
+ throw new UsageError(`Stash "${name}" has no URL configured`);
77
+ const repo = parseGitRepoUrl(stash.url);
78
+ repoDir = getCachePaths(repo.canonicalUrl).repoDir;
79
+ writable = stash.writable === true;
80
+ }
81
+ else {
82
+ // Honour an explicit primary-stash dir override (keeps the improve gate and
83
+ // the commit on the same directory); otherwise resolve the default.
84
+ repoDir = options?.repoDir ?? resolveStashDir({ readOnly: true });
85
+ // Allow caller to override writable for the primary stash (e.g. from root config.writable)
86
+ if (writableOverride !== undefined) {
87
+ writable = writableOverride;
88
+ }
89
+ }
90
+ // No-op: not a git repo
91
+ if (!isGitBackedStash(repoDir)) {
92
+ return { committed: false, pushed: false, skipped: true, reason: "not a git repository", output: "" };
93
+ }
94
+ // Nothing to commit?
95
+ const statusResult = runGit(["-C", repoDir, "status", "--porcelain"]);
96
+ if (statusResult.error || statusResult.status !== 0) {
97
+ throw new Error(`git status failed: ${statusResult.error?.message || statusResult.stderr?.trim() || "unknown error"}`);
98
+ }
99
+ if (!statusResult.stdout.trim()) {
100
+ return { committed: false, pushed: false, skipped: false, output: "nothing to commit, working tree clean" };
101
+ }
102
+ // Scoped staging (#476 + the auto-sync incident): NEVER refuse akm's commit
103
+ // because unrelated non-akm files exist in the working tree. When the stash
104
+ // dir is shared with a non-akm project (stash root == project repo root), a
105
+ // blunt `git add -A` would sweep the user's unrelated WIP into the stash's
106
+ // remote. We avoid that by SCOPING what we stage, not by refusing the commit.
107
+ //
108
+ // Precedence:
109
+ // 1. Explicit modified-file list (`options.paths`) — stage exactly those.
110
+ // 2. Managed pathspecs (TYPE_DIRS values + `.akm`) that exist on disk —
111
+ // stages everything akm owns and, by construction, never stages non-akm
112
+ // WIP. This preserves the #476 protection WITHOUT refusing.
113
+ // 3. Last-resort `git add -A` — ONLY when neither an explicit list nor any
114
+ // managed pathspec can be resolved. This is the maintainer-approved
115
+ // "or all files if we cannot determine the exact file list" fallback and
116
+ // is the one (rare) path that could include unrelated non-akm files.
117
+ if (!stageScopedChanges(repoDir, options?.paths)) {
118
+ throw new Error(`git add failed while staging akm changes in ${repoDir}`);
119
+ }
120
+ // Nothing actually staged → don't create an empty commit. This happens when
121
+ // only non-akm files were dirty (precedence 2 staged nothing).
122
+ const stagedResult = runGit(["-C", repoDir, "diff", "--cached", "--quiet"]);
123
+ if (stagedResult.status === 0) {
124
+ return { committed: false, pushed: false, skipped: false, output: "nothing to commit" };
125
+ }
126
+ // Commit — supply fallback identity so fresh environments without
127
+ // user.name/user.email configured can always commit to the default stash.
128
+ const commitResult = runGit([
129
+ "-C",
130
+ repoDir,
131
+ "-c",
132
+ "user.name=akm",
133
+ "-c",
134
+ "user.email=akm@local",
135
+ "commit",
136
+ "-m",
137
+ commitMessage,
138
+ ]);
139
+ if (commitResult.status !== 0) {
140
+ throw new Error(`git commit failed: ${commitResult.stderr?.trim() || "unknown error"}`);
141
+ }
142
+ // Push only when there is a remote AND the stash is marked writable
143
+ const remoteResult = runGit(["-C", repoDir, "remote"]);
144
+ if (remoteResult.status !== 0) {
145
+ throw new Error(`git remote failed: ${remoteResult.stderr?.trim() || "unknown error"}`);
146
+ }
147
+ const hasRemote = remoteResult.stdout.trim().length > 0;
148
+ if (!hasRemote || !writable || !allowPush) {
149
+ return { committed: true, pushed: false, skipped: false, output: commitResult.stdout.trim() };
150
+ }
151
+ const pushResult = runGit(["-C", repoDir, "push"], { timeout: 120_000 });
152
+ if (pushResult.status !== 0) {
153
+ throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
154
+ }
155
+ return {
156
+ committed: true,
157
+ pushed: true,
158
+ skipped: false,
159
+ output: (commitResult.stdout + pushResult.stdout).trim() || "changes committed and pushed",
160
+ };
161
+ }
162
+ /**
163
+ * Stage akm's changes in `repoDir` using the scoped-staging precedence
164
+ * documented at the call site (#476). Returns `false` only when a `git add`
165
+ * subprocess fails; returns `true` otherwise (including when nothing matched —
166
+ * the caller then detects "nothing staged" via `git diff --cached --quiet`).
167
+ *
168
+ * @param paths Optional explicit repo-relative paths akm wrote this run. When
169
+ * provided and non-empty, exactly those are staged (chunked to stay under
170
+ * argv length limits). Otherwise we fall back to the managed pathspecs, and
171
+ * finally to `git add -A` only if no managed pathspec exists on disk.
172
+ */
173
+ function stageScopedChanges(repoDir, paths) {
174
+ // Precedence 1: explicit modified-file list.
175
+ const explicit = (paths ?? []).filter((p) => typeof p === "string" && p.length > 0);
176
+ if (explicit.length > 0) {
177
+ return addPathspecsChunked(repoDir, explicit);
178
+ }
179
+ // Precedence 2: managed pathspecs that exist on disk (TYPE_DIRS + `.akm`).
180
+ const managed = [...Object.values(TYPE_DIRS), ".akm"].filter((dir) => fs.existsSync(path.join(repoDir, dir)));
181
+ if (managed.length > 0) {
182
+ return addPathspecsChunked(repoDir, managed);
183
+ }
184
+ // Precedence 3 (last resort): nothing akm-managed resolved on disk. Stage
185
+ // everything. This is the ONLY branch that can include unrelated non-akm
186
+ // files — it is the explicit, maintainer-approved "or all files if we cannot
187
+ // determine the exact file list" fallback and should be rare/never in
188
+ // practice (a git-backed stash always has at least one managed subtree once
189
+ // akm has written to it).
190
+ const addAll = runGit(["-C", repoDir, "add", "-A"]);
191
+ return addAll.status === 0;
192
+ }
193
+ /**
194
+ * Run `git add -- <pathspec>...` in chunks so a very large path list never
195
+ * exceeds the OS argv-length limit. Each chunk must succeed.
196
+ */
197
+ function addPathspecsChunked(repoDir, pathspecs) {
198
+ const CHUNK = 500;
199
+ for (let i = 0; i < pathspecs.length; i += CHUNK) {
200
+ const chunk = pathspecs.slice(i, i + CHUNK);
201
+ const result = runGit(["-C", repoDir, "add", "--", ...chunk]);
202
+ if (result.status !== 0)
203
+ return false;
204
+ }
205
+ return true;
206
+ }
207
+ function findGitStashByTarget(stashes, target) {
208
+ return stashes.find((stash) => matchesGitStashTarget(stash, target));
209
+ }
210
+ function matchesGitStashTarget(stash, target) {
211
+ if (stash.type !== "git")
212
+ return false;
213
+ if (stash.name === target || stash.url === target)
214
+ return true;
215
+ if (!stash.url)
216
+ return false;
217
+ try {
218
+ const repo = parseGitRepoUrl(stash.url);
219
+ if (repo.canonicalUrl === target)
220
+ return true;
221
+ return buildGithubTargetAliases(repo.canonicalUrl).has(target);
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ }
227
+ function buildGithubTargetAliases(canonicalUrl) {
228
+ try {
229
+ const parsed = new URL(canonicalUrl);
230
+ if (parsed.hostname !== "github.com")
231
+ return new Set();
232
+ const segments = parsed.pathname.split("/").filter(Boolean);
233
+ if (segments.length < 2)
234
+ return new Set();
235
+ const owner = segments[0];
236
+ const repo = segments[1];
237
+ const aliases = new Set([`${owner}/${repo}`, `github:${owner}/${repo}`]);
238
+ if (segments[2] === "tree" && segments.length >= 4) {
239
+ const ref = segments.slice(3).join("/");
240
+ aliases.add(`${owner}/${repo}#${ref}`);
241
+ aliases.add(`github:${owner}/${repo}#${ref}`);
242
+ }
243
+ return aliases;
244
+ }
245
+ catch {
246
+ return new Set();
247
+ }
248
+ }