akm-cli 0.9.0-beta.6 → 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 (326) hide show
  1. package/CHANGELOG.md +663 -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 -14
  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 +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  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 -1083
  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 +1313 -1278
  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 +243 -599
  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 +55 -4
  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 +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -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 +510 -2537
  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 +115 -0
  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 +212 -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 +7 -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 -882
  138. package/dist/commands/read/curate.js +419 -103
  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 +29 -41
  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 +389 -58
  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 +21 -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 +142 -1091
  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 +356 -780
  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 +103 -24
  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 +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  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 +11 -1
  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 +57 -15
  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 +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  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/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -0,0 +1,39 @@
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 { BaseLinter } from "./base-linter.js";
5
+ /** Recommended `category` values for facts (see docs/design/fact-asset-type.md). */
6
+ const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
7
+ /**
8
+ * Linter for `facts/` assets.
9
+ *
10
+ * Extra check beyond base:
11
+ * - `missing-category`: a fact without a non-empty `category` frontmatter
12
+ * key. Category scopes the fact (personal/team/project/convention/meta)
13
+ * and drives how it is surfaced/injected, so it is expected. Reported as a
14
+ * non-fixable warning — we cannot infer the right scope automatically.
15
+ */
16
+ export class FactLinter extends BaseLinter {
17
+ types = ["facts"];
18
+ lint(ctx) {
19
+ const issues = this.runBaseChecks(ctx);
20
+ const category = typeof ctx.data.category === "string" ? ctx.data.category.trim() : "";
21
+ if (!category) {
22
+ issues.push({
23
+ file: ctx.relPath,
24
+ issue: "missing-category",
25
+ detail: "fact is missing a `category` (personal|team|project|convention|meta)",
26
+ fixed: false,
27
+ });
28
+ }
29
+ else if (!KNOWN_CATEGORIES.has(category)) {
30
+ issues.push({
31
+ file: ctx.relPath,
32
+ issue: "missing-category",
33
+ detail: `unrecognized category "${category}" (expected one of: ${[...KNOWN_CATEGORIES].join(", ")})`,
34
+ fixed: false,
35
+ });
36
+ }
37
+ return issues;
38
+ }
39
+ }
@@ -20,6 +20,7 @@ const STASH_SUBDIRS = [
20
20
  "lessons",
21
21
  "tasks",
22
22
  "knowledge",
23
+ "facts",
23
24
  ];
24
25
  // ── Helpers ───────────────────────────────────────────────────────────────────
25
26
  function collectYamlFiles(dir) {
@@ -85,7 +86,8 @@ export function akmLint(options = {}) {
85
86
  const fix = options.fix ?? false;
86
87
  const fixed = [];
87
88
  const flagged = [];
88
- for (const subdir of STASH_SUBDIRS) {
89
+ const dirsToScan = options.typeFilter ? STASH_SUBDIRS.filter((d) => d === options.typeFilter) : STASH_SUBDIRS;
90
+ for (const subdir of dirsToScan) {
89
91
  const dirPath = path.join(stashRoot, subdir);
90
92
  // Tasks are .yml files; everything else is .md
91
93
  const files = subdir === "tasks" ? collectYamlFiles(dirPath) : collectMarkdownFiles(dirPath);
@@ -97,7 +99,9 @@ export function akmLint(options = {}) {
97
99
  if (entry.isDirectory()) {
98
100
  const subdirIssues = linter.lintDirectory(path.join(dirPath, entry.name), stashRoot);
99
101
  for (const issue of subdirIssues) {
100
- if (issue.fixed) {
102
+ // Tristate-safe: only `true` counts as fixed; `false` and "failed"
103
+ // are both flagged.
104
+ if (issue.fixed === true) {
101
105
  fixed.push(issue);
102
106
  }
103
107
  else {
@@ -108,6 +112,9 @@ export function akmLint(options = {}) {
108
112
  }
109
113
  }
110
114
  for (const filePath of files) {
115
+ // Skip registry-cached read-only files — --fix must not mutate them.
116
+ if (filePath.includes("/.cache/") || filePath.includes("/registry/"))
117
+ continue;
111
118
  const relPath = path.relative(stashRoot, filePath);
112
119
  let raw;
113
120
  try {
@@ -142,10 +149,11 @@ export function akmLint(options = {}) {
142
149
  fileDeleted = true;
143
150
  fixed.push(issue);
144
151
  }
145
- else if (issue.fixed) {
152
+ else if (issue.fixed === true) {
146
153
  fixed.push(issue);
147
154
  }
148
155
  else {
156
+ // fixed === false (not fixable / no fix requested) or "failed" (fix attempted but threw)
149
157
  flagged.push(issue);
150
158
  }
151
159
  }
@@ -159,16 +167,26 @@ export function akmLint(options = {}) {
159
167
  // findings go into `flagged`, never `fixed`.
160
168
  const envRoots = [stashRoot, ...extraStashRoots];
161
169
  for (const root of envRoots) {
162
- const dir = path.join(root, "env");
163
- if (!fs.existsSync(dir))
164
- continue;
165
- for (const envPath of collectEnvFiles(dir)) {
166
- const baseName = path.basename(envPath, ".env");
167
- // "default" (or empty) maps to ".env" → env:default
168
- const ref = baseName === "" ? "env:default" : `env:${baseName}`;
169
- const relPath = path.relative(root, envPath);
170
- for (const issue of checkEnvForDangerousKeys(envPath, relPath, ref)) {
171
- flagged.push(issue);
170
+ // The `env` assets live under `env/` (ref prefix `env:`); whole-file
171
+ // `secret` assets live under `secrets/` (canonical ref prefix `secret:`,
172
+ // singular). Map the scan directory to its canonical ref prefix so the
173
+ // finding's `Ref:` field matches what `akm show`/`akm secret` accept.
174
+ for (const { scanSubdir, refPrefix } of [
175
+ { scanSubdir: "env", refPrefix: "env" },
176
+ { scanSubdir: "secrets", refPrefix: "secret" },
177
+ ]) {
178
+ const dir = path.join(root, scanSubdir);
179
+ if (!fs.existsSync(dir))
180
+ continue;
181
+ for (const envPath of collectEnvFiles(dir)) {
182
+ const baseName = path.basename(envPath, ".env");
183
+ // A dotfile literally named `.env` has an empty baseName — use the full
184
+ // basename so it doesn't collide with `default.env` → refPrefix:default.
185
+ const ref = baseName === "" ? `${refPrefix}:.env` : `${refPrefix}:${baseName}`;
186
+ const relPath = path.relative(root, envPath);
187
+ for (const issue of checkEnvForDangerousKeys(envPath, relPath, ref)) {
188
+ flagged.push(issue);
189
+ }
172
190
  }
173
191
  }
174
192
  }
@@ -33,7 +33,7 @@ export class MemoryLinter extends BaseLinter {
33
33
  file: ctx.relPath,
34
34
  issue: "orphaned-stub",
35
35
  detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
36
- fixed: false,
36
+ fixed: "failed",
37
37
  });
38
38
  }
39
39
  // Signal caller to skip remaining checks via a sentinel issue
@@ -4,6 +4,7 @@
4
4
  import { AgentLinter } from "./agent-linter.js";
5
5
  import { CommandLinter } from "./command-linter.js";
6
6
  import { DefaultLinter } from "./default-linter.js";
7
+ import { FactLinter } from "./fact-linter.js";
7
8
  import { KnowledgeLinter } from "./knowledge-linter.js";
8
9
  import { MemoryLinter } from "./memory-linter.js";
9
10
  import { SkillLinter } from "./skill-linter.js";
@@ -18,15 +19,19 @@ const LINTERS = [
18
19
  new KnowledgeLinter(),
19
20
  new SkillLinter(),
20
21
  new TaskLinter(),
21
- new DefaultLinter(),
22
+ new FactLinter(),
22
23
  ];
24
+ // Single shared DefaultLinter instance — used both as the explicit "lessons"
25
+ // handler and as the fallback for any unrecognised asset type.
26
+ const DEFAULT_LINTER = new DefaultLinter();
23
27
  const LINTER_MAP = new Map();
24
28
  for (const linter of LINTERS) {
25
29
  for (const t of linter.types) {
26
30
  LINTER_MAP.set(t, linter);
27
31
  }
28
32
  }
29
- const DEFAULT_LINTER = new DefaultLinter();
33
+ // Register "lessons" explicitly so there is only one DefaultLinter instance.
34
+ LINTER_MAP.set("lessons", DEFAULT_LINTER);
30
35
  /**
31
36
  * Return the appropriate linter for the given stash subdirectory name.
32
37
  * Falls back to `DefaultLinter` for unknown types.
@@ -28,9 +28,9 @@ export class TaskLinter extends BaseLinter {
28
28
  if (!("schedule" in ctx.data) || typeof ctx.data.schedule !== "string" || ctx.data.schedule.trim() === "") {
29
29
  missing.push("schedule");
30
30
  }
31
- // enabled: must be present (boolean value of false is valid)
32
- if (!("enabled" in ctx.data)) {
33
- missing.push("enabled");
31
+ // enabled: must be present AND a boolean (value of false is valid, but "yes" / 1 are not)
32
+ if (!("enabled" in ctx.data) || typeof ctx.data.enabled !== "boolean") {
33
+ missing.push("enabled (must be a boolean)");
34
34
  }
35
35
  // At least one of: prompt, workflow, or command
36
36
  const hasTarget = "prompt" in ctx.data || "workflow" in ctx.data || "command" in ctx.data;
@@ -2,6 +2,7 @@
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
  import fs from "node:fs";
5
+ import { parseWorkflow } from "../../workflows/parser.js";
5
6
  import { BaseLinter } from "./base-linter.js";
6
7
  const PLACEHOLDER_STRINGS = ["Describe what this workflow accomplishes", "Example Workflow"];
7
8
  /**
@@ -32,7 +33,7 @@ export class WorkflowLinter extends BaseLinter {
32
33
  file: ctx.relPath,
33
34
  issue: "placeholder-stub",
34
35
  detail: `could not delete: ${e instanceof Error ? e.message : String(e)}`,
35
- fixed: false,
36
+ fixed: "failed",
36
37
  });
37
38
  }
38
39
  return issues;
@@ -44,6 +45,30 @@ export class WorkflowLinter extends BaseLinter {
44
45
  fixed: false,
45
46
  });
46
47
  }
48
+ const isReadOnly = ctx.filePath.includes("/.cache/") || ctx.filePath.includes("/registry/");
49
+ if (!isReadOnly) {
50
+ try {
51
+ const result = parseWorkflow(ctx.raw, { path: ctx.filePath });
52
+ if (!result.ok) {
53
+ for (const err of result.errors ?? []) {
54
+ issues.push({
55
+ file: ctx.relPath,
56
+ issue: "invalid-workflow-structure",
57
+ detail: err.message ?? String(err),
58
+ fixed: false,
59
+ });
60
+ }
61
+ }
62
+ }
63
+ catch (e) {
64
+ issues.push({
65
+ file: ctx.relPath,
66
+ issue: "invalid-workflow-structure",
67
+ detail: `workflow parser error: ${e instanceof Error ? e.message : String(e)}`,
68
+ fixed: false,
69
+ });
70
+ }
71
+ }
47
72
  return issues;
48
73
  }
49
74
  #checkPlaceholderStub(body) {
@@ -28,7 +28,7 @@ import { parsePositiveIntFlag } from "../cli/parse-args.js";
28
28
  import { defineJsonCommand, output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared.js";
29
29
  import { closeDatabase, collectTagSetFromEntries, openExistingDatabase } from "../indexer/db/db.js";
30
30
  import { EMBEDDED_HINTS, EMBEDDED_HINTS_FULL } from "../output/cli-hints.js";
31
- import { getHyphenatedArg, getOutputMode, parseDetailLevel } from "../output/context.js";
31
+ import { getOutputMode, parseDetailLevel } from "../output/context.js";
32
32
  import { formatEventLine } from "../output/text.js";
33
33
  import { getDirname } from "../runtime.js";
34
34
  import { akmEventsList, akmEventsTail } from "./events.js";
@@ -90,9 +90,9 @@ const eventsTailCommand = defineCommand({
90
90
  },
91
91
  async run({ args }) {
92
92
  await runWithJsonErrors(async () => {
93
- const intervalMs = parsePositiveIntFlag(getHyphenatedArg(args, "interval-ms"), "--interval-ms");
94
- const maxDurationMs = parsePositiveIntFlag(getHyphenatedArg(args, "max-duration-ms"), "--max-duration-ms");
95
- const maxEvents = parsePositiveIntFlag(getHyphenatedArg(args, "max-events"), "--max-events");
93
+ const intervalMs = parsePositiveIntFlag(args["interval-ms"], "--interval-ms");
94
+ const maxDurationMs = parsePositiveIntFlag(args["max-duration-ms"], "--max-duration-ms");
95
+ const maxEvents = parsePositiveIntFlag(args["max-events"], "--max-events");
96
96
  const mode = getOutputMode();
97
97
  // In streaming text mode we want each event to print as soon as it
98
98
  // arrives. The polling loop emits via `onEvent`; the final result is
@@ -17,9 +17,9 @@
17
17
  import fs from "node:fs";
18
18
  import { z } from "zod";
19
19
  import { UsageError } from "../../core/errors.js";
20
- import { PROPOSAL_SOURCES } from "./validators/proposals.js";
20
+ import { PROPOSAL_SOURCES } from "./repository.js";
21
21
  // Valid `generator` values for a drain rule are exactly the canonical proposal
22
- // `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/validators/proposals.ts). The
22
+ // `source` values (see {@link PROPOSAL_SOURCES} in src/commands/proposal/repository.ts). The
23
23
  // engine matches rules via `policy.accept.find(r => r.generator === proposal.source)`,
24
24
  // so a generator that is not a real source can never match — it would be a
25
25
  // silent permanent no-op. Validate against the closed set to surface typos.
@@ -39,12 +39,20 @@ const GeneratorSchema = z.enum(PROPOSAL_SOURCES, {
39
39
  export const PERSONAL_STASH = {
40
40
  name: "personal-stash",
41
41
  accept: [
42
- // Extract proposals carry freshly-pulled real content — accept when present.
43
- { generator: "extract", minContentLines: 1 },
42
+ // Extract proposals carry freshly-pulled real content — accept when present,
43
+ // but cap the diff for parity with reflect(80)/consolidate(200): an
44
+ // arbitrarily large extract should not auto-promote with zero LLM calls.
45
+ { generator: "extract", minContentLines: 1, maxDiffLines: 200 },
44
46
  // Reflect refinements: accept small ones; larger refinements defer to review.
45
47
  { generator: "reflect", maxDiffLines: 80 },
46
48
  // Consolidate within the diff band; mid-band lands in `defer` below.
47
49
  { generator: "consolidate", maxDiffLines: 200 },
50
+ // Recombine: accept only confirmed type:lesson proposals (promoted by the
51
+ // recombine confidence gate) within the diff band. type:hypothesis proposals
52
+ // have no matching rule here and stay pending on the audited-autonomous
53
+ // queue until recombine supersedes them or the 90-day TTL sweeps them —
54
+ // there is no manual-review rung (06-M3).
55
+ { generator: "recombine", requireType: "lesson", maxDiffLines: 200 },
48
56
  ],
49
57
  rejectEmpty: true,
50
58
  // Mid-band consolidate, distill duplicates, and contradiction escalations are
@@ -83,6 +91,7 @@ const DrainAcceptRuleSchema = z
83
91
  generator: GeneratorSchema,
84
92
  maxDiffLines: z.number().int().positive().optional(),
85
93
  minContentLines: z.number().int().nonnegative().optional(),
94
+ requireType: z.string().optional(),
86
95
  })
87
96
  .strict();
88
97
  const DrainPolicySchema = z
@@ -9,7 +9,7 @@
9
9
  * the engine behind `akm proposal drain` and (later) the `triage` improve
10
10
  * pre-pass.
11
11
  *
12
- * Design (see docs/technical/proposal-triage-implementation-plan.md):
12
+ * Design (see docs/archive/proposal-triage-implementation-plan.md):
13
13
  * - Reuses `listProposals` (no source filter — generator filtering is
14
14
  * in-memory) and the `akmProposalAccept` / `akmProposalReject` wrappers from
15
15
  * `proposal.ts` so the standard `promoted` / `rejected` events are emitted.
@@ -36,17 +36,15 @@
36
36
  */
37
37
  import fs from "node:fs";
38
38
  import path from "node:path";
39
- import { assertNever } from "../../core/assert.js";
40
39
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
41
40
  import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
42
41
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
43
42
  import { appendEvent } from "../../core/events.js";
44
43
  import { info, warn } from "../../core/warn.js";
45
- import { runAgent } from "../../integrations/agent/index.js";
46
- import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
44
+ import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
47
45
  import { chatCompletion, stripJsonFences } from "../../llm/client.js";
48
46
  import { akmProposalAccept, akmProposalReject } from "./proposal.js";
49
- import { listProposals, recordGateDecision } from "./validators/proposals.js";
47
+ import { listProposals, recordGateDecision } from "./repository.js";
50
48
  // ---------------------------------------------------------------------------
51
49
  // Content helpers
52
50
  // ---------------------------------------------------------------------------
@@ -80,7 +78,16 @@ export function classifyProposal(proposal, policy, maxDiffLines) {
80
78
  if (policy.rejectEmpty && isEmptyDiff(proposal)) {
81
79
  return { verdict: "reject", reason: "empty diff", gate: { reason: "empty-diff" } };
82
80
  }
83
- const rule = policy.accept.find((r) => r.generator === proposal.source);
81
+ const rule = policy.accept.find((r) => {
82
+ if (r.generator !== proposal.source)
83
+ return false;
84
+ if (r.requireType !== undefined) {
85
+ const fm = parseFrontmatter(proposal.payload.content ?? "").data;
86
+ if (typeof fm.type !== "string" || fm.type !== r.requireType)
87
+ return false;
88
+ }
89
+ return true;
90
+ });
84
91
  if (rule) {
85
92
  const lines = contentLineCount(content);
86
93
  const body = contentBodyLineCount(content);
@@ -200,59 +207,38 @@ export function parseJudgmentVerdict(raw) {
200
207
  return { decision, reason: typeof reason === "string" ? reason : "" };
201
208
  }
202
209
  /**
203
- * Dispatch a single judgment prompt to the resolved runner. The switch mirrors
204
- * the canonical consumer at `reflect.ts:1060-1090`: llm `chatCompletion`
205
- * (no filesystem), agent `runAgent`, sdk `runOpencodeSdk`.
210
+ * Dispatch a single judgment prompt to the resolved runner via the unified
211
+ * {@link executeRunner} seam (X3). The `llm` arm is drain-specific (wraps
212
+ * `chatCompletion` — no filesystem) so it is supplied as the `llm` handler; the
213
+ * byte-identical `agent` / `sdk` arms route to the default profile runners (or
214
+ * the injected {@link JudgmentSeams} test fakes). A failed spawn warns and
215
+ * yields `null`, matching the prior per-arm behavior.
206
216
  */
207
217
  async function dispatchJudgment(runner, prompt, seams) {
208
- let raw;
209
- switch (runner.kind) {
210
- case "llm": {
211
- const messages = [{ role: "user", content: prompt }];
212
- raw = seams.chat
213
- ? await seams.chat(runner, messages)
214
- : await chatCompletion(runner.connection, messages, {
215
- ...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
216
- });
217
- break;
218
- }
219
- case "agent": {
220
- const stdout = await runProfileJudgment(seams.runAgentFn ?? runAgent, runner.profile, prompt, "agent", runner.timeoutMs);
221
- if (stdout === null)
222
- return null;
223
- raw = stdout;
224
- break;
225
- }
226
- case "sdk": {
227
- const stdout = await runProfileJudgment(seams.runSdkFn ?? runOpencodeSdk, runner.profile, prompt, "sdk", runner.timeoutMs);
228
- if (stdout === null)
229
- return null;
230
- raw = stdout;
231
- break;
232
- }
233
- default:
234
- // Exhaustiveness arm (H1): a 4th RunnerSpec kind becomes a compile error
235
- // here instead of leaving `raw` undefined at runtime.
236
- return assertNever(runner);
237
- }
238
- return parseJudgmentVerdict(raw);
239
- }
240
- /**
241
- * Shared spawn-runner dispatch for the byte-identical `agent` / `sdk` arms: run
242
- * the profile-based runner, log a labelled warning on failure, and return the
243
- * captured stdout (or `null` on failure). `label` only changes the warn prefix.
244
- */
245
- async function runProfileJudgment(run, profile, prompt, label, timeoutMs) {
246
- const result = await run(profile, prompt, {
218
+ const runOptions = {
247
219
  stdio: "captured",
248
220
  parseOutput: "text",
249
- ...(timeoutMs !== undefined ? { timeoutMs } : {}),
221
+ ...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
222
+ };
223
+ const result = await executeRunner(runner, prompt, runOptions, {
224
+ llm: async (spec, p) => {
225
+ const messages = [{ role: "user", content: p }];
226
+ const raw = seams.chat
227
+ ? await seams.chat(spec, messages)
228
+ : await chatCompletion(spec.connection, messages, {
229
+ ...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
230
+ });
231
+ // chatCompletion has no failure envelope — a returned string is success.
232
+ return { ok: true, exitCode: 0, stdout: raw, stderr: "", durationMs: 0 };
233
+ },
234
+ ...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
235
+ ...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
250
236
  });
251
237
  if (!result.ok) {
252
- warn(`[triage] judgment ${label} failed: ${result.error ?? result.reason ?? "unknown error"}`);
238
+ warn(`[triage] judgment ${runner.kind} failed: ${result.error ?? result.reason ?? "unknown error"}`);
253
239
  return null;
254
240
  }
255
- return result.stdout;
241
+ return parseJudgmentVerdict(result.stdout);
256
242
  }
257
243
  /**
258
244
  * Run the judgment tier over the deferred items. The runner only *judges*; the
@@ -361,6 +347,14 @@ export async function drainProposals(opts, promoteFn = akmProposalAccept, reject
361
347
  // — these are re-stamped `no-judge-configured` when no runner resolves them.
362
348
  const needsJudge = new Set();
363
349
  for (const proposal of pending) {
350
+ // Do NOT reclassify a proposal that was already conclusively stamped
351
+ // `auto-rejected` by a prior gate run (e.g. the improve confidence gate).
352
+ // Overwriting an authoritative rejection with `auto-accepted` would corrupt
353
+ // the audit trail and silently promote content the gate explicitly rejected.
354
+ // Such proposals remain pending until the TTL expires them — the queue is
355
+ // audited-autonomous; no manual-review rung exists (06-M3).
356
+ if (proposal.gateDecision?.outcome === "auto-rejected")
357
+ continue;
364
358
  const decision = classifyProposal(proposal, opts.policy, opts.maxDiffLines);
365
359
  if (decision === null)
366
360
  continue;
@@ -0,0 +1,115 @@
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
+ * Legacy filesystem proposal import (#578).
6
+ *
7
+ * Before 0.9.0 proposals lived as per-uuid JSON directories under
8
+ * `<stashDir>/.akm/proposals/` (live) and `…/proposals/archive/` (archived).
9
+ * The first proposal operation against a stash imports any legacy
10
+ * `proposal.json` files into the `proposals` table (INSERT OR IGNORE keyed on
11
+ * the UUID, so re-runs never duplicate) and records the stash in
12
+ * `proposal_fs_imports` so later invocations skip the directory walk. The
13
+ * legacy files are left in place untouched — they are inert after import and
14
+ * may be removed by the operator at leisure.
15
+ */
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { warn } from "../../core/warn.js";
19
+ import { hasImportedFsProposals, insertProposalIfAbsent, recordFsProposalsImport, } from "../../storage/repositories/proposals-repository.js";
20
+ /** Legacy (pre-0.9.0) proposal directory: `<stashDir>/.akm/proposals[/archive]`. */
21
+ function legacyProposalsRoot(stashDir, archive) {
22
+ const root = path.join(stashDir, ".akm", "proposals");
23
+ return archive ? path.join(root, "archive") : root;
24
+ }
25
+ /**
26
+ * One-shot import of legacy `proposal.json` files into the `proposals` table.
27
+ *
28
+ * Idempotent at two levels: the `proposal_fs_imports` ledger skips the
29
+ * directory walk after the first successful import, and INSERT OR IGNORE
30
+ * (keyed on the proposal UUID) protects against duplicates even if the walk
31
+ * re-runs. Legacy `backup.<ext>` files are inlined into `backupContent` so
32
+ * `akm proposal revert` keeps working for proposals accepted before 0.9.0.
33
+ *
34
+ * The legacy files are never modified or deleted — after import they are
35
+ * inert artifacts the operator can remove at leisure.
36
+ */
37
+ export function importLegacyProposalFiles(db, stashDir) {
38
+ if (hasImportedFsProposals(db, stashDir))
39
+ return;
40
+ const liveRoot = legacyProposalsRoot(stashDir, false);
41
+ if (!fs.existsSync(liveRoot))
42
+ return;
43
+ let imported = 0;
44
+ for (const archive of [false, true]) {
45
+ const root = legacyProposalsRoot(stashDir, archive);
46
+ let entries;
47
+ try {
48
+ entries = fs.readdirSync(root, { withFileTypes: true });
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ for (const entry of entries) {
54
+ if (!entry.isDirectory() || entry.name === "archive")
55
+ continue;
56
+ const proposalDir = path.join(root, entry.name);
57
+ const proposal = readLegacyProposalFile(proposalDir);
58
+ if (!proposal)
59
+ continue;
60
+ if (insertProposalIfAbsent(db, proposal, stashDir))
61
+ imported += 1;
62
+ }
63
+ }
64
+ recordFsProposalsImport(db, stashDir, imported);
65
+ if (imported > 0) {
66
+ warn(`[proposals] imported ${imported} legacy proposal file(s) from ${liveRoot} into state.db`);
67
+ }
68
+ }
69
+ /**
70
+ * Parse one legacy proposal directory into a {@link Proposal}, inlining the
71
+ * backup file (when present) as `backupContent`. Returns undefined — with a
72
+ * warning — when the `proposal.json` is missing, unreadable, or malformed, so
73
+ * a single corrupt legacy entry never blocks the import of the rest.
74
+ */
75
+ function readLegacyProposalFile(proposalDir) {
76
+ const filePath = path.join(proposalDir, "proposal.json");
77
+ let parsed;
78
+ try {
79
+ parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
80
+ }
81
+ catch (err) {
82
+ warn(`[proposals] skipping legacy proposal at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
83
+ return undefined;
84
+ }
85
+ if (typeof parsed !== "object" ||
86
+ parsed === null ||
87
+ typeof parsed.id !== "string" ||
88
+ typeof parsed.ref !== "string") {
89
+ warn(`[proposals] skipping legacy proposal at ${filePath}: not a proposal object`);
90
+ return undefined;
91
+ }
92
+ const { backup, ...rest } = parsed;
93
+ let backupContent;
94
+ if (typeof backup === "string" && backup.length > 0) {
95
+ try {
96
+ backupContent = fs.readFileSync(path.join(proposalDir, backup), "utf8");
97
+ }
98
+ catch {
99
+ // Backup file lost — import the proposal anyway; revert for it will
100
+ // surface "no backup available", same as a new-asset proposal.
101
+ }
102
+ }
103
+ return {
104
+ ...rest,
105
+ payload: {
106
+ content: rest.payload?.content ?? "",
107
+ ...(rest.payload?.frontmatter ? { frontmatter: rest.payload.frontmatter } : {}),
108
+ },
109
+ createdAt: rest.createdAt ?? "",
110
+ updatedAt: rest.updatedAt ?? rest.createdAt ?? "",
111
+ status: rest.status ?? "pending",
112
+ source: rest.source ?? "import",
113
+ ...(backupContent !== undefined ? { backupContent } : {}),
114
+ };
115
+ }