akm-cli 0.8.3 → 0.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/tasks/core/backup.yml +4 -0
  9. package/dist/assets/tasks/core/extract.yml +4 -0
  10. package/dist/assets/tasks/core/improve.yml +4 -0
  11. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  12. package/dist/assets/tasks/core/sync.yml +4 -0
  13. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  14. package/dist/assets/tasks/core/version-check.yml +4 -0
  15. package/dist/cli/config-migrate.js +6 -6
  16. package/dist/cli/config-validate.js +4 -4
  17. package/dist/cli/confirm.js +3 -3
  18. package/dist/cli/parse-args.js +1 -1
  19. package/dist/cli/shared.js +51 -14
  20. package/dist/cli-node.mjs +26 -0
  21. package/dist/cli.js +171 -3862
  22. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  23. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  24. package/dist/commands/agent/contribute-cli.js +200 -0
  25. package/dist/commands/completions.js +1 -1
  26. package/dist/commands/config-cli.js +240 -3
  27. package/dist/commands/config-edit.js +344 -0
  28. package/dist/commands/db-cli.js +2 -2
  29. package/dist/commands/env/env-cli.js +529 -0
  30. package/dist/commands/env/env.js +410 -0
  31. package/dist/commands/env/secret-cli.js +259 -0
  32. package/dist/commands/{secret.js → env/secret.js} +6 -47
  33. package/dist/commands/events.js +4 -4
  34. package/dist/commands/feedback-cli.js +18 -34
  35. package/dist/commands/graph/graph-cli.js +132 -0
  36. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  37. package/dist/commands/health/checks.js +279 -0
  38. package/dist/commands/health.js +94 -262
  39. package/dist/commands/{consolidate.js → improve/consolidate.js} +48 -36
  40. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  41. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  42. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  43. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  44. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  45. package/dist/commands/{extract.js → improve/extract.js} +185 -26
  46. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +4 -4
  47. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +44 -22
  48. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  49. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +1 -1
  50. package/dist/commands/{improve.js → improve/improve.js} +517 -253
  51. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  52. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  53. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  54. package/dist/commands/{reflect.js → improve/reflect.js} +33 -28
  55. package/dist/commands/improve/session-asset.js +248 -0
  56. package/dist/commands/lint/agent-linter.js +1 -1
  57. package/dist/commands/lint/base-linter.js +55 -37
  58. package/dist/commands/lint/command-linter.js +1 -1
  59. package/dist/commands/lint/default-linter.js +1 -1
  60. package/dist/commands/lint/env-key-rules.js +1 -1
  61. package/dist/commands/lint/index.js +19 -25
  62. package/dist/commands/lint/knowledge-linter.js +1 -1
  63. package/dist/commands/lint/memory-linter.js +1 -1
  64. package/dist/commands/lint/registry.js +8 -8
  65. package/dist/commands/lint/skill-linter.js +1 -1
  66. package/dist/commands/lint/task-linter.js +1 -1
  67. package/dist/commands/lint/workflow-linter.js +1 -1
  68. package/dist/commands/lint.js +1 -1
  69. package/dist/commands/observability-cli.js +244 -0
  70. package/dist/commands/proposal/drain-policies.js +3 -3
  71. package/dist/commands/proposal/drain.js +15 -10
  72. package/dist/commands/proposal/proposal-cli.js +478 -0
  73. package/dist/commands/{proposal.js → proposal/proposal.js} +5 -5
  74. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  75. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  76. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  77. package/dist/{core → commands/proposal/validators}/proposals.js +13 -7
  78. package/dist/commands/{curate.js → read/curate.js} +7 -7
  79. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  80. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  81. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  82. package/dist/commands/read/search-cli.js +207 -0
  83. package/dist/commands/{search.js → read/search.js} +22 -27
  84. package/dist/commands/{show.js → read/show.js} +31 -45
  85. package/dist/commands/registry-cli.js +8 -8
  86. package/dist/commands/remember.js +8 -8
  87. package/dist/commands/sources/add-cli.js +293 -0
  88. package/dist/commands/{history.js → sources/history.js} +27 -25
  89. package/dist/commands/{info.js → sources/info.js} +6 -6
  90. package/dist/commands/{init.js → sources/init.js} +6 -6
  91. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  92. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  93. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  94. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  95. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  96. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  97. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  98. package/dist/commands/sources/sources-cli.js +305 -0
  99. package/dist/commands/sources/stash-cli.js +219 -0
  100. package/dist/commands/{stash-skeleton.js → sources/stash-skeleton.js} +2 -1
  101. package/dist/commands/tasks/default-tasks.js +173 -0
  102. package/dist/commands/tasks/tasks-cli.js +210 -0
  103. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  104. package/dist/commands/wiki-cli.js +307 -0
  105. package/dist/commands/workflow-cli.js +329 -0
  106. package/dist/core/action-contributors.js +1 -1
  107. package/dist/core/assert.js +40 -0
  108. package/dist/core/asset/asset-create.js +54 -0
  109. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  110. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  111. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  112. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  113. package/dist/core/{stash-meta.js → asset/stash-meta.js} +1 -1
  114. package/dist/core/best-effort.js +64 -0
  115. package/dist/core/common.js +32 -18
  116. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  117. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  118. package/dist/core/{config-schema.js → config/config-schema.js} +45 -1
  119. package/dist/core/config/config-types.js +16 -0
  120. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  121. package/dist/core/{config.js → config/config.js} +10 -8
  122. package/dist/core/env-secret-ref.js +90 -0
  123. package/dist/core/errors.js +13 -3
  124. package/dist/core/events.js +27 -4
  125. package/dist/core/file-lock.js +1 -1
  126. package/dist/core/improve-types.js +48 -0
  127. package/dist/core/lesson-lint.js +2 -2
  128. package/dist/core/paths.js +2 -2
  129. package/dist/core/ripgrep/install.js +2 -2
  130. package/dist/core/ripgrep/resolve.js +2 -2
  131. package/dist/core/state-db.js +88 -46
  132. package/dist/core/text-truncation.js +148 -0
  133. package/dist/core/time.js +1 -1
  134. package/dist/core/write-source.js +98 -85
  135. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  136. package/dist/indexer/{db.js → db/db.js} +126 -116
  137. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  138. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  139. package/dist/indexer/ensure-index.js +4 -4
  140. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  141. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  142. package/dist/indexer/indexer.js +37 -30
  143. package/dist/indexer/init.js +54 -0
  144. package/dist/indexer/manifest.js +10 -10
  145. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +92 -23
  146. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  147. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  148. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  149. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  150. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  151. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  152. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  153. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  154. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  155. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  156. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  157. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  158. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  159. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  160. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  161. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  162. package/dist/integrations/agent/builder-shared.js +39 -0
  163. package/dist/integrations/agent/builders.js +14 -81
  164. package/dist/integrations/agent/config.js +6 -4
  165. package/dist/integrations/agent/detect.js +1 -1
  166. package/dist/integrations/agent/index.js +23 -8
  167. package/dist/integrations/agent/prompts.js +2 -3
  168. package/dist/integrations/agent/runner.js +22 -3
  169. package/dist/integrations/agent/spawn.js +9 -10
  170. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  171. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  172. package/dist/integrations/harnesses/claude/index.js +64 -0
  173. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +16 -1
  174. package/dist/integrations/harnesses/index.js +144 -0
  175. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  176. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  177. package/dist/integrations/harnesses/opencode/index.js +59 -0
  178. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  179. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  180. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  181. package/dist/integrations/harnesses/types.js +43 -0
  182. package/dist/integrations/lockfile.js +7 -16
  183. package/dist/integrations/session-logs/index.js +82 -9
  184. package/dist/llm/call-ai.js +4 -4
  185. package/dist/llm/client.js +131 -6
  186. package/dist/llm/embedder.js +6 -6
  187. package/dist/llm/embedders/local.js +9 -22
  188. package/dist/llm/embedders/remote.js +2 -2
  189. package/dist/llm/embedders/types.js +1 -1
  190. package/dist/llm/graph-extract.js +31 -12
  191. package/dist/llm/index-passes.js +1 -1
  192. package/dist/llm/memory-infer.js +12 -5
  193. package/dist/llm/metadata-enhance.js +2 -2
  194. package/dist/output/context.js +6 -44
  195. package/dist/output/renderers.js +88 -58
  196. package/dist/output/shapes/curate.js +7 -3
  197. package/dist/output/shapes/distill.js +7 -3
  198. package/dist/output/shapes/env-list.js +18 -16
  199. package/dist/output/shapes/events.js +5 -4
  200. package/dist/output/shapes/helpers.js +2 -4
  201. package/dist/output/shapes/history.js +7 -3
  202. package/dist/output/shapes/passthrough.js +8 -11
  203. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  204. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  205. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  206. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  207. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  208. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  209. package/dist/output/shapes/registry-search.js +7 -3
  210. package/dist/output/shapes/registry.js +12 -0
  211. package/dist/output/shapes/search.js +7 -3
  212. package/dist/output/shapes/secret-list.js +18 -16
  213. package/dist/output/shapes/show.js +7 -3
  214. package/dist/output/shapes.js +55 -30
  215. package/dist/output/text/add.js +2 -3
  216. package/dist/output/text/clone.js +2 -3
  217. package/dist/output/text/config.js +2 -3
  218. package/dist/output/text/curate.js +4 -3
  219. package/dist/output/text/distill.js +2 -3
  220. package/dist/output/text/enable-disable.js +5 -4
  221. package/dist/output/text/env.js +13 -0
  222. package/dist/output/text/events.js +5 -4
  223. package/dist/output/text/feedback.js +4 -3
  224. package/dist/output/text/helpers.js +54 -39
  225. package/dist/output/text/history.js +2 -3
  226. package/dist/output/text/import.js +2 -3
  227. package/dist/output/text/index.js +2 -3
  228. package/dist/output/text/info.js +2 -3
  229. package/dist/output/text/init.js +2 -3
  230. package/dist/output/text/list.js +2 -3
  231. package/dist/output/text/proposal/producer.js +9 -0
  232. package/dist/output/text/proposal/proposal.js +13 -0
  233. package/dist/output/text/registry-commands.js +8 -7
  234. package/dist/output/text/registry.js +12 -0
  235. package/dist/output/text/remember.js +4 -3
  236. package/dist/output/text/remove.js +2 -3
  237. package/dist/output/text/save.js +2 -3
  238. package/dist/output/text/search.js +4 -3
  239. package/dist/output/text/show.js +4 -3
  240. package/dist/output/text/update.js +2 -3
  241. package/dist/output/text/upgrade.js +2 -3
  242. package/dist/output/text/wiki.js +12 -11
  243. package/dist/output/text/workflow.js +12 -10
  244. package/dist/output/text.js +66 -32
  245. package/dist/registry/build-index.js +11 -10
  246. package/dist/registry/factory.js +1 -1
  247. package/dist/registry/origin-resolve.js +1 -1
  248. package/dist/registry/providers/index.js +2 -2
  249. package/dist/registry/providers/skills-sh.js +91 -72
  250. package/dist/registry/providers/static-index.js +75 -52
  251. package/dist/registry/resolve.js +3 -3
  252. package/dist/runtime.js +242 -0
  253. package/dist/scripts/migrate-storage.js +1594 -673
  254. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +240 -166
  255. package/dist/setup/detect.js +311 -9
  256. package/dist/setup/harness-config-import.js +6 -120
  257. package/dist/setup/setup.js +454 -43
  258. package/dist/sources/include.js +1 -1
  259. package/dist/sources/provider-factory.js +2 -2
  260. package/dist/sources/providers/filesystem.js +3 -3
  261. package/dist/sources/providers/git.js +9 -9
  262. package/dist/sources/providers/index.js +4 -4
  263. package/dist/sources/providers/npm.js +6 -6
  264. package/dist/sources/providers/provider-utils.js +13 -20
  265. package/dist/sources/providers/sync-from-ref.js +5 -5
  266. package/dist/sources/providers/tar-utils.js +2 -2
  267. package/dist/sources/providers/website.js +2 -2
  268. package/dist/sources/resolve.js +5 -5
  269. package/dist/sources/website-ingest.js +5 -5
  270. package/dist/storage/database.js +102 -0
  271. package/dist/storage/engines/sqlite-migrations.js +42 -0
  272. package/dist/storage/locations.js +25 -0
  273. package/dist/storage/repositories/index-db.js +43 -0
  274. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  275. package/dist/tasks/backends/cron.js +4 -4
  276. package/dist/tasks/backends/exec-utils.js +32 -0
  277. package/dist/tasks/backends/index.js +3 -3
  278. package/dist/tasks/backends/launchd.js +7 -14
  279. package/dist/tasks/backends/schtasks.js +7 -16
  280. package/dist/tasks/embedded.js +71 -0
  281. package/dist/tasks/parser.js +2 -2
  282. package/dist/tasks/resolveAkmBin.js +1 -1
  283. package/dist/tasks/runner.js +28 -15
  284. package/dist/tasks/schedule.js +1 -1
  285. package/dist/tasks/validator.js +7 -7
  286. package/dist/text-import-hook.mjs +51 -0
  287. package/dist/version.js +2 -1
  288. package/dist/wiki/wiki.js +7 -7
  289. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  290. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  291. package/dist/workflows/cli.js +1 -1
  292. package/dist/workflows/db.js +50 -32
  293. package/dist/workflows/parser.js +4 -4
  294. package/dist/workflows/renderer.js +5 -5
  295. package/dist/workflows/runtime/agent-identity.js +56 -0
  296. package/dist/workflows/runtime/checkin.js +57 -0
  297. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  298. package/dist/workflows/validate-summary.js +82 -0
  299. package/docs/README.md +1 -1
  300. package/docs/data-and-telemetry.md +6 -6
  301. package/package.json +16 -8
  302. package/dist/commands/add-cli.js +0 -279
  303. package/dist/commands/env.js +0 -213
  304. package/dist/integrations/agent/sdk-runner.js +0 -126
  305. package/dist/output/shapes/vault-list.js +0 -19
  306. package/dist/output/text/proposal-producer.js +0 -8
  307. package/dist/output/text/proposal.js +0 -12
  308. package/dist/output/text/vault.js +0 -16
  309. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  310. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  311. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  312. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  313. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  314. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  315. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  316. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -105,3 +105,151 @@ export function detectTruncatedDescription(description) {
105
105
  }
106
106
  return null;
107
107
  }
108
+ // ── Post-generation repair pass (issue #556) ─────────────────────────────────
109
+ /**
110
+ * Minimum length (chars) for a repaired description to be considered usable.
111
+ * Mirrors the floor in `isValidDescription` (≥20). Kept local so this module
112
+ * stays dependency-free of the validators (avoids an import cycle).
113
+ */
114
+ const MIN_REPAIRED_DESCRIPTION_LEN = 20;
115
+ /** Maximum length (chars) for a repaired description. Mirrors `isValidDescription` (≤400). */
116
+ const MAX_REPAIRED_DESCRIPTION_LEN = 400;
117
+ /**
118
+ * Strip trailing truncation fragments from a candidate clause: repeatedly drop
119
+ * a hanging-connector word and/or trailing `,` `;` `:` `+` and ellipses until
120
+ * the clause no longer looks truncated. PURE — no fabrication, only removal.
121
+ *
122
+ * Returns the trimmed-to-last-complete-token clause (may be empty if the whole
123
+ * thing was connectors/punctuation).
124
+ */
125
+ function stripTrailingTruncationFragment(clause) {
126
+ let s = clause.trim();
127
+ // Loop because a tail can stack: "… related to the" → drop "the" → drop "to".
128
+ for (let guard = 0; guard < 64; guard++) {
129
+ const before = s;
130
+ // Drop trailing ellipsis / connector-only punctuation+operators.
131
+ s = s.replace(/\s*(\.{3,}|…)$/u, "").trim();
132
+ s = s.replace(/\s*[,;:+]+$/u, "").trim();
133
+ // Drop a trailing hanging-connector word (and any punctuation glued to it).
134
+ const m = s.match(/(?:^|\s)([A-Za-z']+)[.!?]*$/u);
135
+ if (m) {
136
+ const word = (m[1] ?? "").toLowerCase();
137
+ if (TRUNCATION_TRAILING_WORDS.has(word)) {
138
+ // Remove just the final word token (keep preceding text).
139
+ s = s.slice(0, s.length - m[0].length).trim();
140
+ }
141
+ }
142
+ if (s === before)
143
+ break;
144
+ }
145
+ return s;
146
+ }
147
+ /**
148
+ * Split prose into sentences on `.`/`!`/`?` boundaries (followed by whitespace
149
+ * or end). Keeps the terminating punctuation. Deliberately simple — good enough
150
+ * for the short, single-paragraph descriptions/bodies this repair operates on.
151
+ */
152
+ function splitSentences(text) {
153
+ const out = [];
154
+ for (const match of text.matchAll(/[^.!?]+[.!?]+(?=\s|$)|[^.!?]+$/gu)) {
155
+ const s = match[0].trim();
156
+ if (s)
157
+ out.push(s);
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * True when `candidate` is a self-contained, non-truncated clause of acceptable
163
+ * length. Used to gate repair outputs without importing the full validator
164
+ * (which would create a cycle). The full `isValidDescription` still runs
165
+ * downstream — this is only the truncation/length subset the repair targets.
166
+ */
167
+ function isUsableClause(candidate) {
168
+ const c = candidate.trim();
169
+ if (c.length < MIN_REPAIRED_DESCRIPTION_LEN)
170
+ return false;
171
+ if (c.length > MAX_REPAIRED_DESCRIPTION_LEN)
172
+ return false;
173
+ if (detectTruncatedDescription(c) !== null)
174
+ return false;
175
+ // Must contain at least one word character (not pure punctuation).
176
+ if (!/[A-Za-z0-9]/.test(c))
177
+ return false;
178
+ return true;
179
+ }
180
+ /** Append a period when the clause lacks terminal sentence punctuation. PURE. */
181
+ function ensureTerminalPunctuation(clause) {
182
+ const c = clause.trim();
183
+ if (/[.!?]$/.test(c))
184
+ return c;
185
+ return `${c}.`;
186
+ }
187
+ /**
188
+ * Deterministically repair an LLM-generated description that was sliced
189
+ * mid-sentence (issue #556). The repair NEVER fabricates new claims:
190
+ *
191
+ * 1. If the description is not truncated, it is returned **byte-identical**
192
+ * (zero behaviour change for already-valid descriptions).
193
+ * 2. Otherwise:
194
+ * a. **trim-to-last-complete-clause** — strip the trailing
195
+ * truncation-indicator word(s)/punctuation; if a complete earlier
196
+ * sentence survives, use the longest non-truncated prefix.
197
+ * b. **swap-in first body sentence** — if (a) yields nothing usable and a
198
+ * `body` is provided, use the first clean, complete sentence of the
199
+ * body.
200
+ * c. **fallback** — if neither produces a usable, non-truncated clause,
201
+ * return the original string unchanged so the existing validation
202
+ * rejects it exactly as before (no regression, no fabrication).
203
+ *
204
+ * @param description The raw description (possibly truncated).
205
+ * @param body Optional asset body to source a clean completion sentence from.
206
+ */
207
+ export function repairTruncatedDescription(description, body) {
208
+ if (typeof description !== "string")
209
+ return description;
210
+ // Guarantee: untruncated input passes through byte-identical.
211
+ if (detectTruncatedDescription(description) === null)
212
+ return description;
213
+ const trimmed = description.trim();
214
+ // (a) trim-to-last-complete-clause.
215
+ // First, try the whole string with its trailing fragment stripped.
216
+ const stripped = stripTrailingTruncationFragment(trimmed);
217
+ if (isUsableClause(stripped)) {
218
+ return ensureTerminalPunctuation(stripped);
219
+ }
220
+ // If the description has multiple sentences, the truncation is in the last
221
+ // one — fall back to the longest leading run of complete sentences.
222
+ const sentences = splitSentences(trimmed);
223
+ if (sentences.length > 1) {
224
+ for (let take = sentences.length - 1; take >= 1; take--) {
225
+ const prefix = sentences.slice(0, take).join(" ").trim();
226
+ const cleaned = stripTrailingTruncationFragment(prefix);
227
+ if (isUsableClause(cleaned))
228
+ return ensureTerminalPunctuation(cleaned);
229
+ }
230
+ }
231
+ // (b) swap-in first clean, complete body sentence.
232
+ if (typeof body === "string" && body.trim().length > 0) {
233
+ const bodyText = body
234
+ .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "") // drop any frontmatter
235
+ .replace(/```[\s\S]*?```/g, " ") // drop fenced code
236
+ .replace(/`[^`]*`/g, " "); // drop inline code spans
237
+ for (const rawLine of bodyText.split(/\n/)) {
238
+ const line = rawLine
239
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
240
+ .replace(/\*([^*]+)\*/g, "$1")
241
+ .replace(/^[#*\->_\s]+/, "")
242
+ .trim();
243
+ if (!line)
244
+ continue;
245
+ if (/^[a-z_]+:\s/i.test(line))
246
+ continue; // skip yaml-ish leak lines
247
+ for (const sentence of splitSentences(line)) {
248
+ if (isUsableClause(sentence))
249
+ return ensureTerminalPunctuation(sentence);
250
+ }
251
+ }
252
+ }
253
+ // (c) fallback: return original unchanged — validation rejects as before.
254
+ return description;
255
+ }
package/dist/core/time.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * interpret the same set of formats (ISO-8601, epoch ms, plain date strings)
9
9
  * consistently without private re-implementations drifting apart.
10
10
  */
11
- import { UsageError } from "./errors";
11
+ import { UsageError } from "./errors.js";
12
12
  // ── Since-flag parsing ───────────────────────────────────────────────────────
13
13
  /**
14
14
  * Parse a user-supplied `--since` value and return an ISO-8601 timestamp
@@ -2,31 +2,34 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * write-source — the only place in the codebase that branches on `source.kind`.
5
+ * write-source — the command-layer helper that performs asset writes.
6
6
  *
7
- * v1 architecture spec §2.6 / §2.7 / §10 step 5: writing to a source is *not*
8
- * a SourceProvider interface concern. It's a small command-layer helper that
9
- * does a plain filesystem write, plus a git-specific commit (and optional
10
- * push) when the source is backed by a git working tree.
7
+ * v1 architecture spec §2.6 / §2.7 / §10 step 5 (amended for 0.9.0): writing to
8
+ * a source is *not* a SourceProvider interface concern. It's a small
9
+ * command-layer helper that does a plain filesystem write for **every** kind.
11
10
  *
12
- * If a third kind ever needs special write handling, it gets added here. For
13
- * v1 there are exactly two cases. Adding more parallel scoring systems for
14
- * different provider kinds is explicitly disallowed by CLAUDE.md.
11
+ * 0.9.0 amendment (issue #507): the per-asset git commit/push path is retired.
12
+ * `writeAssetToSource` / `deleteAssetFromSource` no longer branch on `kind` for
13
+ * commit behaviour they only ever touch the filesystem. Git-backed targets
14
+ * are committed in a SINGLE batch at the operation boundary via
15
+ * {@link commitWriteTargetBoundary} (which delegates to `saveGitStash`). This
16
+ * stages `.akm/` + sibling assets together as one complete commit instead of
17
+ * one noisy, incomplete commit per asset.
15
18
  *
16
- * This module is the **single dispatch point** for `kind`-branching write
17
- * logic. Callers (remember, import, source-add, etc.) MUST go through
18
- * `writeAssetToSource` / `deleteAssetFromSource` rather than re-inlining the
19
- * filesystem-write + git-commit dance.
19
+ * This module is still the **single dispatch point** for write/delete: callers
20
+ * (remember, import, source-add, etc.) MUST go through `writeAssetToSource` /
21
+ * `deleteAssetFromSource` rather than re-inlining a filesystem write, and they
22
+ * fire {@link commitWriteTargetBoundary} once after a batch of mutations to a
23
+ * writable git target.
20
24
  */
21
- import { spawnSync } from "node:child_process";
22
25
  import fs from "node:fs";
23
26
  import path from "node:path";
24
- import { getCachePaths, parseGitRepoUrl } from "../sources/providers/git";
25
- import { makeAssetRef } from "./asset-ref";
26
- import { resolveAssetPathFromName, TYPE_DIRS } from "./asset-spec";
27
- import { isWithin, resolveStashDir } from "./common";
28
- import { resolveConfiguredSources } from "./config";
29
- import { ConfigError, UsageError } from "./errors";
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";
30
+ import { isWithin, resolveStashDir } from "./common.js";
31
+ import { resolveConfiguredSources } from "./config/config.js";
32
+ import { ConfigError, UsageError } from "./errors.js";
30
33
  /**
31
34
  * Source kinds that the loader is allowed to mark `writable: true`. Anything
32
35
  * else is rejected at config load (per locked decision 4) — see
@@ -114,41 +117,91 @@ export function assertWritableAllowedForKind(entry) {
114
117
  * `ref`. Always:
115
118
  *
116
119
  * 1. Refuses if `config.writable` is not truthy (per §5.4).
117
- * 2. Performs a plain filesystem write to `path.join(source.path, …)`.
120
+ * 2. Rejects unsupported kinds (anything but `filesystem` / `git`).
121
+ * 3. Performs a plain filesystem write to `path.join(source.path, …)`.
118
122
  *
119
- * For sources of `kind === "git"`, additionally:
120
- *
121
- * 3. `git -C <path> add <file>`
122
- * 4. `git -C <path> commit -m "Update <ref>"`
123
- * 5. `git -C <path> push` when `config.options.pushOnCommit` is truthy.
124
- *
125
- * Any other `kind` reaching this helper is a configuration bug — the loader
126
- * rejects unsupported writable kinds — so we throw {@link ConfigError}.
123
+ * No commit runs here — for **every** kind. Git-backed targets are committed in
124
+ * one batch at the operation boundary via {@link commitWriteTargetBoundary}
125
+ * (0.9.0 amendment, issue #507). The caller fires that boundary commit once
126
+ * after a batch of mutations to a writable git target.
127
127
  */
128
128
  export async function writeAssetToSource(source, config, ref, content) {
129
129
  ensureWritable(source, config);
130
+ assertSupportedKind(source);
130
131
  const filePath = resolveAssetFilePath(source, ref);
131
132
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
132
133
  const normalized = content.endsWith("\n") ? content : `${content}\n`;
133
134
  fs.writeFileSync(filePath, normalized, "utf8");
134
- await runKindSpecificCommit(source, config, filePath, `Update ${formatRefForMessage(ref)}`);
135
135
  return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
136
136
  }
137
137
  /**
138
138
  * Delete the asset at `ref` from `source`. Symmetric to
139
- * {@link writeAssetToSource}: same writable check, same git-commit-and-push
140
- * convenience for `kind === "git"`.
139
+ * {@link writeAssetToSource}: same writable check, same unsupported-kind guard,
140
+ * a plain `unlink` with no commit. Git-backed targets are committed once at the
141
+ * operation boundary via {@link commitWriteTargetBoundary}.
141
142
  */
142
143
  export async function deleteAssetFromSource(source, config, ref) {
143
144
  ensureWritable(source, config);
145
+ assertSupportedKind(source);
144
146
  const filePath = resolveAssetFilePath(source, ref);
145
147
  if (!fs.existsSync(filePath)) {
146
148
  throw new UsageError(`Asset "${formatRefForMessage(ref)}" not found in source "${source.name}" (expected at ${filePath}).`, "MISSING_REQUIRED_ARGUMENT");
147
149
  }
148
150
  fs.unlinkSync(filePath);
149
- await runKindSpecificCommit(source, config, filePath, `Remove ${formatRefForMessage(ref)}`);
150
151
  return { path: filePath, ref: makeAssetRef(ref.type, ref.name, ref.origin) };
151
152
  }
153
+ /**
154
+ * Fire the one-shot batch-at-boundary commit for a resolved write target.
155
+ *
156
+ * 0.9.0 (issue #507): replaces the retired per-asset git commit. Callers invoke
157
+ * this EXACTLY ONCE after a batch of writes/deletes to a resolved write target.
158
+ * It is a no-op for any non-git target (plain filesystem sources and the
159
+ * primary stash stay non-committing here — the primary stash is committed by
160
+ * the existing improve auto-sync boundary).
161
+ *
162
+ * For a git target it delegates to `saveGitStash(name, message, writable, …)`,
163
+ * which stages `.akm/` + sibling assets together (`git add -A`), commits once,
164
+ * and pushes when the target is writable, has a remote, and `push !== false`.
165
+ *
166
+ * The push intent honours a deprecated `options.pushOnCommit` on the source
167
+ * config (mapped onto the batch push gate) when `push` is not explicitly set.
168
+ */
169
+ export function commitWriteTargetBoundary(target, message, options) {
170
+ if (target.source.kind !== "git")
171
+ return;
172
+ warnIfPushOnCommit(target.config);
173
+ // Map the deprecated per-asset `pushOnCommit` intent onto the batch push gate
174
+ // when the caller did not pass an explicit push toggle. `saveGitStash` still
175
+ // gates the actual push on writable + remote, so this only ever opts *in*.
176
+ const push = options?.push ?? (target.config.options?.pushOnCommit === true ? true : undefined);
177
+ const writable = resolveWritable(target.config);
178
+ // Commit against the already-resolved repo directory (target.source.path)
179
+ // rather than re-resolving the stash by name through config. The write helper
180
+ // resolved this exact path; the boundary commit must operate on the SAME
181
+ // directory so the staged batch matches what was just written.
182
+ saveGitStash(undefined, message, writable, {
183
+ repoDir: target.source.path,
184
+ ...(push === undefined ? {} : { push }),
185
+ });
186
+ }
187
+ /**
188
+ * Emit a one-time deprecation warning the first time a source config carrying
189
+ * `options.pushOnCommit` is encountered. The field still parses (for old
190
+ * configs) but its per-asset push-on-commit behaviour is retired; its intent is
191
+ * now honoured via the batch push gate (writable + remote + push toggle).
192
+ */
193
+ let pushOnCommitWarned = false;
194
+ function warnIfPushOnCommit(config) {
195
+ if (config.options?.pushOnCommit === undefined)
196
+ return;
197
+ if (pushOnCommitWarned)
198
+ return;
199
+ pushOnCommitWarned = true;
200
+ const label = config.name ? ` on source "${config.name}"` : "";
201
+ process.stderr.write(`warning: \`options.pushOnCommit\`${label} is deprecated (0.9.0) and no longer commits per asset. ` +
202
+ "akm now commits writes in a single batch at the operation boundary and pushes when the target is " +
203
+ "writable with a remote. Remove the option or rely on sync push instead.\n");
204
+ }
152
205
  /**
153
206
  * Resolve the destination for a write per locked decision 3:
154
207
  *
@@ -201,12 +254,10 @@ export function resolveWriteTarget(akmConfig, explicitTarget) {
201
254
  //
202
255
  // The primary stash stays `kind: "filesystem"` on purpose, even when it is a
203
256
  // git repo on disk (recognized elsewhere via isGitBackedStash). Returning
204
- // `kind: "git"` here would route every asset write through the per-asset
205
- // runGitCommit, which is INCOMPLETE (stages only the single asset file,
206
- // leaving .akm/proposals + other state dirty) and NOISY (one commit per
207
- // asset, ~25 per improve run). Recognition is decoupled: per-write stays
208
- // non-committing, and the primary stash is committed in a single batch at
209
- // operation boundaries (e.g. the end-of-run improve auto-sync via saveGitStash).
257
+ // `kind: "git"` here would fire the boundary commit on every write through
258
+ // this resolver, double-committing the primary stash which is already
259
+ // committed in a single batch at operation boundaries (e.g. the end-of-run
260
+ // improve auto-sync via saveGitStash). Per-write stays non-committing.
210
261
  try {
211
262
  const stashDir = resolveStashDir({ readOnly: true });
212
263
  return {
@@ -241,57 +292,19 @@ function resolveAssetFilePath(source, ref) {
241
292
  }
242
293
  return assetPath;
243
294
  }
244
- async function runKindSpecificCommit(source, config, filePath, message) {
245
- if (source.kind === "filesystem") {
246
- return; // No commit step.
247
- }
248
- if (source.kind === "git") {
249
- runGitCommit(source.path, filePath, message);
250
- if (config.options?.pushOnCommit) {
251
- runGitPush(source.path);
252
- }
295
+ /**
296
+ * Reject any kind reaching the write/delete helpers other than the two
297
+ * supported writable kinds. The config loader is the first line of defence
298
+ * (assertWritableAllowedForKind), but we throw here so external callers that
299
+ * bypass the loader still get a clear error.
300
+ */
301
+ function assertSupportedKind(source) {
302
+ if (source.kind === "filesystem" || source.kind === "git")
253
303
  return;
254
- }
255
- // Reject any other kind reaching the helper. The config loader is the
256
- // first line of defence (assertWritableAllowedForKind), but we throw here
257
- // so external callers that bypass the loader still get a clear error.
258
304
  throw new ConfigError(`write-source: unsupported kind "${source.kind}" for source "${source.name}". ` +
259
305
  "Writes are only defined for `filesystem` and `git` sources.", "INVALID_CONFIG_FILE", 'Set `kind: "filesystem"` (or `kind: "git"`) on the source, or add a parallel filesystem entry.');
260
306
  }
261
- function runGitCommit(repoDir, filePath, message) {
262
- // Stage the specific file rather than `add -A` so unrelated working-tree
263
- // changes don't get folded into the asset commit.
264
- const relPath = path.relative(repoDir, filePath) || filePath;
265
- const addResult = spawnSync("git", ["-C", repoDir, "add", "--", relPath], { encoding: "utf8" });
266
- if (addResult.status !== 0) {
267
- throw new Error(`git add failed: ${addResult.stderr?.trim() || "unknown error"}`);
268
- }
269
- // Defense in depth: sanitize the commit subject one more time at the spawn
270
- // boundary. Callers should already pass sanitized strings (via
271
- // formatRefForMessage / saveGitStash), but this guards against future
272
- // refactors that forget. Empty after sanitize falls back to a safe stub.
273
- const safeMessage = sanitizeCommitMessage(message) || "akm update";
274
- // Provide a fallback identity so fresh CI/test environments without
275
- // user.name/user.email configured can always commit.
276
- const commitResult = spawnSync("git", ["-C", repoDir, "-c", "user.name=akm", "-c", "user.email=akm@local", "commit", "-m", safeMessage], { encoding: "utf8" });
277
- if (commitResult.status !== 0) {
278
- // `nothing to commit` is a no-op success — the file may have matched the
279
- // existing tree exactly. Surface other errors verbatim.
280
- const stderr = commitResult.stderr ?? "";
281
- if (/nothing to commit|no changes added/i.test(stderr) ||
282
- /nothing to commit|no changes added/i.test(commitResult.stdout ?? "")) {
283
- return;
284
- }
285
- throw new Error(`git commit failed: ${stderr.trim() || "unknown error"}`);
286
- }
287
- }
288
- function runGitPush(repoDir) {
289
- const pushResult = spawnSync("git", ["-C", repoDir, "push"], { encoding: "utf8", timeout: 120_000 });
290
- if (pushResult.status !== 0) {
291
- throw new Error(`git push failed: ${pushResult.stderr?.trim() || "unknown error"}`);
292
- }
293
- }
294
- function formatRefForMessage(ref) {
307
+ export function formatRefForMessage(ref) {
295
308
  // Sanitize each component independently. `ref.origin` originates from user
296
309
  // config and could contain CR/LF that would otherwise be smuggled into the
297
310
  // commit subject and forge trailers downstream. `ref.type` and `ref.name`
@@ -4,7 +4,7 @@
4
4
  /**
5
5
  * MVP data-directory backup for AKM.
6
6
  *
7
- * The DB upgrade path in `src/indexer/db.ts` `handleVersionUpgrade()` is
7
+ * The DB upgrade path in `src/indexer/db/db.ts` `handleVersionUpgrade()` is
8
8
  * intentionally destructive: when `DB_VERSION` bumps and a stored DB is at an
9
9
  * older version, ~17 tables are dropped and recreated. Until 0.9.0 ships a
10
10
  * full migration framework, this MVP captures a recursive copy of the entire
@@ -27,7 +27,8 @@
27
27
  */
28
28
  import fs from "node:fs";
29
29
  import path from "node:path";
30
- import { warn } from "../core/warn";
30
+ import { bestEffort } from "../../core/best-effort.js";
31
+ import { warn } from "../../core/warn.js";
31
32
  /** Default reason recorded for backups that don't override it. */
32
33
  export const DEFAULT_BACKUP_REASON = "version-upgrade";
33
34
  /** Reason recorded for backups taken before the embedding-dim drop path. */
@@ -95,12 +96,9 @@ export function measureDataDirSize(dirPath) {
95
96
  stack.push(full);
96
97
  }
97
98
  else if (entry.isFile()) {
98
- try {
99
+ bestEffort(() => {
99
100
  total += fs.statSync(full).size;
100
- }
101
- catch {
102
- // File vanished between readdir and stat — ignore.
103
- }
101
+ }, "file vanished between readdir and stat");
104
102
  }
105
103
  }
106
104
  }
@@ -154,7 +152,7 @@ export function listBackups(dataDir) {
154
152
  let sizeBytes;
155
153
  let reason = DEFAULT_BACKUP_REASON;
156
154
  if (fs.existsSync(metaPath)) {
157
- try {
155
+ bestEffort(() => {
158
156
  const raw = fs.readFileSync(metaPath, "utf8");
159
157
  const parsed = JSON.parse(raw);
160
158
  if (typeof parsed.createdAt === "string")
@@ -167,10 +165,7 @@ export function listBackups(dataDir) {
167
165
  sizeBytes = parsed.sizeBytes;
168
166
  if (typeof parsed.reason === "string" && parsed.reason.length > 0)
169
167
  reason = parsed.reason;
170
- }
171
- catch {
172
- // Malformed metadata — fall back to filesystem-derived values.
173
- }
168
+ }, "malformed backup metadata — fall back to filesystem-derived values");
174
169
  }
175
170
  if (!createdAt) {
176
171
  try {
@@ -286,12 +281,7 @@ export function backupDataDir(opts) {
286
281
  catch (err) {
287
282
  warn("[akm] data dir backup failed — %s; upgrade will proceed without a snapshot", err instanceof Error ? err.message : String(err));
288
283
  // Best-effort cleanup of the partial copy so we don't litter the data dir.
289
- try {
290
- fs.rmSync(finalDest, { recursive: true, force: true });
291
- }
292
- catch {
293
- /* ignore */
294
- }
284
+ bestEffort(() => fs.rmSync(finalDest, { recursive: true, force: true }), "cleanup partial backup copy");
295
285
  return null;
296
286
  }
297
287
  const createdAt = now.toISOString();
@@ -369,12 +359,7 @@ function copyDataDirExcludingBackups(srcRoot, destRoot) {
369
359
  // dir occasionally carries symlinked source roots; following them
370
360
  // could explode the backup size unexpectedly.
371
361
  const target = fs.readlinkSync(srcPath);
372
- try {
373
- fs.symlinkSync(target, destPath);
374
- }
375
- catch {
376
- /* ignore — symlink creation can fail on Windows without admin */
377
- }
362
+ bestEffort(() => fs.symlinkSync(target, destPath), "symlink creation can fail on Windows without admin");
378
363
  }
379
364
  // Other entry types (block/character/fifo/socket) are silently skipped.
380
365
  }