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
@@ -24,26 +24,28 @@
24
24
  import fs from "node:fs";
25
25
  import os from "node:os";
26
26
  import path from "node:path";
27
- import { assertNever } from "../../core/assert.js";
28
27
  import { parseAssetRef } from "../../core/asset/asset-ref.js";
29
28
  import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
30
29
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
31
30
  import { stripMarkdownFences } from "../../core/asset/markdown.js";
31
+ import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
32
32
  import { resolveStashDir } from "../../core/common.js";
33
33
  import { loadConfig } from "../../core/config/config.js";
34
34
  import { ConfigError, UsageError } from "../../core/errors.js";
35
35
  import { appendEvent, readEvents } from "../../core/events.js";
36
36
  import { lintLessonContent } from "../../core/lesson-lint.js";
37
+ import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
37
38
  import { lookup } from "../../indexer/indexer.js";
38
39
  import { runAgent, } from "../../integrations/agent/index.js";
39
40
  import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
40
41
  import { buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
41
42
  import { resolveImproveProcessRunnerFromProfile, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
43
+ import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
42
44
  import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
43
45
  import { chatCompletion } from "../../llm/client.js";
44
46
  import { isLlmFeatureEnabled } from "../../llm/feature-gate.js";
45
47
  import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "../agent/agent-support.js";
46
- import { checkReflectSize } from "../proposal/validators/proposal-quality-validators.js";
48
+ import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
47
49
  import { createProposal, isProposalSkipped, listProposals, } from "../proposal/validators/proposals.js";
48
50
  import { deriveLessonRef, runLessonQualityJudge } from "./distill.js";
49
51
  import { classifyReflectChange } from "./reflect-noise.js";
@@ -383,6 +385,101 @@ function splitFrontmatter(raw) {
383
385
  return { fmText: null, body: raw };
384
386
  return { fmText: m[1], body: m[2] };
385
387
  }
388
+ /**
389
+ * Strip an LLM-appended duplicate frontmatter block from a body string.
390
+ *
391
+ * When the LLM echoes the original source file verbatim after its rewrite,
392
+ * the resulting body contains a second `---...---` YAML block. We detect it
393
+ * by requiring BOTH a balanced fence (opening + closing `---`) AND YAML-like
394
+ * `key: value` content inside, so legitimate Markdown thematic breaks and
395
+ * code-fence examples are never truncated.
396
+ */
397
+ function stripAppendedFrontmatter(body) {
398
+ const fencePattern = /\n---\r?\n([\s\S]*?)\n---\r?\n/;
399
+ const match = body.match(fencePattern);
400
+ if (!match)
401
+ return body;
402
+ // Only strip when the captured block looks like YAML frontmatter.
403
+ if (!/^\w[\w-]*:/m.test(match[1]))
404
+ return body;
405
+ return body.slice(0, body.indexOf(match[0])).replace(/\s+$/, "");
406
+ }
407
+ /**
408
+ * #636 — deterministically derive a valid `description` from an asset's existing
409
+ * metadata when one is missing. Sources, in priority order: the `title:`
410
+ * frontmatter field, the first `# Heading` in the (proposed or source) body, and
411
+ * the first sentence of the opening body paragraph. The candidate is normalized
412
+ * (whitespace collapsed, trailing punctuation/markdown stripped, clamped to the
413
+ * description max) and only returned if it PASSES `isValidDescription` — so this
414
+ * never produces a heading-fragment, truncated, or otherwise gate-failing value.
415
+ * Returns `undefined` when nothing usable can be derived (caller leaves the
416
+ * proposal as-is rather than fabricating prose).
417
+ *
418
+ * This is intentionally deterministic and lives in the reflect proposal-build
419
+ * path — it does NOT touch the validators or the promote-time repair.
420
+ */
421
+ function deriveDescriptionFromAsset(title, proposedBody, sourceBody, targetRef) {
422
+ // Each candidate is tagged with its kind. A title or `# Heading` is a bare
423
+ // fragment ("Paged.js — Named Page") that reads poorly as a description even
424
+ // when it is long enough to pass the length gate, so for those we prefer the
425
+ // padded sentence form. A prose sentence is already a sentence, so it is used
426
+ // as-is (padding it would double-wrap an already-complete sentence).
427
+ const candidates = [];
428
+ // 1. title: frontmatter
429
+ if (typeof title === "string" && title.trim())
430
+ candidates.push({ text: title.trim(), kind: "fragment" });
431
+ // 2. first `# Heading` (proposed body first, then source body)
432
+ for (const body of [proposedBody, sourceBody]) {
433
+ const headingMatch = body.match(/^#{1,6}\s+(.+?)\s*$/m);
434
+ if (headingMatch?.[1])
435
+ candidates.push({ text: headingMatch[1].trim(), kind: "fragment" });
436
+ }
437
+ // 3. first sentence of the opening prose paragraph (skip headings, fences,
438
+ // list markers, blockquotes — those are not prose).
439
+ for (const body of [proposedBody, sourceBody]) {
440
+ const firstSentence = firstProseSentence(body);
441
+ if (firstSentence)
442
+ candidates.push({ text: firstSentence, kind: "prose" });
443
+ }
444
+ for (const { text, kind } of candidates) {
445
+ const normalized = normalizeDescriptionCandidate(text);
446
+ if (!normalized)
447
+ continue;
448
+ // For a title/heading fragment, try the padded sentence form FIRST so the
449
+ // result reads as a sentence rather than a bare fragment — a short but valid
450
+ // title like "Paged.js — Named Page" (21 chars) would otherwise be returned
451
+ // verbatim. Fall back to the bare form only if the padded form fails the
452
+ // gate. A prose candidate is already a sentence, so it is used as-is.
453
+ const variants = kind === "fragment" ? [`Reference notes on ${normalized}.`, normalized] : [normalized];
454
+ for (const v of variants) {
455
+ const clamped = v.length > DESCRIPTION_MAX_CHARS ? v.slice(0, DESCRIPTION_MAX_CHARS).trimEnd() : v;
456
+ if (isValidDescription(clamped, targetRef, { skipRefTailCheck: true }).ok)
457
+ return clamped;
458
+ }
459
+ }
460
+ return undefined;
461
+ }
462
+ /** Extract the first prose sentence from a markdown body, or `""` if none. */
463
+ function firstProseSentence(body) {
464
+ for (const rawLine of body.split(/\r?\n/)) {
465
+ const line = rawLine.trim();
466
+ if (!line)
467
+ continue;
468
+ if (/^(#{1,6}\s|```|~~~|[-*+]\s|\d+\.\s|>|\||<!--)/.test(line))
469
+ continue;
470
+ const sentenceMatch = line.match(/^(.+?[.!?])(\s|$)/);
471
+ return (sentenceMatch?.[1] ?? line).trim();
472
+ }
473
+ return "";
474
+ }
475
+ /** Normalize a description candidate: strip markdown markers, collapse space. */
476
+ function normalizeDescriptionCandidate(raw) {
477
+ return raw
478
+ .replace(/`/g, "")
479
+ .replace(/^[#>*\-\s]+/, "")
480
+ .replace(/\s+/g, " ")
481
+ .trim();
482
+ }
386
483
  /**
387
484
  * Reflect post-processor — enforces the safety rails described at the top of
388
485
  * this file:
@@ -408,7 +505,7 @@ function splitFrontmatter(raw) {
408
505
  * from `payload.frontmatter` so identity fields can be enforced. Size guard
409
506
  * is skipped because there is no source to compare against.
410
507
  */
411
- function sanitizeReflectPayload(payload, sourceContent, targetRef) {
508
+ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
412
509
  const warnings = [];
413
510
  const { fmText: sourceFmText, body: sourceBody } = sourceContent
414
511
  ? splitFrontmatter(sourceContent)
@@ -449,7 +546,35 @@ function sanitizeReflectPayload(payload, sourceContent, targetRef) {
449
546
  mergedFm[field] = sourceFm[field];
450
547
  }
451
548
  }
452
- const cleanedBody = rawLlmBody.replace(/^\s+/, "");
549
+ const cleanedBody = stripAppendedFrontmatter(rawLlmBody.replace(/^\s+/, ""));
550
+ // #636 — deterministic description fallback (reflect-side belt-and-suspenders).
551
+ // If the type requires a `description` and the merged frontmatter is still
552
+ // MISSING one (source had none AND the model didn't author one), derive a
553
+ // description DETERMINISTICALLY from the existing `title:` frontmatter or the
554
+ // first `# Heading` / opening body sentence — never free-form invention. This
555
+ // runs in the reflect proposal-build path, BEFORE the proposal is created, so
556
+ // the validator/promote path is left untouched (no gate fabricates content).
557
+ //
558
+ // Scope is the issue's target: a source asset that ALREADY carries frontmatter
559
+ // (e.g. scraped docs: `source`/`title`/`scraped`) but has a MISSING/empty
560
+ // `description`. We deliberately do NOT fire when:
561
+ // - the source has no frontmatter block at all (injecting one would be a
562
+ // structural change and would defeat the #580 no-op/cosmetic noise gate
563
+ // for a pure body echo), or
564
+ // - a present-but-otherwise-invalid description exists (too short, a heading
565
+ // fragment) — overwriting authored content is out of scope; the prompt
566
+ // instruction handles improving it instead.
567
+ const refType = targetRef.includes(":") ? (targetRef.split(":")[0] ?? "") : "";
568
+ const mergedDesc = mergedFm.description;
569
+ const descIsMissing = typeof mergedDesc !== "string" || mergedDesc.trim().length === 0;
570
+ const sourceHadFrontmatter = sourceFmText !== null && Object.keys(sourceFm).length > 0;
571
+ if (refType && requiresDescription(refType) && descIsMissing && sourceHadFrontmatter) {
572
+ const derived = deriveDescriptionFromAsset(mergedFm.title, cleanedBody, sourceBody, targetRef);
573
+ if (derived) {
574
+ mergedFm.description = derived;
575
+ warnings.push("Synthesized a deterministic `description` from title/heading (#636) — source and proposal lacked one.");
576
+ }
577
+ }
453
578
  // Size guard — only when source body is meaningfully large. The pure
454
579
  // predicate lives in `core/proposal-quality-validators` so the same check
455
580
  // also runs inside `runProposalValidators` on `proposal accept`.
@@ -585,6 +710,9 @@ export async function akmReflect(options = {}) {
585
710
  metadata: {
586
711
  ...(options.task ? { task: options.task } : {}),
587
712
  ...(options.profile ? { profile: options.profile } : {}),
713
+ // Attribution tagging: stamp the eligibility lane so reflect_invoked can be
714
+ // sliced by lane downstream. See EligibilitySource.
715
+ ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
588
716
  },
589
717
  });
590
718
  // Fix #3 (observability 0.8.0): every failure path below MUST emit
@@ -723,6 +851,9 @@ export async function akmReflect(options = {}) {
723
851
  // Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
724
852
  // reproducing proposals that have already been reviewed and refused.
725
853
  const rejectedProposals = readRejectedProposals(stash, options.ref);
854
+ // Standards "rulebook" for this target — wiki schema (wiki page) or stash
855
+ // convention/meta facts (non-wiki asset); empty when neither fires.
856
+ const standardsContext = resolveStandardsContext(options.ref, stash);
726
857
  // 5. Spawn the agent — with optional Self-Refine loop (R-1 / #372).
727
858
  //
728
859
  // maxRefineIters controls how many agent invocations are made:
@@ -791,6 +922,7 @@ export async function akmReflect(options = {}) {
791
922
  ...(schemaHints.length > 0 ? { schemaHints } : {}),
792
923
  ...(relatedLessons.length > 0 ? { relatedLessons } : {}),
793
924
  ...(options.task ? { task: options.task } : {}),
925
+ ...(standardsContext.trim() ? { standardsContext } : {}),
794
926
  ...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
795
927
  ...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
796
928
  // R-1: inject prior draft as self-critique target on iterations > 0
@@ -822,44 +954,39 @@ export async function akmReflect(options = {}) {
822
954
  iterResult = await runAgent(resolvedProfile, prompt, runOptions);
823
955
  }
824
956
  else if (runnerSpec) {
825
- // v2: dispatch through unified RunnerSpec
957
+ // v2: dispatch through the unified RunnerSpec seam (X3). The `agent` /
958
+ // `sdk` arms route to the default profile runners; the `llm` arm is
959
+ // reflect-specific (wraps `runReflectViaLlm` — its bespoke iteration
960
+ // shape) so it is supplied as the `llm` handler.
826
961
  const runOptions = {
827
962
  stdio: "captured",
828
963
  parseOutput: "text",
829
964
  ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
830
965
  };
831
- switch (runnerSpec.kind) {
832
- case "llm":
833
- // LLM HTTP path — `draftFilePath` is accepted for type symmetry
834
- // (see `RunReflectViaLlmOptions.draftFilePath` docstring) but is
835
- // intentionally a no-op. The prompt builder above also did not
836
- // include the file-write contract for this kind, so the LLM is
837
- // still asked for JSON via stdout.
838
- iterResult = await runReflectViaLlm({
839
- prompt,
840
- connection: runnerSpec.connection,
841
- timeoutMs: runnerSpec.timeoutMs ?? (typeof resolvedTimeoutMs === "number" ? resolvedTimeoutMs : undefined),
842
- priorDraft,
843
- iteration: iter,
844
- responseSchema: REFLECT_JSON_SCHEMA,
845
- chat: options.chat,
846
- ...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
847
- });
848
- break;
849
- case "sdk":
850
- iterResult = await runOpencodeSdk(runnerSpec.profile, prompt ?? "", runOptions);
851
- break;
852
- case "agent":
853
- iterResult = await runAgent(runnerSpec.profile, prompt, {
854
- ...runOptions,
855
- ...(runnerSpec.timeoutMs !== undefined ? { timeoutMs: runnerSpec.timeoutMs } : {}),
856
- });
857
- break;
858
- default:
859
- // Exhaustiveness arm (H1): a 4th RunnerSpec kind becomes a compile
860
- // error here instead of leaving `iterResult` unassigned at runtime.
861
- assertNever(runnerSpec);
862
- }
966
+ iterResult = await executeRunner(runnerSpec, prompt ?? "", runOptions, {
967
+ llm: async (spec) =>
968
+ // LLM HTTP path — `draftFilePath` is accepted for type symmetry
969
+ // (see `RunReflectViaLlmOptions.draftFilePath` docstring) but is
970
+ // intentionally a no-op. The prompt builder above also did not
971
+ // include the file-write contract for this kind, so the LLM is
972
+ // still asked for JSON via stdout.
973
+ runReflectViaLlm({
974
+ prompt,
975
+ connection: spec.connection,
976
+ timeoutMs: spec.timeoutMs ?? (typeof resolvedTimeoutMs === "number" ? resolvedTimeoutMs : undefined),
977
+ priorDraft,
978
+ iteration: iter,
979
+ responseSchema: REFLECT_JSON_SCHEMA,
980
+ chat: options.chat,
981
+ ...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
982
+ }),
983
+ // The `agent` arm (and only the agent arm — preserving prior behavior)
984
+ // overlays `spec.timeoutMs` onto the base run options.
985
+ runAgent: (profile, p, opts) => runAgent(profile, p, {
986
+ ...opts,
987
+ ...(runnerSpec.timeoutMs !== undefined ? { timeoutMs: runnerSpec.timeoutMs } : {}),
988
+ }),
989
+ });
863
990
  }
864
991
  else {
865
992
  // Production path (v1): dispatch directly to the appropriate runner.
@@ -1148,8 +1275,19 @@ export async function akmReflect(options = {}) {
1148
1275
  // (new-asset proposals have nothing to diff against).
1149
1276
  if (assetContent !== undefined) {
1150
1277
  const changeKind = classifyReflectChange(assetContent, payload.content);
1151
- if (changeKind !== "substantive") {
1152
- const subreason = changeKind === "noop" ? "reflect_skipped_noop" : "reflect_skipped_cosmetic";
1278
+ // 'low-value' is config-gated (#639). DEFAULT OFF — absent = byte-identical
1279
+ // pre-#639 behaviour (low-value treated the same as substantive). Resolved
1280
+ // by the caller from the ACTIVE improve profile's
1281
+ // `processes.reflect.lowValueFilter.enabled` and passed via options, so the
1282
+ // running profile (not a hardcoded `profiles.improve.default`) decides.
1283
+ const lowValueFilterEnabled = options.lowValueFilter === true;
1284
+ const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
1285
+ if (isDeferred) {
1286
+ const subreason = changeKind === "noop"
1287
+ ? "reflect_skipped_noop"
1288
+ : changeKind === "low-value"
1289
+ ? "reflect_skipped_low_value"
1290
+ : "reflect_skipped_cosmetic";
1153
1291
  emitReflectFailed("no_change", subreason, options.ref, { changeKind });
1154
1292
  return {
1155
1293
  schemaVersion: 1,
@@ -1157,7 +1295,9 @@ export async function akmReflect(options = {}) {
1157
1295
  reason: "no_change",
1158
1296
  error: changeKind === "noop"
1159
1297
  ? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
1160
- : `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
1298
+ : changeKind === "low-value"
1299
+ ? `Reflect skipped: proposed content for ${payload.ref} is a low-value prose micro-rewrite (few changed tokens, no structural changes); no proposal created.`
1300
+ : `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
1161
1301
  ...(options.ref ? { ref: options.ref } : {}),
1162
1302
  exitCode: result.exitCode,
1163
1303
  };
@@ -1228,6 +1368,9 @@ export async function akmReflect(options = {}) {
1228
1368
  // `parseAgentProposalPayload` already clamps to [0, 1] and drops non-
1229
1369
  // finite values; `createProposal` runs its own sanitizer as a safety net.
1230
1370
  ...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
1371
+ // Attribution tagging: persist the eligibility lane on the proposal so it
1372
+ // survives to accept/reject/revert time even across runs. See EligibilitySource.
1373
+ ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
1231
1374
  };
1232
1375
  const proposalResult = createProposal(stash, createInput, options.ctx);
1233
1376
  if (isProposalSkipped(proposalResult)) {