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
  * `akm tasks` — register, inspect, run, and remove scheduled task assets.
3
6
  *
@@ -7,7 +10,7 @@
7
10
  */
8
11
  import fs from "node:fs";
9
12
  import path from "node:path";
10
- import { parseAssetRef } from "../core/asset-ref";
13
+ import { stringify as yamlStringify } from "yaml";
11
14
  import { resolveAssetPathFromName } from "../core/asset-spec";
12
15
  import { isWithin, resolveStashDir } from "../core/common";
13
16
  import { loadConfig } from "../core/config";
@@ -21,10 +24,16 @@ import { resolveAkmInvocation } from "../tasks/resolveAkmBin";
21
24
  import { exitCodeForStatus, readTaskHistory, runTask } from "../tasks/runner";
22
25
  import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT, translateToCron } from "../tasks/schedule";
23
26
  import { validateTaskDocument } from "../tasks/validator";
27
+ import { resolveImproveProfile } from "./improve-profiles";
24
28
  export async function akmTasksAdd(input) {
25
29
  const id = normaliseTaskId(input.id);
26
- if ((input.workflow && input.prompt) || (!input.workflow && !input.prompt)) {
27
- throw new UsageError("Pass exactly one of --workflow <ref> or --prompt <inline|asset-ref|./file.md>.", "INVALID_FLAG_VALUE");
30
+ const hasCommand = input.command !== undefined &&
31
+ input.command !== null &&
32
+ !(typeof input.command === "string" && input.command.trim() === "") &&
33
+ !(Array.isArray(input.command) && input.command.length === 0);
34
+ const targetCount = [Boolean(input.workflow), Boolean(input.prompt), hasCommand].filter(Boolean).length;
35
+ if (targetCount !== 1) {
36
+ throw new UsageError("Pass exactly one of --workflow <ref>, --prompt <asset-ref|./file.md|text>, or --command <shell-command>.", "INVALID_FLAG_VALUE");
28
37
  }
29
38
  // Validate the schedule for the active backend before writing anything.
30
39
  const backend = backendNameForPlatform();
@@ -39,20 +48,23 @@ export async function akmTasksAdd(input) {
39
48
  if (fs.existsSync(assetPath) && !input.force) {
40
49
  throw new UsageError(`Task "${id}" already exists. Pass --force to overwrite, or use \`akm tasks remove ${id}\` first.`, "RESOURCE_ALREADY_EXISTS");
41
50
  }
42
- const markdown = renderTaskMarkdown({
51
+ const yaml = renderTaskYaml({
43
52
  id,
44
53
  schedule: input.schedule,
45
54
  workflow: input.workflow,
46
55
  prompt: input.prompt,
56
+ command: input.command,
47
57
  profile: input.profile,
48
58
  params: input.params,
59
+ name: input.name,
49
60
  description: input.description,
61
+ when_to_use: input.when_to_use,
50
62
  tags: input.tags,
51
63
  enabled: input.disabled !== true,
52
64
  });
53
- const task = parseTaskDocument({ markdown, filePath: assetPath, id });
65
+ const task = parseTaskDocument({ yaml, filePath: assetPath, id });
54
66
  await validateTaskDocument(task, { backend, stashDir });
55
- fs.writeFileSync(assetPath, markdown.endsWith("\n") ? markdown : `${markdown}\n`, "utf8");
67
+ fs.writeFileSync(assetPath, yaml.endsWith("\n") ? yaml : `${yaml}\n`, "utf8");
56
68
  // Install in the OS scheduler. If install fails after the file was written,
57
69
  // delete the file so the on-disk state never claims a task is registered
58
70
  // when it isn't.
@@ -80,19 +92,58 @@ export async function akmTasksAdd(input) {
80
92
  target: task.target,
81
93
  };
82
94
  }
95
+ /**
96
+ * Emit a single grouped stderr warning for legacy `.md` task files in the
97
+ * tasks directory. 0.8.0 requires task definitions to be pure `.yml`; any
98
+ * leftover `.md` files from 0.7.x would otherwise be silently skipped, which
99
+ * makes scheduled tasks vanish without operator notice. We do NOT auto-migrate
100
+ * — that is a separate workstream — but operators must see the affected files.
101
+ *
102
+ * `seen` is module-level so the warning is emitted at most once per process,
103
+ * even when both `akm tasks list` and `akm tasks sync` are invoked in the same
104
+ * akm run.
105
+ */
106
+ const warnedLegacyMdDirs = new Set();
107
+ function warnLegacyMdTaskFiles(typeRoot) {
108
+ if (warnedLegacyMdDirs.has(typeRoot))
109
+ return;
110
+ let mdFiles;
111
+ try {
112
+ mdFiles = fs.readdirSync(typeRoot).filter((f) => f.endsWith(".md"));
113
+ }
114
+ catch {
115
+ return;
116
+ }
117
+ if (mdFiles.length === 0)
118
+ return;
119
+ warnedLegacyMdDirs.add(typeRoot);
120
+ const affected = mdFiles.map((f) => `tasks/${f}`).join(", ");
121
+ process.stderr.write(`WARNING: ${mdFiles.length} task file(s) use the legacy .md format and were ignored.\n` +
122
+ ` AKM 0.8.0 requires tasks as pure .yml. See docs/migration/v0.7-to-v0.8.md#task-definition-files-mdfrontmatter--yml.\n` +
123
+ ` Affected: ${affected}\n`);
124
+ }
125
+ /**
126
+ * Reset the legacy `.md` task warning de-duplication state. Test-only escape
127
+ * hatch — production code should never call this.
128
+ */
129
+ export function _resetLegacyMdTaskWarningStateForTests() {
130
+ warnedLegacyMdDirs.clear();
131
+ }
83
132
  export async function akmTasksList() {
84
133
  const stashDir = resolveStashDir();
85
134
  const typeRoot = path.join(stashDir, "tasks");
86
135
  if (!fs.existsSync(typeRoot))
87
136
  return { tasks: [] };
88
- const files = fs.readdirSync(typeRoot).filter((f) => f.endsWith(".md"));
137
+ const entries = fs.readdirSync(typeRoot);
138
+ warnLegacyMdTaskFiles(typeRoot);
139
+ const files = entries.filter((f) => f.endsWith(".yml"));
89
140
  const tasks = [];
90
141
  for (const file of files) {
91
- const id = file.slice(0, -3);
142
+ const id = file.slice(0, -4);
92
143
  const filePath = path.join(typeRoot, file);
93
144
  let task;
94
145
  try {
95
- task = parseTaskDocument({ markdown: fs.readFileSync(filePath, "utf8"), filePath, id });
146
+ task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
96
147
  }
97
148
  catch {
98
149
  continue; // skip malformed files; `akm tasks show <id>` will surface the error
@@ -104,7 +155,9 @@ export async function akmTasksList() {
104
155
  schedule: task.schedule,
105
156
  enabled: task.enabled,
106
157
  target: task.target,
158
+ name: task.name,
107
159
  description: task.description,
160
+ when_to_use: task.when_to_use,
108
161
  tags: task.tags,
109
162
  });
110
163
  }
@@ -113,9 +166,12 @@ export async function akmTasksList() {
113
166
  export async function akmTasksShow(id) {
114
167
  const normalised = normaliseTaskId(id);
115
168
  const stashDir = resolveStashDir();
169
+ const typeRoot = path.join(stashDir, "tasks");
170
+ if (fs.existsSync(typeRoot))
171
+ warnLegacyMdTaskFiles(typeRoot);
116
172
  const filePath = await resolveAssetPath(stashDir, "task", normalised);
117
173
  const task = parseTaskDocument({
118
- markdown: fs.readFileSync(filePath, "utf8"),
174
+ yaml: fs.readFileSync(filePath, "utf8"),
119
175
  filePath,
120
176
  id: normalised,
121
177
  });
@@ -128,13 +184,18 @@ export async function akmTasksShow(id) {
128
184
  cron: translateToCron(spec),
129
185
  enabled: task.enabled,
130
186
  target: task.target,
187
+ name: task.name,
131
188
  description: task.description,
189
+ when_to_use: task.when_to_use,
132
190
  tags: task.tags,
133
191
  };
134
192
  }
135
193
  export async function akmTasksRemove(id) {
136
194
  const normalised = normaliseTaskId(id);
137
195
  const stashDir = resolveStashDir();
196
+ const typeRoot = path.join(stashDir, "tasks");
197
+ if (fs.existsSync(typeRoot))
198
+ warnLegacyMdTaskFiles(typeRoot);
138
199
  const filePath = await resolveAssetPath(stashDir, "task", normalised);
139
200
  const sched = selectBackend();
140
201
  try {
@@ -148,24 +209,31 @@ export async function akmTasksRemove(id) {
148
209
  export async function akmTasksSetEnabled(id, enabled) {
149
210
  const normalised = normaliseTaskId(id);
150
211
  const stashDir = resolveStashDir();
212
+ const typeRoot = path.join(stashDir, "tasks");
213
+ if (fs.existsSync(typeRoot))
214
+ warnLegacyMdTaskFiles(typeRoot);
151
215
  const filePath = await resolveAssetPath(stashDir, "task", normalised);
152
- const markdown = fs.readFileSync(filePath, "utf8");
153
- const updated = setEnabledInMarkdown(markdown, enabled);
216
+ const yaml = fs.readFileSync(filePath, "utf8");
217
+ const updated = setEnabledInYaml(yaml, enabled);
154
218
  fs.writeFileSync(filePath, updated, "utf8");
155
219
  const sched = selectBackend();
156
220
  try {
157
221
  await sched.setEnabled(normalised, enabled);
158
222
  }
159
223
  catch (err) {
160
- // Roll the file back so the markdown source-of-truth and the OS
224
+ // Roll the file back so the YAML source-of-truth and the OS
161
225
  // scheduler don't diverge silently when the backend call fails.
162
- fs.writeFileSync(filePath, markdown, "utf8");
226
+ fs.writeFileSync(filePath, yaml, "utf8");
163
227
  throw err;
164
228
  }
165
229
  return { id: normalised, enabled, backend: sched.name };
166
230
  }
167
231
  export async function akmTasksRun(id) {
168
232
  const normalised = normaliseTaskId(id);
233
+ const stashDir = resolveStashDir();
234
+ const typeRoot = path.join(stashDir, "tasks");
235
+ if (fs.existsSync(typeRoot))
236
+ warnLegacyMdTaskFiles(typeRoot);
169
237
  const result = await runTask(normalised);
170
238
  return {
171
239
  ok: result.status === "completed" || result.status === "disabled",
@@ -176,6 +244,10 @@ export async function akmTasksRun(id) {
176
244
  export async function akmTasksHistory(input) {
177
245
  const limit = input.limit !== undefined && input.limit > 0 ? input.limit : 50;
178
246
  const id = input.id ? normaliseTaskId(input.id) : undefined;
247
+ const stashDir = resolveStashDir();
248
+ const typeRoot = path.join(stashDir, "tasks");
249
+ if (fs.existsSync(typeRoot))
250
+ warnLegacyMdTaskFiles(typeRoot);
179
251
  return { rows: readTaskHistory({ id, limit }) };
180
252
  }
181
253
  /**
@@ -187,11 +259,13 @@ export async function akmTasksHistory(input) {
187
259
  export async function akmTasksSync() {
188
260
  const stashDir = resolveStashDir();
189
261
  const typeRoot = path.join(stashDir, "tasks");
262
+ if (fs.existsSync(typeRoot))
263
+ warnLegacyMdTaskFiles(typeRoot);
190
264
  const fileIds = fs.existsSync(typeRoot)
191
265
  ? fs
192
266
  .readdirSync(typeRoot)
193
- .filter((f) => f.endsWith(".md"))
194
- .map((f) => f.slice(0, -3))
267
+ .filter((f) => f.endsWith(".yml"))
268
+ .map((f) => f.slice(0, -4))
195
269
  : [];
196
270
  const sched = selectBackend();
197
271
  const backend = backendNameForPlatform();
@@ -200,10 +274,10 @@ export async function akmTasksSync() {
200
274
  const unchanged = [];
201
275
  const skipped = [];
202
276
  for (const id of fileIds) {
203
- const filePath = path.join(typeRoot, `${id}.md`);
277
+ const filePath = path.join(typeRoot, `${id}.yml`);
204
278
  let task;
205
279
  try {
206
- task = parseTaskDocument({ markdown: fs.readFileSync(filePath, "utf8"), filePath, id });
280
+ task = parseTaskDocument({ yaml: fs.readFileSync(filePath, "utf8"), filePath, id });
207
281
  }
208
282
  catch (err) {
209
283
  skipped.push({ id, reason: err instanceof Error ? err.message : String(err) });
@@ -243,10 +317,32 @@ export async function akmTasksDoctor() {
243
317
  catch (err) {
244
318
  warnings.push(err instanceof Error ? err.message : String(err));
245
319
  }
320
+ try {
321
+ const stashDir = resolveStashDir();
322
+ const typeRoot = path.join(stashDir, "tasks");
323
+ if (fs.existsSync(typeRoot))
324
+ warnLegacyMdTaskFiles(typeRoot);
325
+ }
326
+ catch {
327
+ // doctor must never fail on stash-resolution; the warning is best-effort
328
+ }
246
329
  const backend = backendNameForPlatform();
247
330
  const config = loadConfig();
248
- const defaultProfile = config.agent?.default;
249
- const profiles = listAgentProfileNames(config.agent);
331
+ // v2: prefer profiles.agent / defaults.agent; fall back to legacy agent.default
332
+ const defaultProfile = config.defaults?.agent;
333
+ const profiles = config.profiles?.agent ? Object.keys(config.profiles.agent) : listAgentProfileNames(config);
334
+ // §6.1: surface the effective triage settings for the default improve
335
+ // profile. The struct is a fixed shape, so this is a deliberate addition.
336
+ const improveProfileName = typeof config.defaults?.improve === "string" ? config.defaults.improve : "default";
337
+ const triage = resolveImproveProfile(config.defaults?.improve, config).processes?.triage;
338
+ const improveTriage = triage
339
+ ? {
340
+ defaultProfile: improveProfileName,
341
+ enabled: triage.enabled === true,
342
+ applyMode: triage.applyMode ?? "queue",
343
+ policy: triage.policy ?? "personal-stash",
344
+ }
345
+ : undefined;
250
346
  return {
251
347
  backend,
252
348
  akm: invocation,
@@ -255,12 +351,15 @@ export async function akmTasksDoctor() {
255
351
  agent: { defaultProfile, available: profiles },
256
352
  scheduleSubset: SCHEDULE_SUPPORTED_SUBSET_HINT,
257
353
  warnings,
354
+ ...(improveTriage ? { improveTriage } : {}),
258
355
  };
259
356
  }
260
357
  // ── helpers ─────────────────────────────────────────────────────────────────
261
358
  const VALID_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
262
359
  function normaliseTaskId(raw) {
263
- const id = raw.trim().replace(/\.md$/, "");
360
+ // Accept both .yml and .md suffixes from users so muscle memory from the
361
+ // pre-0.8.0 markdown task format doesn't produce a confusing "task not found".
362
+ const id = raw.trim().replace(/\.(yml|md)$/, "");
264
363
  if (!id) {
265
364
  throw new UsageError("Task id must be non-empty.", "MISSING_REQUIRED_ARGUMENT");
266
365
  }
@@ -269,63 +368,35 @@ function normaliseTaskId(raw) {
269
368
  }
270
369
  return id;
271
370
  }
272
- function renderTaskMarkdown(input) {
273
- const lines = ["---"];
274
- lines.push(`schedule: ${yamlQuote(input.schedule)}`);
371
+ function renderTaskYaml(input) {
372
+ const obj = { schedule: input.schedule };
275
373
  if (input.workflow) {
276
- lines.push(`workflow: ${yamlQuote(input.workflow)}`);
374
+ obj.workflow = input.workflow;
277
375
  if (input.params) {
278
- const parsed = parseJsonObjectArg(input.params);
279
- lines.push("params:");
280
- for (const [k, v] of Object.entries(parsed)) {
281
- lines.push(` ${k}: ${yamlScalarValue(v)}`);
282
- }
376
+ obj.params = parseJsonObjectArg(input.params);
283
377
  }
284
378
  }
285
379
  else if (input.prompt) {
286
- if (looksLikeAssetRef(input.prompt) || isFilePath(input.prompt) || input.prompt === "inline") {
287
- lines.push(`prompt: ${yamlQuote(input.prompt)}`);
288
- }
289
- else {
290
- lines.push(`prompt: inline`);
291
- }
380
+ obj.prompt = input.prompt;
292
381
  if (input.profile)
293
- lines.push(`profile: ${yamlQuote(input.profile)}`);
382
+ obj.profile = input.profile;
294
383
  }
295
- lines.push(`enabled: ${input.enabled}`);
296
- if (input.description)
297
- lines.push(`description: ${yamlQuote(input.description)}`);
298
- if (input.tags && input.tags.length > 0) {
299
- lines.push(`tags: [${input.tags.map((t) => yamlQuote(t)).join(", ")}]`);
384
+ else if (input.command !== undefined) {
385
+ // Emit a string when given a string, an array when given an array. The
386
+ // parser accepts both forms; preserving the caller's shape keeps the YAML
387
+ // ergonomic for humans editing the file later.
388
+ obj.command = input.command;
300
389
  }
301
- lines.push("---", "");
302
- if (input.workflow) {
303
- lines.push(`# Task: ${humanise(input.id)}`, "");
304
- }
305
- else if (input.prompt) {
306
- if (looksLikeAssetRef(input.prompt) || isFilePath(input.prompt) || input.prompt === "inline") {
307
- lines.push(`# Task: ${humanise(input.id)}`, "");
308
- }
309
- else {
310
- // Raw inline prompt — use the body itself.
311
- lines.push(input.prompt.trim(), "");
312
- }
313
- }
314
- return lines.join("\n");
315
- }
316
- function yamlQuote(value) {
317
- if (/^[A-Za-z_][A-Za-z0-9_.\-/:]*$/.test(value))
318
- return value;
319
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
320
- }
321
- function yamlScalarValue(v) {
322
- if (typeof v === "string")
323
- return yamlQuote(v);
324
- if (typeof v === "number" || typeof v === "boolean")
325
- return String(v);
326
- if (v === null)
327
- return "null";
328
- return JSON.stringify(v);
390
+ obj.enabled = input.enabled;
391
+ if (input.name)
392
+ obj.name = input.name;
393
+ if (input.description)
394
+ obj.description = input.description;
395
+ if (input.when_to_use)
396
+ obj.when_to_use = input.when_to_use;
397
+ if (input.tags && input.tags.length > 0)
398
+ obj.tags = input.tags;
399
+ return yamlStringify(obj);
329
400
  }
330
401
  function parseJsonObjectArg(raw) {
331
402
  let parsed;
@@ -340,31 +411,26 @@ function parseJsonObjectArg(raw) {
340
411
  }
341
412
  return parsed;
342
413
  }
343
- function looksLikeAssetRef(s) {
344
- return /^[a-z][a-z0-9_-]*:[^\s]/i.test(s) && !s.startsWith("./") && !s.startsWith("/");
345
- }
346
- function isFilePath(s) {
347
- return s.startsWith("./") || s.startsWith("../") || path.isAbsolute(s);
348
- }
349
- function humanise(id) {
350
- return id.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
351
- }
352
414
  /**
353
- * Toggle the `enabled:` value in a task markdown's frontmatter without doing
354
- * a round-trip through the parser+renderer (which would lose comments and
355
- * formatting choices). Inserts the key right before the closing `---` if
356
- * absent.
415
+ * Toggle the `enabled:` value in a task YAML file in-place without a full
416
+ * parse/render round-trip (which would reformat the file). Appends the key
417
+ * if absent.
418
+ *
419
+ * Preserves inline comments (e.g. `enabled: true # important`) and uses
420
+ * case-sensitive matching (YAML keys are case-sensitive).
357
421
  */
358
- export function setEnabledInMarkdown(markdown, enabled) {
359
- const m = markdown.match(/^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r\n|\r|\n|$))([\s\S]*)$/);
360
- if (!m) {
361
- throw new UsageError("Task markdown is missing frontmatter; cannot toggle enabled.", "INVALID_FLAG_VALUE");
422
+ export function setEnabledInYaml(yaml, enabled) {
423
+ // Match: key prefix (group 1), value (group 2), optional trailing comment (group 3)
424
+ const pattern = /^(enabled:\s*)([^\s#\r\n][^\r\n]*?)(\s*(?:#[^\r\n]*))?$/m;
425
+ if (pattern.test(yaml)) {
426
+ return yaml.replace(pattern, `$1${enabled}$3`);
427
+ }
428
+ // Handle the case where enabled: has no value yet (bare key)
429
+ const simplePattern = /^(enabled:)\s*$/m;
430
+ if (simplePattern.test(yaml)) {
431
+ return yaml.replace(simplePattern, `$1 ${enabled}`);
362
432
  }
363
- const [, openFence, fmBody, closeFence, body] = m;
364
- const replaced = fmBody.match(/(^|\r?\n)enabled:\s*[^\r\n]*/i)
365
- ? fmBody.replace(/(^|\r?\n)enabled:\s*[^\r\n]*/i, `$1enabled: ${enabled}`)
366
- : `${fmBody}\nenabled: ${enabled}`;
367
- return `${openFence}${replaced}${closeFence}${body}`;
433
+ return `${yaml.trimEnd()}\nenabled: ${enabled}\n`;
368
434
  }
369
435
  // Re-exported so tests can verify the validator path directly.
370
436
  // Re-export error classes consumed by callers that want to instanceof-check.
@@ -375,11 +441,11 @@ export { ConfigError, exitCodeForStatus, NotFoundError, parseTaskDocument, Usage
375
441
  // user passes a ref, we accept the bare name part too.
376
442
  export function parseTaskRef(input) {
377
443
  if (input.includes(":")) {
378
- const ref = parseAssetRef(input);
379
- if (ref.type !== "task") {
444
+ const [typePart, ...rest] = input.split(":");
445
+ if (typePart !== "task" || rest.length === 0) {
380
446
  throw new UsageError(`Expected a task id or task:<id> ref, got "${input}".`, "INVALID_FLAG_VALUE");
381
447
  }
382
- return { id: normaliseTaskId(ref.name) };
448
+ return { id: normaliseTaskId(rest.join(":")) };
383
449
  }
384
450
  return { id: normaliseTaskId(input) };
385
451
  }
@@ -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
  const URL_RE = /https?:\/\/[^\s"'<>)\]]+/g;
2
5
  const TIMEOUT_MS = 5000;
3
6
  const MAX_URLS = 20;
@@ -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
  import { defaultRendererRegistry } from "./asset-registry";
2
5
  function registryActionContributor(registry) {
3
6
  return {
@@ -1,6 +1,13 @@
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
  import path from "node:path";
2
5
  import { isAssetType } from "./common";
3
6
  import { UsageError } from "./errors";
7
+ /** Accepted spelling aliases mapping to a canonical asset type. */
8
+ const TYPE_ALIASES = {
9
+ environment: "env",
10
+ };
4
11
  // ── Construction ────────────────────────────────────────────────────────────
5
12
  /**
6
13
  * Build a ref string from components.
@@ -46,12 +53,16 @@ export function parseAssetRef(ref) {
46
53
  }
47
54
  const rawType = body.slice(0, colon);
48
55
  const rawName = body.slice(colon + 1);
49
- if (!isAssetType(rawType)) {
56
+ // Type aliases: `environment:` is an accepted spelling of the canonical
57
+ // `env:` type. (`vault:` remains its own deprecated type so the frozen
58
+ // `vaults/` copy keeps resolving through the 0.8.x window.)
59
+ const resolvedType = TYPE_ALIASES[rawType] ?? rawType;
60
+ if (!isAssetType(resolvedType)) {
50
61
  throw new UsageError(`Invalid asset type: "${rawType}".`, "MISSING_REQUIRED_ARGUMENT");
51
62
  }
52
63
  validateName(rawName);
53
64
  const name = normalizeName(rawName);
54
- return { type: rawType, name, origin: origin || undefined };
65
+ return { type: resolvedType, name, origin: origin || undefined };
55
66
  }
56
67
  // ── Validation ──────────────────────────────────────────────────────────────
57
68
  function validateName(name) {
@@ -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
  * Central registry for asset type renderer and action builder maps.
3
6
  *
@@ -21,9 +24,11 @@ export const TYPE_TO_RENDERER = {
21
24
  lesson: "lesson-md",
22
25
  memory: "memory-md",
23
26
  workflow: "workflow-md",
27
+ env: "env-file",
24
28
  vault: "vault-env",
29
+ secret: "secret-file",
25
30
  wiki: "wiki-md",
26
- task: "task-md",
31
+ task: "task-yaml",
27
32
  };
28
33
  /** Map asset types to action builder functions for search results. */
29
34
  export const ACTION_BUILDERS = {
@@ -35,7 +40,9 @@ export const ACTION_BUILDERS = {
35
40
  lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
36
41
  memory: (ref) => `akm show ${ref} -> recall context`,
37
42
  workflow: (ref) => buildWorkflowAction(ref),
38
- vault: (ref) => `akm show ${ref} -> inspect keys; source "$(akm vault path ${ref})" -> load values; akm vault run ${ref} -- <command> -> run with injected env`,
43
+ env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (the agent-safe path — values never reach stdout). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
44
+ vault: (ref) => `DEPRECATED (use env): akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with injected env`,
45
+ secret: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
39
46
  wiki: (ref) => `akm show ${ref} -> read the wiki page`,
40
47
  task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
41
48
  };
@@ -0,0 +1,88 @@
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
+ * Canonical asset-on-disk serialization.
6
+ *
7
+ * Before this module, 9+ call sites across `src/` independently reimplemented
8
+ * `yamlStringify(fm).trimEnd() + "---\n…\n---\n\n${body}"` to assemble a
9
+ * Markdown asset. The reimplementations drifted (different body normalization,
10
+ * different separator newlines, different trailing-newline policy), which is
11
+ * exactly the kind of silent format-shift the proposal-quality validators end
12
+ * up chasing downstream. This file is the single point of truth for
13
+ * "what does a well-formed AKM asset look like on disk".
14
+ *
15
+ * Two helpers are exported:
16
+ * - `serializeFrontmatter(fm)` — YAML for the frontmatter block only, with
17
+ * no `---` fences and no trailing newline. Single home for quoting style,
18
+ * field-order policy, and trailing-whitespace rules.
19
+ * - `assembleAsset(fm, body)` — frontmatter wrapped in `---` fences with a
20
+ * blank line between the closing fence and the body, and exactly one
21
+ * trailing `\n`. Single home for body normalization and the file-shape
22
+ * contract.
23
+ *
24
+ * Contract (must hold for the dedup to be safe):
25
+ * - Idempotent: `parseFrontmatter(assembleAsset(fm, body))` re-assembled
26
+ * reproduces the same bytes.
27
+ * - Field order: insertion order of `fm` is preserved (the caller controls
28
+ * ordering; the helper never reorders).
29
+ * - Quoting: `yaml.stringify` defaults — no custom quoting logic.
30
+ * - Trailing newline: exactly one `\n` at end of output.
31
+ * - Body normalization: leading newlines are stripped (`/^\n+/`). This
32
+ * collapses the assorted `body.replace(/^\n+/, "")` /
33
+ * `body.startsWith("\n") ? "" : "\n" + body` / bare `${body}` patterns
34
+ * onto the most aggressive existing normalizer.
35
+ */
36
+ import { stringify as yamlStringify } from "yaml";
37
+ /**
38
+ * Serialize a frontmatter object to its on-disk YAML form, without `---`
39
+ * fences and without a trailing newline.
40
+ *
41
+ * Two calls with the same input produce byte-identical output. Field order is
42
+ * preserved from the input object's insertion order — callers control
43
+ * ordering, the helper never reorders.
44
+ */
45
+ export function serializeFrontmatter(frontmatter) {
46
+ return yamlStringify(frontmatter).trimEnd();
47
+ }
48
+ /**
49
+ * Assemble a complete asset file string from a frontmatter object and a body.
50
+ *
51
+ * Output shape: `---\n<yaml>\n---\n\n<body>\n` where:
52
+ * - `<yaml>` is `serializeFrontmatter(frontmatter)`.
53
+ * - `<body>` has any leading `\n` characters stripped.
54
+ * - Exactly one `\n` terminates the file.
55
+ *
56
+ * Idempotent under round-trip through the project's `parseFrontmatter`.
57
+ */
58
+ export function assembleAsset(frontmatter, body) {
59
+ return assembleAssetFromString(serializeFrontmatter(frontmatter), body);
60
+ }
61
+ /**
62
+ * Same fence/body assembly as `assembleAsset` but takes a pre-serialized
63
+ * frontmatter string. Use this when a caller needs its own frontmatter
64
+ * serializer (e.g. defensive single-line flattening for untrusted LLM
65
+ * output, or JSON.stringify-per-value for guaranteed-quoted scalars) while
66
+ * still sharing the canonical fence-and-body template.
67
+ *
68
+ * The `serializedFm` argument must already match `serializeFrontmatter`'s
69
+ * contract: no `---` fences, no trailing newline. Trailing whitespace is
70
+ * trimmed defensively.
71
+ *
72
+ * Output contract — identical to `assembleAsset`:
73
+ * - `---\n<serializedFm>\n---\n\n<body>\n`
74
+ * - body has leading `\n` characters stripped
75
+ * - exactly one `\n` terminates the file
76
+ *
77
+ * This helper is the single point of truth for the fence-and-body template.
78
+ * Three command surfaces (`reflect`, `distill`, `consolidate`) call it
79
+ * directly because their inputs are pre-validated LLM payloads where the
80
+ * full `yamlStringify` may emit shapes (`|`-block scalars, anchors) that
81
+ * the project's hand-rolled `parseFrontmatter` subset parser cannot read.
82
+ */
83
+ export function assembleAssetFromString(serializedFm, body) {
84
+ const yaml = serializedFm.replace(/\s+$/, "");
85
+ const normalizedBody = body.replace(/^\n+/, "");
86
+ const withTrailingNewline = normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`;
87
+ return `---\n${yaml}\n---\n\n${withTrailingNewline}`;
88
+ }