akm-cli 0.8.0-rc2 → 0.8.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 (313) hide show
  1. package/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
  2. package/README.md +22 -6
  3. package/SECURITY.md +93 -0
  4. package/dist/assets/help/help-accept.md +12 -0
  5. package/dist/assets/help/help-improve.md +81 -0
  6. package/dist/{commands → assets}/help/help-proposals.md +7 -4
  7. package/dist/assets/help/help-reject.md +11 -0
  8. package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
  9. package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
  10. package/dist/assets/profiles/default.json +15 -0
  11. package/dist/assets/profiles/graph-refresh.json +13 -0
  12. package/dist/assets/profiles/memory-focus.json +12 -0
  13. package/dist/assets/profiles/quick.json +15 -0
  14. package/dist/assets/profiles/thorough.json +15 -0
  15. package/dist/assets/prompts/extract-session.md +80 -0
  16. package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
  17. package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
  18. package/dist/cli/config-migrate.js +144 -0
  19. package/dist/cli/config-validate.js +39 -0
  20. package/dist/cli/confirm.js +73 -0
  21. package/dist/cli/parse-args.js +93 -3
  22. package/dist/cli/shared.js +129 -0
  23. package/dist/cli.js +2141 -1268
  24. package/dist/commands/add-cli.js +279 -0
  25. package/dist/commands/agent-dispatch.js +20 -12
  26. package/dist/commands/agent-support.js +11 -5
  27. package/dist/commands/completions.js +3 -0
  28. package/dist/commands/config-cli.js +129 -517
  29. package/dist/commands/consolidate.js +1557 -147
  30. package/dist/commands/curate.js +44 -3
  31. package/dist/commands/db-cli.js +23 -0
  32. package/dist/commands/distill-promotion-policy.js +5 -3
  33. package/dist/commands/distill.js +906 -100
  34. package/dist/commands/env.js +213 -0
  35. package/dist/commands/eval-cases.js +3 -0
  36. package/dist/commands/events.js +3 -0
  37. package/dist/commands/extract-cli.js +127 -0
  38. package/dist/commands/extract-prompt.js +217 -0
  39. package/dist/commands/extract.js +477 -0
  40. package/dist/commands/feedback-cli.js +331 -0
  41. package/dist/commands/graph.js +260 -5
  42. package/dist/commands/health.js +1042 -55
  43. package/dist/commands/history.js +51 -16
  44. package/dist/commands/improve-auto-accept.js +97 -0
  45. package/dist/commands/improve-cli.js +236 -0
  46. package/dist/commands/improve-profiles.js +138 -0
  47. package/dist/commands/improve-result-file.js +167 -0
  48. package/dist/commands/improve.js +1736 -346
  49. package/dist/commands/info.js +26 -28
  50. package/dist/commands/init.js +49 -1
  51. package/dist/commands/installed-stashes.js +6 -23
  52. package/dist/commands/knowledge.js +3 -0
  53. package/dist/commands/lint/agent-linter.js +3 -0
  54. package/dist/commands/lint/base-linter.js +199 -5
  55. package/dist/commands/lint/command-linter.js +3 -0
  56. package/dist/commands/lint/default-linter.js +3 -0
  57. package/dist/commands/lint/env-key-rules.js +154 -0
  58. package/dist/commands/lint/index.js +92 -3
  59. package/dist/commands/lint/knowledge-linter.js +3 -0
  60. package/dist/commands/lint/markdown-insertion.js +343 -0
  61. package/dist/commands/lint/memory-linter.js +3 -0
  62. package/dist/commands/lint/registry.js +3 -0
  63. package/dist/commands/lint/skill-linter.js +3 -0
  64. package/dist/commands/lint/task-linter.js +15 -12
  65. package/dist/commands/lint/types.js +3 -0
  66. package/dist/commands/lint/workflow-linter.js +3 -0
  67. package/dist/commands/lint.js +3 -0
  68. package/dist/commands/migration-help.js +5 -2
  69. package/dist/commands/proposal-drain-policies.js +128 -0
  70. package/dist/commands/proposal-drain.js +477 -0
  71. package/dist/commands/proposal.js +60 -6
  72. package/dist/commands/propose.js +24 -19
  73. package/dist/commands/reflect.js +1004 -94
  74. package/dist/commands/registry-cli.js +150 -0
  75. package/dist/commands/registry-search.js +3 -0
  76. package/dist/commands/remember-cli.js +257 -0
  77. package/dist/commands/remember.js +15 -6
  78. package/dist/commands/schema-repair.js +88 -15
  79. package/dist/commands/search.js +99 -14
  80. package/dist/commands/secret.js +173 -0
  81. package/dist/commands/self-update.js +3 -0
  82. package/dist/commands/show.js +32 -13
  83. package/dist/commands/source-add.js +7 -35
  84. package/dist/commands/source-clone.js +3 -0
  85. package/dist/commands/source-manage.js +3 -0
  86. package/dist/commands/tasks.js +161 -95
  87. package/dist/commands/url-checker.js +3 -0
  88. package/dist/core/action-contributors.js +3 -0
  89. package/dist/core/asset-ref.js +13 -2
  90. package/dist/core/asset-registry.js +9 -2
  91. package/dist/core/asset-serialize.js +88 -0
  92. package/dist/core/asset-spec.js +61 -5
  93. package/dist/core/common.js +93 -5
  94. package/dist/core/concurrent.js +3 -0
  95. package/dist/core/config-io.js +347 -0
  96. package/dist/core/config-migration.js +622 -0
  97. package/dist/core/config-schema.js +558 -0
  98. package/dist/core/config-sources.js +108 -0
  99. package/dist/core/config-types.js +4 -0
  100. package/dist/core/config-walker.js +337 -0
  101. package/dist/core/config.js +366 -1077
  102. package/dist/core/errors.js +42 -20
  103. package/dist/core/events.js +31 -25
  104. package/dist/core/file-lock.js +104 -0
  105. package/dist/core/frontmatter.js +75 -10
  106. package/dist/core/lesson-lint.js +3 -0
  107. package/dist/core/markdown.js +3 -0
  108. package/dist/core/memory-belief.js +62 -0
  109. package/dist/core/memory-contradiction-detect.js +274 -0
  110. package/dist/core/memory-improve.js +142 -14
  111. package/dist/core/parse.js +3 -0
  112. package/dist/core/paths.js +218 -50
  113. package/dist/core/proposal-quality-validators.js +380 -0
  114. package/dist/core/proposal-validators.js +11 -3
  115. package/dist/core/proposals.js +464 -5
  116. package/dist/core/state-db.js +349 -56
  117. package/dist/core/text-truncation.js +107 -0
  118. package/dist/core/time.js +3 -0
  119. package/dist/core/tty.js +59 -0
  120. package/dist/core/warn.js +7 -2
  121. package/dist/core/write-source.js +12 -0
  122. package/dist/indexer/db-backup.js +391 -0
  123. package/dist/indexer/db-search.js +136 -28
  124. package/dist/indexer/db.js +661 -166
  125. package/dist/indexer/ensure-index.js +3 -0
  126. package/dist/indexer/file-context.js +3 -0
  127. package/dist/indexer/graph-boost.js +162 -40
  128. package/dist/indexer/graph-db.js +241 -51
  129. package/dist/indexer/graph-dedup.js +3 -7
  130. package/dist/indexer/graph-extraction.js +242 -149
  131. package/dist/indexer/index-context.js +3 -9
  132. package/dist/indexer/indexer.js +86 -16
  133. package/dist/indexer/llm-cache.js +24 -19
  134. package/dist/indexer/manifest.js +3 -0
  135. package/dist/indexer/matchers.js +184 -11
  136. package/dist/indexer/memory-inference.js +94 -50
  137. package/dist/indexer/metadata-contributors.js +3 -0
  138. package/dist/indexer/metadata.js +110 -50
  139. package/dist/indexer/path-resolver.js +3 -0
  140. package/dist/indexer/project-context.js +192 -0
  141. package/dist/indexer/ranking-contributors.js +134 -7
  142. package/dist/indexer/ranking.js +8 -1
  143. package/dist/indexer/search-fields.js +5 -9
  144. package/dist/indexer/search-hit-enrichers.js +91 -2
  145. package/dist/indexer/search-source.js +20 -1
  146. package/dist/indexer/semantic-status.js +4 -1
  147. package/dist/indexer/staleness-detect.js +447 -0
  148. package/dist/indexer/usage-events.js +12 -9
  149. package/dist/indexer/walker.js +3 -0
  150. package/dist/integrations/agent/builders.js +135 -0
  151. package/dist/integrations/agent/config.js +121 -401
  152. package/dist/integrations/agent/detect.js +3 -0
  153. package/dist/integrations/agent/index.js +6 -14
  154. package/dist/integrations/agent/model-aliases.js +55 -0
  155. package/dist/integrations/agent/profiles.js +3 -0
  156. package/dist/integrations/agent/prompts.js +137 -8
  157. package/dist/integrations/agent/runner.js +208 -0
  158. package/dist/integrations/agent/sdk-runner.js +8 -2
  159. package/dist/integrations/agent/spawn.js +54 -14
  160. package/dist/integrations/github.js +3 -0
  161. package/dist/integrations/lockfile.js +22 -51
  162. package/dist/integrations/session-logs/index.js +4 -0
  163. package/dist/integrations/session-logs/inline-refs.js +35 -0
  164. package/dist/integrations/session-logs/pre-filter.js +152 -0
  165. package/dist/integrations/session-logs/providers/claude-code.js +226 -0
  166. package/dist/integrations/session-logs/providers/opencode.js +231 -25
  167. package/dist/integrations/session-logs/types.js +3 -0
  168. package/dist/llm/call-ai.js +14 -26
  169. package/dist/llm/client.js +16 -2
  170. package/dist/llm/embedder.js +20 -29
  171. package/dist/llm/embedders/cache.js +3 -7
  172. package/dist/llm/embedders/local.js +42 -1
  173. package/dist/llm/embedders/remote.js +20 -8
  174. package/dist/llm/embedders/types.js +3 -7
  175. package/dist/llm/feature-gate.js +92 -56
  176. package/dist/llm/graph-extract.js +402 -31
  177. package/dist/llm/index-passes.js +44 -29
  178. package/dist/llm/memory-infer.js +30 -2
  179. package/dist/llm/metadata-enhance.js +3 -7
  180. package/dist/output/cli-hints.js +7 -4
  181. package/dist/output/context.js +60 -8
  182. package/dist/output/renderers.js +170 -194
  183. package/dist/output/shapes/curate.js +56 -0
  184. package/dist/output/shapes/distill.js +10 -0
  185. package/dist/output/shapes/env-list.js +19 -0
  186. package/dist/output/shapes/events.js +11 -0
  187. package/dist/output/shapes/helpers.js +424 -0
  188. package/dist/output/shapes/history.js +7 -0
  189. package/dist/output/shapes/passthrough.js +105 -0
  190. package/dist/output/shapes/proposal-accept.js +7 -0
  191. package/dist/output/shapes/proposal-diff.js +7 -0
  192. package/dist/output/shapes/proposal-list.js +7 -0
  193. package/dist/output/shapes/proposal-producer.js +11 -0
  194. package/dist/output/shapes/proposal-reject.js +7 -0
  195. package/dist/output/shapes/proposal-show.js +7 -0
  196. package/dist/output/shapes/registry-search.js +6 -0
  197. package/dist/output/shapes/registry.js +30 -0
  198. package/dist/output/shapes/search.js +6 -0
  199. package/dist/output/shapes/secret-list.js +19 -0
  200. package/dist/output/shapes/show.js +6 -0
  201. package/dist/output/shapes/vault-list.js +19 -0
  202. package/dist/output/shapes.js +51 -549
  203. package/dist/output/text/add.js +6 -0
  204. package/dist/output/text/clone.js +6 -0
  205. package/dist/output/text/config.js +6 -0
  206. package/dist/output/text/curate.js +6 -0
  207. package/dist/output/text/distill.js +7 -0
  208. package/dist/output/text/enable-disable.js +7 -0
  209. package/dist/output/text/events.js +10 -0
  210. package/dist/output/text/feedback.js +6 -0
  211. package/dist/output/text/helpers.js +1059 -0
  212. package/dist/output/text/history.js +7 -0
  213. package/dist/output/text/import.js +6 -0
  214. package/dist/output/text/index.js +6 -0
  215. package/dist/output/text/info.js +6 -0
  216. package/dist/output/text/init.js +6 -0
  217. package/dist/output/text/list.js +6 -0
  218. package/dist/output/text/proposal-producer.js +8 -0
  219. package/dist/output/text/proposal.js +12 -0
  220. package/dist/output/text/registry-commands.js +11 -0
  221. package/dist/output/text/registry.js +30 -0
  222. package/dist/output/text/remember.js +6 -0
  223. package/dist/output/text/remove.js +6 -0
  224. package/dist/output/text/save.js +6 -0
  225. package/dist/output/text/search.js +6 -0
  226. package/dist/output/text/show.js +6 -0
  227. package/dist/output/text/update.js +6 -0
  228. package/dist/output/text/upgrade.js +6 -0
  229. package/dist/output/text/vault.js +16 -0
  230. package/dist/output/text/wiki.js +15 -0
  231. package/dist/output/text/workflow.js +14 -0
  232. package/dist/output/text.js +44 -1329
  233. package/dist/registry/build-index.js +3 -0
  234. package/dist/registry/create-provider-registry.js +3 -0
  235. package/dist/registry/factory.js +4 -1
  236. package/dist/registry/origin-resolve.js +3 -0
  237. package/dist/registry/providers/index.js +3 -0
  238. package/dist/registry/providers/skills-sh.js +11 -2
  239. package/dist/registry/providers/static-index.js +10 -1
  240. package/dist/registry/providers/types.js +3 -24
  241. package/dist/registry/resolve.js +11 -16
  242. package/dist/registry/types.js +3 -0
  243. package/dist/scripts/migrate-storage.js +17767 -0
  244. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
  245. package/dist/scripts/migrations/v16-to-v17.js +141 -0
  246. package/dist/setup/detect.js +3 -0
  247. package/dist/setup/ripgrep-install.js +3 -0
  248. package/dist/setup/ripgrep-resolve.js +3 -0
  249. package/dist/setup/setup.js +306 -67
  250. package/dist/setup/steps.js +3 -15
  251. package/dist/sources/include.js +3 -0
  252. package/dist/sources/provider-factory.js +3 -11
  253. package/dist/sources/provider.js +3 -20
  254. package/dist/sources/providers/filesystem.js +19 -23
  255. package/dist/sources/providers/git.js +171 -21
  256. package/dist/sources/providers/index.js +3 -0
  257. package/dist/sources/providers/install-types.js +3 -13
  258. package/dist/sources/providers/npm.js +3 -4
  259. package/dist/sources/providers/provider-utils.js +3 -0
  260. package/dist/sources/providers/sync-from-ref.js +3 -11
  261. package/dist/sources/providers/tar-utils.js +3 -0
  262. package/dist/sources/providers/website.js +18 -22
  263. package/dist/sources/resolve.js +3 -0
  264. package/dist/sources/types.js +3 -0
  265. package/dist/sources/website-ingest.js +3 -0
  266. package/dist/tasks/backends/cron.js +3 -0
  267. package/dist/tasks/backends/exec-utils.js +3 -0
  268. package/dist/tasks/backends/index.js +3 -11
  269. package/dist/tasks/backends/launchd.js +4 -1
  270. package/dist/tasks/backends/schtasks.js +4 -1
  271. package/dist/tasks/parser.js +51 -38
  272. package/dist/tasks/resolveAkmBin.js +3 -0
  273. package/dist/tasks/runner.js +35 -9
  274. package/dist/tasks/schedule.js +20 -1
  275. package/dist/tasks/schema.js +5 -3
  276. package/dist/tasks/validator.js +6 -3
  277. package/dist/version.js +3 -0
  278. package/dist/wiki/wiki-templates.js +6 -3
  279. package/dist/wiki/wiki.js +4 -1
  280. package/dist/workflows/authoring.js +4 -1
  281. package/dist/workflows/cli.js +3 -0
  282. package/dist/workflows/db.js +140 -10
  283. package/dist/workflows/document-cache.js +3 -10
  284. package/dist/workflows/parser.js +3 -0
  285. package/dist/workflows/renderer.js +3 -0
  286. package/dist/workflows/runs.js +18 -1
  287. package/dist/workflows/schema.js +3 -0
  288. package/dist/workflows/scope-key.js +3 -0
  289. package/dist/workflows/validator.js +5 -9
  290. package/docs/README.md +7 -2
  291. package/docs/data-and-telemetry.md +225 -0
  292. package/docs/migration/release-notes/0.7.5.md +2 -2
  293. package/docs/migration/release-notes/0.8.0.md +57 -5
  294. package/docs/migration/v0.7-to-v0.8.md +1378 -0
  295. package/package.json +28 -11
  296. package/.github/LICENSE +0 -374
  297. package/dist/commands/help/help-accept.md +0 -9
  298. package/dist/commands/help/help-improve.md +0 -53
  299. package/dist/commands/help/help-reject.md +0 -8
  300. package/dist/commands/install-audit.js +0 -385
  301. package/dist/commands/vault.js +0 -310
  302. package/dist/indexer/match-contributors.js +0 -141
  303. package/dist/integrations/agent/pipeline.js +0 -39
  304. package/dist/integrations/agent/runners.js +0 -31
  305. package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
  306. /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
  307. /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
  308. /package/dist/{commands → assets}/help/help-propose.md +0 -0
  309. /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
  310. /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
  311. /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
  312. /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
  313. /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
@@ -1,3 +1,6 @@
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/.
1
4
  /**
2
5
  * state.db — Durable SQLite database for non-regenerable akm state.
3
6
  *
@@ -315,6 +318,138 @@ const MIGRATIONS = [
315
318
  ON task_history(status);
316
319
  `,
317
320
  },
321
+ // ── Migration 003 — improve_runs ────────────────────────────────────────────
322
+ //
323
+ // Records every `akm improve` invocation as a durable row, replacing the
324
+ // legacy `<stash>/.akm/runs/<runId>/improve-result.json` artifact files.
325
+ //
326
+ // The `dry_run` column is FIRST-CLASS and indexed so productivity audits can
327
+ // cleanly filter dry-run probes out of real-run analyses without parsing
328
+ // `result_json`. The dry-run/real-run artifact-trap (recorded in
329
+ // feedback_akm_dryrun_artifact_trap) was the specific motivating bug.
330
+ //
331
+ // Indexed (query) columns:
332
+ // id TEXT PK — runId (`buildImproveRunId()` output).
333
+ // started_at TEXT — ISO-8601; indexed for time-range queries.
334
+ // stash_dir TEXT — absolute stash root; multi-stash scoping.
335
+ // dry_run INTEGER — 0/1; indexed for productivity audits.
336
+ // scope_mode TEXT — "all" | "type" | "ref"; indexed via composite
337
+ // with stash_dir for stash-scoped scope queries.
338
+ //
339
+ // Non-indexed payload:
340
+ // completed_at TEXT — ISO-8601 or NULL if interrupted.
341
+ // profile TEXT — improve profile name (nullable).
342
+ // scope_value TEXT — type name or asset ref (nullable).
343
+ // guidance TEXT — user-provided guidance text, if any.
344
+ // ok INTEGER — 0/1; whether the run produced ok=true.
345
+ // result_json TEXT — full AkmImproveResult JSON.
346
+ // metrics_json TEXT — aggregate counts extracted from result, cheap
347
+ // to query without parsing result_json.
348
+ //
349
+ // Extensible (metadata_json) columns:
350
+ // metadata_json TEXT — JSON object for future improve-run fields.
351
+ //
352
+ // ADD COLUMN extension points (future migrations):
353
+ // ALTER TABLE improve_runs ADD COLUMN duration_ms INTEGER DEFAULT NULL;
354
+ // ALTER TABLE improve_runs ADD COLUMN host TEXT DEFAULT NULL;
355
+ //
356
+ // TTL: rows where started_at < NOW() - 90 days can be deleted by
357
+ // `purgeOldImproveRuns()`. No automatic deletion occurs here.
358
+ {
359
+ id: "003-improve-runs",
360
+ up: `
361
+ CREATE TABLE IF NOT EXISTS improve_runs (
362
+ id TEXT PRIMARY KEY,
363
+ started_at TEXT NOT NULL,
364
+ completed_at TEXT,
365
+ stash_dir TEXT NOT NULL,
366
+ dry_run INTEGER NOT NULL DEFAULT 0,
367
+ profile TEXT,
368
+ scope_mode TEXT NOT NULL,
369
+ scope_value TEXT,
370
+ guidance TEXT,
371
+ ok INTEGER NOT NULL,
372
+ result_json TEXT NOT NULL,
373
+ metrics_json TEXT,
374
+ metadata_json TEXT NOT NULL DEFAULT '{}'
375
+ );
376
+
377
+ -- Query patterns supported:
378
+ -- SELECT … WHERE started_at >= ? AND started_at <= ?
379
+ -- → idx_improve_runs_started
380
+ -- SELECT … WHERE dry_run = 0
381
+ -- → idx_improve_runs_dry_run (productivity audits filter trap)
382
+ -- SELECT … WHERE stash_dir = ? AND scope_mode = ?
383
+ -- → idx_improve_runs_stash_scope
384
+ CREATE INDEX IF NOT EXISTS idx_improve_runs_started
385
+ ON improve_runs(started_at);
386
+ CREATE INDEX IF NOT EXISTS idx_improve_runs_dry_run
387
+ ON improve_runs(dry_run);
388
+ CREATE INDEX IF NOT EXISTS idx_improve_runs_stash_scope
389
+ ON improve_runs(stash_dir, scope_mode);
390
+ `,
391
+ },
392
+ // ── Migration 004 — extract_sessions_seen ───────────────────────────────────
393
+ //
394
+ // Tracks which platform sessions the extractor has processed, so the discovery
395
+ // pass in `akm extract --since <window>` skips sessions whose content hasn't
396
+ // changed since the last successful run. Replaces the akm-plugin
397
+ // session-checkpoint hook's implicit "write-once" memory of what's been
398
+ // captured — but persistent and queryable.
399
+ //
400
+ // Indexed (query) columns:
401
+ // harness TEXT — harness name (claude-code, opencode, ...).
402
+ // session_id TEXT — platform-native session identifier.
403
+ // processed_at TEXT — ISO-8601 UTC; when extract last ran on this session.
404
+ // session_ended_at TEXT — session.endedAt at processing time. When a
405
+ // later listSessions reports a *newer* endedAt
406
+ // for the same session_id, the extractor
407
+ // re-processes the appended events.
408
+ // outcome TEXT — "candidates_queued" | "no_candidates" |
409
+ // "skipped" | "failed".
410
+ //
411
+ // Non-indexed columns:
412
+ // candidate_count INTEGER — number of candidates the LLM produced.
413
+ // proposal_count INTEGER — number of proposals actually queued
414
+ // (candidates may fail downstream validation).
415
+ // rationale TEXT — for "no_candidates", the LLM's explanation.
416
+ // source_run TEXT — sourceRun id for PROV-DM traceability.
417
+ // metadata_json TEXT — future-proofing (pre-filter stats, LLM
418
+ // model+version, prompt token count, etc.).
419
+ //
420
+ // PK: (harness, session_id) — one row per session per harness. A re-extract
421
+ // updates the row in place via INSERT OR REPLACE.
422
+ //
423
+ // TTL: no automatic deletion. Sessions stay tracked as long as the source
424
+ // session files exist on disk. Operator can `DELETE FROM extract_sessions_seen
425
+ // WHERE processed_at < ?` for cleanup if desired.
426
+ {
427
+ id: "004-extract-sessions-seen",
428
+ up: `
429
+ CREATE TABLE IF NOT EXISTS extract_sessions_seen (
430
+ harness TEXT NOT NULL,
431
+ session_id TEXT NOT NULL,
432
+ processed_at TEXT NOT NULL,
433
+ session_ended_at TEXT,
434
+ outcome TEXT NOT NULL,
435
+ candidate_count INTEGER NOT NULL DEFAULT 0,
436
+ proposal_count INTEGER NOT NULL DEFAULT 0,
437
+ rationale TEXT,
438
+ source_run TEXT,
439
+ metadata_json TEXT NOT NULL DEFAULT '{}',
440
+ PRIMARY KEY (harness, session_id)
441
+ );
442
+
443
+ -- Query patterns:
444
+ -- SELECT … WHERE harness = ? → idx_extract_sessions_harness
445
+ -- SELECT … WHERE processed_at >= ? → idx_extract_sessions_processed
446
+ -- SELECT … WHERE harness = ? AND session_id = ? → PK
447
+ CREATE INDEX IF NOT EXISTS idx_extract_sessions_harness
448
+ ON extract_sessions_seen(harness);
449
+ CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
450
+ ON extract_sessions_seen(processed_at);
451
+ `,
452
+ },
318
453
  ];
319
454
  /**
320
455
  * Create the migrations table if it does not exist. This must be called
@@ -498,12 +633,21 @@ export function readStateEvents(db, options = {}) {
498
633
  /**
499
634
  * Delete events older than `retentionDays` (default: 90). Safe to call from
500
635
  * a maintenance cron; uses a single DELETE with an index-covered ts predicate.
636
+ *
637
+ * Returns the number of rows actually deleted so callers can emit an
638
+ * `events_purged` observability event. A non-positive or non-finite
639
+ * `retentionDays` is treated as "disabled" and returns 0 without scanning.
501
640
  */
502
641
  export function purgeOldEvents(db, retentionDays = 90) {
503
642
  if (!Number.isFinite(retentionDays) || retentionDays <= 0)
504
- return;
643
+ return 0;
505
644
  const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
506
- db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
645
+ const result = db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
646
+ // bun:sqlite's run() returns { changes, lastInsertRowid }. `changes` may be
647
+ // a number or bigint depending on the underlying lib; coerce to number for
648
+ // the metadata payload.
649
+ const changes = result.changes ?? 0;
650
+ return typeof changes === "bigint" ? Number(changes) : changes;
507
651
  }
508
652
  // ── proposals table helpers ──────────────────────────────────────────────────
509
653
  /**
@@ -643,27 +787,52 @@ export function queryTaskHistory(db, options = {}) {
643
787
  * monotonic integer ids. Callers that persisted a byte-offset cursor must
644
788
  * discard it after migration and use the returned `maxId` as the new cursor.
645
789
  *
646
- * The import is wrapped in a single transaction for atomicity. If the file
647
- * has already been imported (the events table is non-empty and the file
648
- * has not changed since last import), callers should skip calling this
649
- * function de-duplication is NOT performed here to keep the hot path fast.
790
+ * **Idempotency**: each line is pre-checked against the `events` table using
791
+ * `(event_type, ts, ref, metadata_json)` as the duplicate key. Lines whose
792
+ * exact tuple is already present are skipped and reported as `skipped` in the
793
+ * return value. This makes the migration safe to re-run (the v0.7→v0.8
794
+ * migration guide recommends re-running the script as a recovery path; without
795
+ * this guard, every re-run would double-import the entire event log).
796
+ *
797
+ * Duplicate detection is per-import-tuple, not a table-wide UNIQUE constraint:
798
+ * the events table has no UNIQUE constraint at runtime so that
799
+ * `appendEvent` can write multiple events with the same ts (sub-millisecond
800
+ * bursts produce identical `(event_type, ts, ref)` triples in practice). The
801
+ * SELECT-first check is scoped to the import path only.
802
+ *
803
+ * The import is wrapped in a single transaction for atomicity.
650
804
  *
651
805
  * @param db - Open state.db connection.
652
806
  * @param jsonlPath - Absolute path to the events.jsonl file to import.
653
- * @returns Number of rows inserted and the max id assigned.
807
+ * @returns Number of rows inserted, the max id assigned, and the
808
+ * count of rows skipped because an identical event already
809
+ * existed in the table.
654
810
  */
655
811
  export async function importEventsJsonl(db, jsonlPath) {
656
812
  const { readFileSync, existsSync } = await import("node:fs");
657
813
  if (!existsSync(jsonlPath)) {
658
- return { imported: 0, maxId: 0 };
814
+ return { imported: 0, maxId: 0, skipped: 0 };
659
815
  }
660
816
  const text = readFileSync(jsonlPath, "utf8");
661
817
  const lines = text.split("\n").filter((l) => l.trim().length > 0);
662
818
  let imported = 0;
663
819
  let maxId = 0;
664
- const stmt = db.prepare(`INSERT INTO events (event_type, ts, ref, metadata_json)
820
+ let skipped = 0;
821
+ const insertStmt = db.prepare(`INSERT INTO events (event_type, ts, ref, metadata_json)
665
822
  VALUES (?, ?, ?, ?)
666
823
  RETURNING id`);
824
+ // Dedup pre-check: matches by the full tuple including metadata_json so an
825
+ // import is idempotent over identical rows but does not collide with two
826
+ // genuinely different events that happen to share (event_type, ts, ref).
827
+ //
828
+ // Uses IS for ref so two NULL refs compare equal (a plain `=` would treat
829
+ // NULL = NULL as NULL and the row would be re-inserted on every run).
830
+ const existsStmt = db.prepare(`SELECT 1 FROM events
831
+ WHERE event_type = ?
832
+ AND ts = ?
833
+ AND ref IS ?
834
+ AND metadata_json = ?
835
+ LIMIT 1`);
667
836
  db.transaction(() => {
668
837
  for (const line of lines) {
669
838
  let parsed;
@@ -677,7 +846,12 @@ export async function importEventsJsonl(db, jsonlPath) {
677
846
  const ts = typeof parsed.ts === "string" ? parsed.ts : new Date().toISOString();
678
847
  const ref = typeof parsed.ref === "string" ? parsed.ref : null;
679
848
  const metadata = parsed.metadata !== undefined && typeof parsed.metadata === "object" ? JSON.stringify(parsed.metadata) : "{}";
680
- const result = stmt.get(eventType, ts, ref, metadata);
849
+ const duplicate = existsStmt.get(eventType, ts, ref, metadata);
850
+ if (duplicate) {
851
+ skipped++;
852
+ continue;
853
+ }
854
+ const result = insertStmt.get(eventType, ts, ref, metadata);
681
855
  if (result) {
682
856
  imported++;
683
857
  if (result.id > maxId)
@@ -685,7 +859,170 @@ export async function importEventsJsonl(db, jsonlPath) {
685
859
  }
686
860
  }
687
861
  })();
688
- return { imported, maxId };
862
+ return { imported, maxId, skipped };
863
+ }
864
+ /**
865
+ * Compute the cheap aggregate metrics blob from a full improve result.
866
+ *
867
+ * Pure function — no I/O. Used by {@link recordImproveRun} to populate
868
+ * `metrics_json`. Exposed for tests and for any future call site that wants
869
+ * the same aggregation logic without hitting state.db.
870
+ */
871
+ export function computeImproveRunMetrics(result) {
872
+ const plannedCount = Array.isArray(result.plannedRefs) ? result.plannedRefs.length : 0;
873
+ const actions = Array.isArray(result.actions) ? result.actions : [];
874
+ const actionsCount = actions.length;
875
+ let acceptedCount = 0;
876
+ let rejectedCount = 0;
877
+ let autoAcceptedCount = 0;
878
+ let errorCount = 0;
879
+ for (const action of actions) {
880
+ switch (action.mode) {
881
+ case "reflect":
882
+ case "distill":
883
+ case "memory-inference":
884
+ case "graph-extraction":
885
+ acceptedCount++;
886
+ break;
887
+ case "reflect-cooldown":
888
+ case "reflect-skipped":
889
+ case "distill-skipped":
890
+ rejectedCount++;
891
+ break;
892
+ case "reflect-failed":
893
+ case "error":
894
+ errorCount++;
895
+ break;
896
+ case "memory-prune":
897
+ // Prune is bookkeeping, not "accepted" content authoring; count
898
+ // separately as a no-op for the audit aggregate.
899
+ break;
900
+ }
901
+ // Legacy: pre-gate action results may carry autoAccepted: true (reflect path).
902
+ const r = action.result;
903
+ if (r && r.autoAccepted === true)
904
+ autoAcceptedCount++;
905
+ }
906
+ // Add gate-promoted count from the unified PostPhaseAutoAcceptGate (all phases).
907
+ autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
908
+ return { plannedCount, actionsCount, acceptedCount, rejectedCount, autoAcceptedCount, errorCount };
909
+ }
910
+ /**
911
+ * Insert a single improve-run row into `improve_runs`. Uses parameterised SQL.
912
+ *
913
+ * Idempotency: the table's PRIMARY KEY is `id`, so re-running with the same
914
+ * runId would error. Callers mint a fresh runId per invocation via
915
+ * {@link buildImproveRunId} so this is not a concern in practice — but the
916
+ * default behaviour is INSERT (not REPLACE) so accidental dupes surface as
917
+ * a SQLite constraint error rather than silently overwriting a prior record.
918
+ *
919
+ * The `metrics` parameter defaults to the output of
920
+ * {@link computeImproveRunMetrics} when not supplied. Pass an explicit
921
+ * `metrics` object to override the derivation (e.g. tests).
922
+ */
923
+ export function recordImproveRun(db, input) {
924
+ const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
925
+ db.prepare(`
926
+ INSERT INTO improve_runs
927
+ (id, started_at, completed_at, stash_dir, dry_run, profile,
928
+ scope_mode, scope_value, guidance, ok, result_json, metrics_json, metadata_json)
929
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
930
+ `).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.profile, input.scopeMode, input.scopeValue, input.guidance, input.ok ? 1 : 0, JSON.stringify(input.result), JSON.stringify(metricsObj), JSON.stringify(input.metadata ?? {}));
931
+ }
932
+ /**
933
+ * Delete improve_runs rows older than `retentionDays` (default: 90). Mirrors
934
+ * {@link purgeOldEvents} — same default, same return shape (number of rows
935
+ * actually deleted), same disabled-when-non-finite semantics.
936
+ *
937
+ * Safe to call from the improve post-loop maintenance pass alongside
938
+ * `purgeOldEvents(db, retentionDays)`.
939
+ */
940
+ export function purgeOldImproveRuns(db, retentionDays = 90) {
941
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0)
942
+ return 0;
943
+ const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
944
+ const result = db.prepare("DELETE FROM improve_runs WHERE started_at < ?").run(cutoff);
945
+ const changes = result.changes ?? 0;
946
+ return typeof changes === "bigint" ? Number(changes) : changes;
947
+ }
948
+ /**
949
+ * Record (or update) one session's extract outcome. INSERT-OR-REPLACE so the
950
+ * row reflects the most recent run; downstream skip-logic compares
951
+ * `session_ended_at` against the live session metadata to decide if anything
952
+ * new arrived since `processed_at`.
953
+ */
954
+ export function upsertExtractedSession(db, input) {
955
+ const endedAtIso = typeof input.sessionEndedAt === "number" && Number.isFinite(input.sessionEndedAt)
956
+ ? new Date(input.sessionEndedAt).toISOString()
957
+ : null;
958
+ db.prepare(`
959
+ INSERT OR REPLACE INTO extract_sessions_seen
960
+ (harness, session_id, processed_at, session_ended_at, outcome,
961
+ candidate_count, proposal_count, rationale, source_run, metadata_json)
962
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
963
+ `).run(input.harness, input.sessionId, input.processedAt, endedAtIso, input.outcome, input.candidateCount, input.proposalCount, input.rationale ?? null, input.sourceRun ?? null, JSON.stringify(input.metadata ?? {}));
964
+ }
965
+ /**
966
+ * Fetch a single session's last extract record, or `undefined` when the
967
+ * session has never been processed.
968
+ */
969
+ export function getExtractedSession(db, harness, sessionId) {
970
+ // bun:sqlite returns null (not undefined) when no row matches — normalize so
971
+ // callers can rely on `if (!row)` and `toBeUndefined()` equivalently.
972
+ const row = db
973
+ .prepare("SELECT * FROM extract_sessions_seen WHERE harness = ? AND session_id = ?")
974
+ .get(harness, sessionId);
975
+ return row ?? undefined;
976
+ }
977
+ /**
978
+ * Bulk-fetch session-extract status for a list of sessionIds in one harness.
979
+ * Returns a Map keyed by sessionId so callers can do O(1) lookups while
980
+ * iterating the discovery list.
981
+ */
982
+ export function getExtractedSessionsMap(db, harness, sessionIds) {
983
+ const out = new Map();
984
+ if (sessionIds.length === 0)
985
+ return out;
986
+ // SQLite has a ~999 param ceiling; chunk if a caller ever exceeds that.
987
+ const CHUNK = 500;
988
+ for (let i = 0; i < sessionIds.length; i += CHUNK) {
989
+ const chunk = sessionIds.slice(i, i + CHUNK);
990
+ const placeholders = chunk.map(() => "?").join(",");
991
+ const rows = db
992
+ .prepare(`SELECT * FROM extract_sessions_seen
993
+ WHERE harness = ? AND session_id IN (${placeholders})`)
994
+ .all(harness, ...chunk);
995
+ for (const row of rows)
996
+ out.set(row.session_id, row);
997
+ }
998
+ return out;
999
+ }
1000
+ /**
1001
+ * Decide whether a session should be skipped because the extractor has
1002
+ * already processed it AND nothing has changed since. The "anything new since
1003
+ * last extract?" rule is: the live `sessionEndedAtMs` is strictly later than
1004
+ * the recorded `session_ended_at`. Same-or-earlier endedAt means we'd be
1005
+ * re-processing the exact same content for no gain.
1006
+ *
1007
+ * Returns:
1008
+ * - `false` — no prior row, or session has new content since last extract.
1009
+ * The caller should process it.
1010
+ * - `true` — the session was already processed and hasn't been updated.
1011
+ * The caller should skip.
1012
+ */
1013
+ export function shouldSkipAlreadyExtractedSession(prior, liveSessionEndedAtMs) {
1014
+ if (!prior)
1015
+ return false;
1016
+ // No live timestamp → can't tell if anything's new. Be conservative and
1017
+ // skip — the operator can pass --force later if we add it.
1018
+ if (typeof liveSessionEndedAtMs !== "number" || !Number.isFinite(liveSessionEndedAtMs)) {
1019
+ return true;
1020
+ }
1021
+ const priorMs = prior.session_ended_at ? Date.parse(prior.session_ended_at) : Number.NaN;
1022
+ if (!Number.isFinite(priorMs))
1023
+ return false;
1024
+ // Re-process when there's new content; skip when the session is unchanged.
1025
+ return liveSessionEndedAtMs <= priorMs;
689
1026
  }
690
1027
  // ── registry_index_cache (goes in index.db, not state.db) ───────────────────
691
1028
  /**
@@ -696,7 +1033,7 @@ export async function importEventsJsonl(db, jsonlPath) {
696
1033
  * created with CREATE TABLE IF NOT EXISTS so it is safe to call inside
697
1034
  * ensureSchema() or as a standalone migration.
698
1035
  *
699
- * Purpose: caches the result of resolving and fetching remote registry kit
1036
+ * Purpose: caches the result of resolving and fetching remote registry stash
700
1037
  * indexes so `akm search` does not hit the network on every invocation.
701
1038
  *
702
1039
  * Indexed (query) columns:
@@ -729,47 +1066,3 @@ export const REGISTRY_INDEX_CACHE_DDL = `
729
1066
  CREATE INDEX IF NOT EXISTS idx_registry_cache_fetched
730
1067
  ON registry_index_cache(fetched_at);
731
1068
  `;
732
- /**
733
- * Ensure the `registry_index_cache` table exists in the given database
734
- * (intended for use with the index.db connection from openDatabase()).
735
- * Idempotent — safe to call on every open.
736
- */
737
- export function ensureRegistryIndexCacheSchema(db) {
738
- db.exec(REGISTRY_INDEX_CACHE_DDL);
739
- }
740
- /**
741
- * Upsert a registry index cache entry.
742
- *
743
- * @param db - Open index.db connection.
744
- * @param registryUrl - Canonical URL of the registry.
745
- * @param indexJson - Serialised registry index document.
746
- * @param etag - HTTP ETag from the response (optional).
747
- * @param lastModified - HTTP Last-Modified from the response (optional).
748
- */
749
- export function upsertRegistryIndexCache(db, registryUrl, indexJson, etag, lastModified) {
750
- db.prepare(`
751
- INSERT INTO registry_index_cache (registry_url, fetched_at, etag, last_modified, index_json)
752
- VALUES (?, ?, ?, ?, ?)
753
- ON CONFLICT(registry_url) DO UPDATE SET
754
- fetched_at = excluded.fetched_at,
755
- etag = excluded.etag,
756
- last_modified = excluded.last_modified,
757
- index_json = excluded.index_json
758
- `).run(registryUrl, new Date().toISOString(), etag ?? null, lastModified ?? null, indexJson);
759
- }
760
- /**
761
- * Look up a cached registry index entry. Returns undefined when not found or
762
- * when the entry is older than `maxAgeMs`.
763
- */
764
- export function getRegistryIndexCache(db, registryUrl, maxAgeMs = 3_600_000 /* 1 hour */) {
765
- const row = db
766
- .prepare(`SELECT fetched_at, etag, last_modified, index_json
767
- FROM registry_index_cache WHERE registry_url = ?`)
768
- .get(registryUrl);
769
- if (!row)
770
- return undefined;
771
- const fetchedAt = Date.parse(row.fetched_at);
772
- if (Number.isNaN(fetchedAt) || Date.now() - fetchedAt > maxAgeMs)
773
- return undefined;
774
- return { indexJson: row.index_json, etag: row.etag, lastModified: row.last_modified };
775
- }
@@ -0,0 +1,107 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Shared text-truncation heuristics used by `distill` and `consolidate`.
6
+ *
7
+ * Both commands need to recognise when an LLM-produced description string was
8
+ * sliced mid-sentence (typically the model hit its output budget). The two
9
+ * implementations historically maintained overlapping-but-not-identical
10
+ * vocabularies of hanging-connector words, which was a maintenance trap.
11
+ *
12
+ * This module is the single source of truth. `distill` continues to layer its
13
+ * own section-heading regex on top (those patterns are distill-specific and
14
+ * intentionally stay local to that module).
15
+ */
16
+ /**
17
+ * Words that, when ending a sentence, suggest the description was truncated
18
+ * mid-sentence. Prepositions, conjunctions, articles, and auxiliary verbs that
19
+ * almost always have *something* following them in well-formed prose.
20
+ *
21
+ * This is the UNION of the two prior vocabularies used by `distill` and
22
+ * `consolidate` — a superset of both, so behaviour is at least as strict as
23
+ * either previous check.
24
+ *
25
+ * Stored lowercased; callers must lower-case the last word before lookup.
26
+ */
27
+ export const TRUNCATION_TRAILING_WORDS = new Set([
28
+ "a",
29
+ "after",
30
+ "an",
31
+ "and",
32
+ "are",
33
+ "as",
34
+ "at",
35
+ "be",
36
+ "been",
37
+ "before",
38
+ "being",
39
+ "but",
40
+ "by",
41
+ "can",
42
+ "could",
43
+ "did",
44
+ "do",
45
+ "does",
46
+ "for",
47
+ "from",
48
+ "had",
49
+ "has",
50
+ "have",
51
+ "if",
52
+ "in",
53
+ "into",
54
+ "is",
55
+ "may",
56
+ "might",
57
+ "must",
58
+ "of",
59
+ "on",
60
+ "onto",
61
+ "or",
62
+ "per",
63
+ "shall",
64
+ "should",
65
+ "so",
66
+ "than",
67
+ "that",
68
+ "the",
69
+ "to",
70
+ "upon",
71
+ "via",
72
+ "was",
73
+ "were",
74
+ "when",
75
+ "which",
76
+ "while",
77
+ "will",
78
+ "with",
79
+ "would",
80
+ ]);
81
+ /**
82
+ * Returns a reason string when `description` looks truncated mid-sentence;
83
+ * returns `null` if the description appears complete.
84
+ *
85
+ * Heuristics:
86
+ * - Trailing `,`, `;`, `:`, or `+` (operator-style cutoff like `max-width:100% +`)
87
+ * - Trailing ellipsis (`...` or `…`)
88
+ * - Last word matches {@link TRUNCATION_TRAILING_WORDS}
89
+ *
90
+ * Does NOT detect section-heading fragments — that check is distill-specific
91
+ * and lives in `src/commands/distill.ts` (`HEADING_FRAGMENT_PATTERNS`).
92
+ */
93
+ export function detectTruncatedDescription(description) {
94
+ const trimmed = description.trim();
95
+ if (trimmed.length === 0)
96
+ return null; // empty handled elsewhere
97
+ if (/[,;:+]$/.test(trimmed))
98
+ return "ends with trailing punctuation/operator";
99
+ if (/\.{3,}$/.test(trimmed) || /…$/.test(trimmed))
100
+ return "ends with ellipsis";
101
+ const lastWord = trimmed.split(/\s+/).pop() ?? "";
102
+ const normalized = lastWord.toLowerCase();
103
+ if (TRUNCATION_TRAILING_WORDS.has(normalized)) {
104
+ return `ends with hanging connector "${lastWord}"`;
105
+ }
106
+ return null;
107
+ }
package/dist/core/time.js CHANGED
@@ -1,3 +1,6 @@
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/.
1
4
  /**
2
5
  * Shared time and date utilities.
3
6
  *
@@ -0,0 +1,59 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Terminal-glyph and color gating helpers (#486).
6
+ *
7
+ * Honor the [NO_COLOR](https://no-color.org/) convention — when NO_COLOR is
8
+ * present in the environment (with any value, including empty), the CLI
9
+ * suppresses both ANSI color codes AND decorative emoji glyphs. Emoji often
10
+ * render as garbage in non-Unicode terminals or get logged as literal bytes
11
+ * when output is piped to text aggregators, so we treat them as a subset of
12
+ * "color" decoration.
13
+ *
14
+ * Detection is also automatic on non-TTY stderr (output piped or redirected
15
+ * to a file), where decorative glyphs add nothing.
16
+ */
17
+ /**
18
+ * Returns true when decorative glyphs and color codes should be emitted.
19
+ * False when NO_COLOR is set or stderr is not a TTY (unless FORCE_COLOR
20
+ * overrides per the de-facto Node convention).
21
+ */
22
+ export function shouldDecorate() {
23
+ if (process.env.NO_COLOR !== undefined)
24
+ return false;
25
+ if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "" && process.env.FORCE_COLOR !== "0") {
26
+ return true;
27
+ }
28
+ return process.stderr.isTTY === true;
29
+ }
30
+ // Map known decorative emoji to plain ASCII fallbacks. Anything not listed
31
+ // is still removed by the catch-all sweep below when decoration is off.
32
+ const EMOJI_FALLBACKS = [
33
+ [/\u{1F44B}\s*/gu, ""], // wave (with trailing space)
34
+ [/✓/g, "[ok]"],
35
+ [/✗/g, "[x]"],
36
+ [/✘/g, "[x]"],
37
+ [/⚠️?/g, "[!]"],
38
+ [/\u{1F4DA}\s*/gu, ""], // books
39
+ ];
40
+ /**
41
+ * If decoration is disabled, replace known emoji with ASCII fallbacks and
42
+ * strip any remaining pictograph code points. If decoration is enabled,
43
+ * return the input unchanged.
44
+ */
45
+ export function plainize(text) {
46
+ if (shouldDecorate())
47
+ return text;
48
+ let out = text;
49
+ for (const [pattern, repl] of EMOJI_FALLBACKS) {
50
+ out = out.replace(pattern, repl);
51
+ }
52
+ // Catch-all for unmapped pictographs. Future-proof when new emoji are added
53
+ // to call sites without updating the explicit map.
54
+ out = out.replace(/[\u{1F300}-\u{1FAFF}]/gu, "");
55
+ // Collapse runs of whitespace introduced by emoji removal, but preserve
56
+ // intentional leading indentation. Only collapses interior runs.
57
+ out = out.replace(/(\S)[ \t]{2,}/g, "$1 ");
58
+ return out;
59
+ }
package/dist/core/warn.js CHANGED
@@ -1,3 +1,6 @@
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/.
1
4
  /**
2
5
  * Module-level quiet/verbose flags and optional file sink for stderr output.
3
6
  *
@@ -81,8 +84,10 @@ function appendToLogFile(level, args) {
81
84
  try {
82
85
  fs.appendFileSync(logFilePath, `[${ts}] [${level}] ${msg}\n`);
83
86
  }
84
- catch {
85
- // Never throw from a logging function log failures are silent.
87
+ catch (e) {
88
+ // Log file write failed emit directly to stderr so the message is not lost.
89
+ process.stderr.write(`[akm:warn] log-file write failed (${logFilePath}): ${e}\n`);
90
+ process.stderr.write(`[${ts}] [${level}] ${msg}\n`);
86
91
  }
87
92
  }
88
93
  /**