akm-cli 0.9.0-beta.5 → 0.9.0-beta.51

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 (221) hide show
  1. package/CHANGELOG.md +711 -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/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -50,7 +50,8 @@ import { makeAssetRef, parseAssetRef } from "../../../core/asset/asset-ref.js";
50
50
  import { resolveAssetPathFromName, TYPE_DIRS } from "../../../core/asset/asset-spec.js";
51
51
  import { NotFoundError, UsageError } from "../../../core/errors.js";
52
52
  import { appendEvent } from "../../../core/events.js";
53
- import { getStateDbPath, getStateProposal, hasImportedFsProposals, insertProposalIfAbsent, listStateProposalIdsByPrefix, listStateProposals, openStateDatabase, recordFsProposalsImport, upsertProposal, } from "../../../core/state-db.js";
53
+ import { getStateProposal, hasImportedFsProposals, insertProposalIfAbsent, listStateProposalIdsByPrefix, listStateProposals, recordFsProposalsImport, upsertProposal, withImmediateTransaction, withStateDb, } from "../../../core/state-db.js";
54
+ import { repairTruncatedDescription } from "../../../core/text-truncation.js";
54
55
  import { warn } from "../../../core/warn.js";
55
56
  import { commitWriteTargetBoundary, formatRefForMessage, resolveWriteTarget, writeAssetToSource, } from "../../../core/write-source.js";
56
57
  import { runProposalValidators } from "./proposal-validators.js";
@@ -74,6 +75,8 @@ export const PROPOSAL_SOURCES = [
74
75
  "consolidate",
75
76
  "extract",
76
77
  "improve",
78
+ "recombine",
79
+ "procedural",
77
80
  // Semi-automated / tool-driven.
78
81
  "feedback",
79
82
  // Human-initiated / CLI-driven.
@@ -91,6 +94,8 @@ export const AUTOMATED_PROPOSAL_SOURCES = [
91
94
  "consolidate",
92
95
  "extract",
93
96
  "improve",
97
+ "recombine",
98
+ "procedural",
94
99
  "schema-repair",
95
100
  ];
96
101
  /**
@@ -156,14 +161,10 @@ function newId(ctx) {
156
161
  * guaranteed to have run before any read or write.
157
162
  */
158
163
  function withProposalsDb(stashDir, ctx, fn) {
159
- const db = openStateDatabase(ctx?.dbPath ?? getStateDbPath());
160
- try {
164
+ return withStateDb((db) => {
161
165
  importLegacyProposalFiles(db, stashDir);
162
166
  return fn(db);
163
- }
164
- finally {
165
- db.close();
166
- }
167
+ }, { path: ctx?.dbPath });
167
168
  }
168
169
  // ── Legacy filesystem import (#578) ─────────────────────────────────────────
169
170
  /** Legacy (pre-0.9.0) proposal directory: `<stashDir>/.akm/proposals[/archive]`. */
@@ -334,37 +335,42 @@ export function createProposal(stashDir, input, ctx) {
334
335
  }
335
336
  const normalizedRef = makeAssetRef(parsedRef.type, parsedRef.name, parsedRef.origin);
336
337
  return withProposalsDb(stashDir, ctx, (db) => {
337
- if (!input.force) {
338
- const skip = checkDedupAndCooldown(db, stashDir, normalizedRef, input, ctx);
339
- if (skip)
340
- return skip;
341
- }
342
- const created = nowIso(ctx);
343
- // Phase 6A: validate confidence is a finite number in [0, 1]. Anything else
344
- // is dropped silently we never store NaN, Infinity, or out-of-range values.
345
- // Callers that mis-report confidence should not poison the auto-accept gate.
346
- const sanitizedConfidence = typeof input.confidence === "number" &&
347
- Number.isFinite(input.confidence) &&
348
- input.confidence >= 0 &&
349
- input.confidence <= 1
350
- ? input.confidence
351
- : undefined;
352
- const proposal = {
353
- id: newId(ctx),
354
- ref: normalizedRef,
355
- status: "pending",
356
- source: input.source,
357
- ...(input.sourceRun !== undefined ? { sourceRun: input.sourceRun } : {}),
358
- createdAt: created,
359
- updatedAt: created,
360
- payload: {
361
- content: input.payload.content,
362
- ...(input.payload.frontmatter !== undefined ? { frontmatter: input.payload.frontmatter } : {}),
363
- },
364
- ...(sanitizedConfidence !== undefined ? { confidence: sanitizedConfidence } : {}),
365
- };
366
- upsertProposal(db, proposal, stashDir);
367
- return proposal;
338
+ return withImmediateTransaction(db, () => {
339
+ if (!input.force) {
340
+ const skip = checkDedupAndCooldown(db, stashDir, normalizedRef, input, ctx);
341
+ if (skip)
342
+ return skip;
343
+ }
344
+ const created = nowIso(ctx);
345
+ // Phase 6A: validate confidence is a finite number in [0, 1]. Anything else
346
+ // is dropped silently we never store NaN, Infinity, or out-of-range values.
347
+ // Callers that mis-report confidence should not poison the auto-accept gate.
348
+ const sanitizedConfidence = typeof input.confidence === "number" &&
349
+ Number.isFinite(input.confidence) &&
350
+ input.confidence >= 0 &&
351
+ input.confidence <= 1
352
+ ? input.confidence
353
+ : undefined;
354
+ const proposal = {
355
+ id: newId(ctx),
356
+ ref: normalizedRef,
357
+ status: "pending",
358
+ source: input.source,
359
+ ...(input.sourceRun !== undefined ? { sourceRun: input.sourceRun } : {}),
360
+ createdAt: created,
361
+ updatedAt: created,
362
+ payload: {
363
+ content: input.payload.content,
364
+ ...(input.payload.frontmatter !== undefined ? { frontmatter: input.payload.frontmatter } : {}),
365
+ },
366
+ ...(sanitizedConfidence !== undefined ? { confidence: sanitizedConfidence } : {}),
367
+ // Attribution tagging: persist the eligibility lane so it survives to
368
+ // accept/reject/revert time. See EligibilitySource.
369
+ ...(input.eligibilitySource !== undefined ? { eligibilitySource: input.eligibilitySource } : {}),
370
+ };
371
+ upsertProposal(db, proposal, stashDir);
372
+ return proposal;
373
+ });
368
374
  });
369
375
  }
370
376
  /**
@@ -517,19 +523,25 @@ export function resolveProposalId(stashDir, idOrRef, ctx) {
517
523
  */
518
524
  export function archiveProposal(stashDir, id, status, reason, ctx) {
519
525
  return withProposalsDb(stashDir, ctx, (db) => {
520
- const existing = requireProposal(db, stashDir, id);
521
- const updated = {
522
- ...existing,
523
- status,
524
- updatedAt: nowIso(ctx),
525
- review: {
526
- outcome: status,
527
- ...(reason !== undefined ? { reason } : {}),
528
- decidedAt: nowIso(ctx),
529
- },
530
- };
531
- upsertProposal(db, updated, stashDir);
532
- return updated;
526
+ return withImmediateTransaction(db, () => {
527
+ const existing = requireProposal(db, stashDir, id);
528
+ if (existing.status !== "pending") {
529
+ throw new UsageError(`Proposal ${id} is not pending (current status: ${existing.status}). Only pending proposals can be ${status}.`, "INVALID_FLAG_VALUE");
530
+ }
531
+ const decidedAt = nowIso(ctx);
532
+ const updated = {
533
+ ...existing,
534
+ status,
535
+ updatedAt: decidedAt,
536
+ review: {
537
+ outcome: status,
538
+ ...(reason !== undefined ? { reason } : {}),
539
+ decidedAt,
540
+ },
541
+ };
542
+ upsertProposal(db, updated, stashDir);
543
+ return updated;
544
+ });
533
545
  });
534
546
  }
535
547
  /**
@@ -548,15 +560,17 @@ export function archiveProposal(stashDir, id, status, reason, ctx) {
548
560
  */
549
561
  export function recordGateDecision(stashDir, id, decision, ctx) {
550
562
  return withProposalsDb(stashDir, ctx, (db) => {
551
- const existing = getStateProposal(db, id, stashDir);
552
- if (!existing)
553
- return undefined;
554
- const updated = {
555
- ...existing,
556
- gateDecision: { ...decision, decidedAt: decision.decidedAt ?? nowIso(ctx) },
557
- };
558
- upsertProposal(db, updated, stashDir);
559
- return updated;
563
+ return withImmediateTransaction(db, () => {
564
+ const existing = getStateProposal(db, id, stashDir);
565
+ if (!existing || existing.status !== "pending")
566
+ return undefined;
567
+ const updated = {
568
+ ...existing,
569
+ gateDecision: { ...decision, decidedAt: decision.decidedAt ?? nowIso(ctx) },
570
+ };
571
+ upsertProposal(db, updated, stashDir);
572
+ return updated;
573
+ });
560
574
  });
561
575
  }
562
576
  /**
@@ -699,6 +713,99 @@ export function expireStaleProposals(stashDir, config, ctx) {
699
713
  export function validateProposal(proposal) {
700
714
  return runProposalValidators(proposal);
701
715
  }
716
+ // ── Content repair ──────────────────────────────────────────────────────────
717
+ /**
718
+ * Attempt bounded, deterministic repair of mechanically-fixable defects in a
719
+ * proposal's markdown content. NEVER fabricates text — only strips known-bad
720
+ * structure and applies {@link repairTruncatedDescription} to a truncated
721
+ * description when one is detected.
722
+ *
723
+ * Repairs performed (in order):
724
+ * 1. Strip body lines that restate frontmatter fields as pseudo-frontmatter
725
+ * (e.g. `**description**: …` or `when_to_use: …` in the body).
726
+ * 2. Remove stray body `---` horizontal-rule lines (leaving exactly the two
727
+ * frontmatter fences when the content has a valid frontmatter block).
728
+ * 3. Apply {@link repairTruncatedDescription} to a truncated/hanging
729
+ * `description` field in the frontmatter.
730
+ *
731
+ * Returns the repaired content string. When no repairs apply the input is
732
+ * returned byte-identical so callers can use strict equality to detect
733
+ * whether a repair actually happened.
734
+ *
735
+ * CRITICAL: This function is CONTENT-PRESERVING. Callers MUST re-validate the
736
+ * repaired output via {@link validateProposal} / {@link runProposalValidators}
737
+ * before promotion — a repair that makes things *worse* (or is simply
738
+ * insufficient) must be caught by the existing gate.
739
+ */
740
+ export function repairProposalContent(content) {
741
+ if (typeof content !== "string" || content.trim() === "")
742
+ return content;
743
+ // Determine whether the content has a frontmatter block so we know how
744
+ // many `---` fence lines are expected.
745
+ const hasFrontmatter = /^---\r?\n[\s\S]*?\r?\n---/.test(content);
746
+ // Split into lines for structural repairs.
747
+ const lines = content.split(/\r?\n/);
748
+ // Track whether we are inside the opening frontmatter block so we can
749
+ // leave it untouched and only repair the body.
750
+ let inFrontmatter = false;
751
+ // Frontmatter fence index tracking: first fence opens FM, second closes it.
752
+ let fmOpenSeen = false;
753
+ let fmCloseSeen = false;
754
+ const repairedLines = [];
755
+ for (const line of lines) {
756
+ const isFence = /^---\s*$/.test(line);
757
+ // Track frontmatter fences (first two `---` fences delimit the FM block).
758
+ if (isFence && !fmCloseSeen) {
759
+ if (!fmOpenSeen) {
760
+ fmOpenSeen = true;
761
+ inFrontmatter = true;
762
+ repairedLines.push(line);
763
+ continue;
764
+ }
765
+ if (inFrontmatter) {
766
+ fmCloseSeen = true;
767
+ inFrontmatter = false;
768
+ repairedLines.push(line);
769
+ continue;
770
+ }
771
+ }
772
+ // We are now in the body (past the frontmatter or no frontmatter).
773
+ if (inFrontmatter) {
774
+ // Still inside the frontmatter — keep as-is.
775
+ repairedLines.push(line);
776
+ continue;
777
+ }
778
+ // Repair 1: Strip pseudo-frontmatter restatements in the body.
779
+ // Matches lines like `**description**: …` or `when_to_use: …`.
780
+ if (/^\s*(\*\*|__)?\s*(description|when_to_use)\s*(\*\*|__)?\s*:/i.test(line)) {
781
+ // Drop the line — it is a structural defect, not user content.
782
+ continue;
783
+ }
784
+ // Repair 2: Remove stray `---` horizontal-rule lines in the body.
785
+ // We keep these only when the content has NO frontmatter (in that case
786
+ // `---` is a legitimate thematic break in plain-body content).
787
+ if (isFence && hasFrontmatter) {
788
+ // Drop: these are extra `---` fences beyond the two frontmatter delimiters.
789
+ continue;
790
+ }
791
+ repairedLines.push(line);
792
+ }
793
+ let repaired = repairedLines.join("\n");
794
+ // Repair 3: Apply repairTruncatedDescription to the description field.
795
+ // We operate on the raw text rather than re-parsing YAML to avoid
796
+ // reformatting unrelated frontmatter keys.
797
+ if (hasFrontmatter) {
798
+ // Extract the body text (after the second `---`) so we can pass it to
799
+ // repairTruncatedDescription as context for the swap-in heuristic.
800
+ const bodyMatch = repaired.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
801
+ const bodyText = bodyMatch?.[1] ?? "";
802
+ repaired = repaired.replace(/^(description:\s*)(.*?)(\r?\n)/m, (_match, prefix, rawDesc, nl) => {
803
+ const fixed = repairTruncatedDescription(rawDesc.trim(), bodyText);
804
+ return `${prefix}${fixed}${nl}`;
805
+ });
806
+ }
807
+ return repaired;
808
+ }
702
809
  /**
703
810
  * Validate a proposal, then promote it through the canonical
704
811
  * {@link writeAssetToSource} dispatch (the single place that branches on
@@ -716,12 +823,31 @@ export async function promoteProposal(stashDir, config, id, options = {}, ctx) {
716
823
  if (proposal.status !== "pending") {
717
824
  throw new UsageError(`Proposal ${id} is not pending (current status: ${proposal.status}). Only pending proposals can be accepted.`, "INVALID_FLAG_VALUE");
718
825
  }
719
- const report = validateProposal(proposal);
826
+ // Attempt bounded auto-repair of mechanically-fixable structural defects
827
+ // (pseudo-frontmatter-in-body, stray `---` fences, truncated description)
828
+ // BEFORE running validation. If the repair produces valid content, we
829
+ // promote the repaired version; if validation still fails, the original
830
+ // error path throws as before. The repair is content-preserving and
831
+ // deterministic — it never invents text.
832
+ const repairedContent = repairProposalContent(proposal.payload.content);
833
+ const proposalToValidate = repairedContent !== proposal.payload.content
834
+ ? { ...proposal, payload: { ...proposal.payload, content: repairedContent } }
835
+ : proposal;
836
+ const report = validateProposal(proposalToValidate);
720
837
  if (!report.ok) {
721
838
  const message = report.findings.map((f) => `[${f.kind}] ${f.message}`).join("\n");
722
839
  throw new UsageError(`Proposal ${id} failed validation:\n${message}`, "MISSING_REQUIRED_ARGUMENT", "Fix the proposal payload (frontmatter / content) and try again, or reject the proposal with a reason.");
723
840
  }
724
- const ref = parseAssetRef(proposal.ref);
841
+ // Use the (possibly repaired) payload for the promotion write. Persist the
842
+ // repaired content back onto the DB row so the audit trail reflects the
843
+ // final promoted payload (not the defective original).
844
+ if (repairedContent !== proposal.payload.content) {
845
+ withProposalsDb(stashDir, ctx, (db) => {
846
+ const updated = { ...proposal, payload: { ...proposal.payload, content: repairedContent } };
847
+ upsertProposal(db, updated, stashDir);
848
+ });
849
+ }
850
+ const ref = parseAssetRef(proposalToValidate.ref);
725
851
  if (!TYPE_DIRS[ref.type]) {
726
852
  throw new UsageError(`Proposal ${id} targets unknown asset type "${ref.type}".`, "INVALID_FLAG_VALUE");
727
853
  }
@@ -743,7 +869,7 @@ export async function promoteProposal(stashDir, config, id, options = {}, ctx) {
743
869
  // missing-revert path is visible.
744
870
  warn(`[proposals] promoteProposal: failed to capture backup for ${id}: ${err instanceof Error ? err.message : String(err)}`);
745
871
  }
746
- const written = await writeAssetToSource(target.source, target.config, ref, proposal.payload.content);
872
+ const written = await writeAssetToSource(target.source, target.config, ref, repairedContent);
747
873
  // 0.9.0 (issue #507): single batch commit at the write boundary for git
748
874
  // targets. No-op for filesystem/primary-stash targets.
749
875
  commitWriteTargetBoundary(target, `Update ${formatRefForMessage(ref)}`);