akm-cli 0.9.0-rc.0 → 0.9.0-rc.13

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 (598) hide show
  1. package/CHANGELOG.md +1283 -22
  2. package/README.md +62 -37
  3. package/SECURITY.md +46 -31
  4. package/dist/akm +162 -38
  5. package/dist/akm-migrate +44 -0
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/assets/hints/cli-hints-full.md +268 -118
  8. package/dist/assets/hints/cli-hints-short.md +87 -24
  9. package/dist/assets/{profiles → improve-strategies}/catchup.json +3 -1
  10. package/dist/assets/{profiles → improve-strategies}/consolidate.json +3 -1
  11. package/dist/assets/{profiles → improve-strategies}/default.json +6 -7
  12. package/dist/assets/improve-strategies/frequent.json +15 -0
  13. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +4 -2
  14. package/dist/assets/{profiles → improve-strategies}/memory-focus.json +4 -1
  15. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +5 -5
  16. package/dist/assets/{profiles → improve-strategies}/quick.json +4 -2
  17. package/dist/assets/improve-strategies/reflect-distill.json +30 -0
  18. package/dist/assets/{profiles → improve-strategies}/thorough.json +1 -1
  19. package/dist/assets/prompts/consolidate-system.md +5 -5
  20. package/dist/assets/prompts/extract-session.md +2 -6
  21. package/dist/assets/prompts/memory-infer-user.md +2 -3
  22. package/dist/assets/prompts/reflect-llm-framed-contract.md +11 -0
  23. package/dist/assets/prompts/reflect-llm-schema-contract.md +3 -0
  24. package/dist/assets/prompts/reflect-output-repair.md +3 -0
  25. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  26. package/dist/assets/stash-skeleton/README.md +38 -10
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +8 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +8 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +14 -1
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +13 -1
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +9 -1
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +11 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +9 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +9 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +8 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +100 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/domains.md +64 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/organization.md +136 -0
  39. package/dist/assets/tasks/core/extract.yml +3 -2
  40. package/dist/assets/tasks/core/improve.yml +2 -1
  41. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  42. package/dist/assets/tasks/core/sync.yml +1 -0
  43. package/dist/assets/tasks/core/version-check.yml +2 -1
  44. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +5 -0
  45. package/dist/assets/tasks/improve/akm-improve-catchup.yml +8 -0
  46. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +5 -0
  47. package/dist/assets/tasks/improve/akm-improve-frequent.yml +5 -0
  48. package/dist/assets/tasks/improve/akm-improve-nightly.yml +5 -0
  49. package/dist/assets/templates/html/health.html +5 -4
  50. package/dist/assets/workflows/workflow-template.md +31 -15
  51. package/dist/cli/invocation.js +279 -0
  52. package/dist/cli/parse-args.js +5 -90
  53. package/dist/cli/retired-commands.js +78 -0
  54. package/dist/cli/shared.js +158 -48
  55. package/dist/cli-node.mjs +2 -1
  56. package/dist/cli.js +747 -293
  57. package/dist/commands/agent/agent-dispatch.js +19 -18
  58. package/dist/commands/agent/agent-support.js +0 -24
  59. package/dist/commands/agent/contribute-cli.js +43 -97
  60. package/dist/commands/completions.js +80 -23
  61. package/dist/commands/config-cli.js +44 -281
  62. package/dist/commands/env/env-binding.js +99 -0
  63. package/dist/commands/env/env-cli.js +84 -224
  64. package/dist/commands/env/env.js +12 -163
  65. package/dist/commands/env/marker-path.js +6 -0
  66. package/dist/commands/env/secret-cli.js +45 -61
  67. package/dist/commands/env/secret.js +32 -62
  68. package/dist/commands/feedback-cli.js +179 -85
  69. package/dist/commands/health/accept-rate.js +58 -0
  70. package/dist/commands/health/advisories.js +7 -8
  71. package/dist/commands/health/checks.js +279 -94
  72. package/dist/commands/health/html-report.js +197 -578
  73. package/dist/commands/health/improve-metrics.js +277 -246
  74. package/dist/commands/health/llm-usage.js +19 -19
  75. package/dist/commands/health/md-report.js +16 -7
  76. package/dist/commands/health/metrics.js +67 -32
  77. package/dist/commands/health/renderers.js +47 -0
  78. package/dist/commands/health/report-view-model.js +508 -0
  79. package/dist/commands/health/stash-exposure.js +1 -1
  80. package/dist/commands/health/surfaces.js +16 -56
  81. package/dist/commands/health/task-runs.js +3 -67
  82. package/dist/{migrate-storage-node.mjs → commands/health/types-checks.js} +1 -5
  83. package/dist/commands/health/types-improve.js +29 -0
  84. package/dist/{output/text/save.js → commands/health/types-metrics.js} +1 -2
  85. package/dist/commands/health/types-result.js +7 -0
  86. package/dist/commands/health/types-runs.js +4 -0
  87. package/dist/commands/health/types-session-log.js +4 -0
  88. package/dist/commands/health/types-windows.js +4 -0
  89. package/dist/commands/health/types.js +26 -21
  90. package/dist/commands/health/windows.js +2 -3
  91. package/dist/commands/health.js +296 -167
  92. package/dist/commands/improve/anti-collapse.js +5 -5
  93. package/dist/commands/improve/autonomy-gate.js +68 -0
  94. package/dist/commands/improve/collapse-detector.js +65 -52
  95. package/dist/commands/improve/consolidate/chunking.js +9 -7
  96. package/dist/commands/improve/consolidate/eligibility.js +1 -23
  97. package/dist/commands/improve/consolidate/merge.js +4 -0
  98. package/dist/commands/improve/consolidate.js +454 -1354
  99. package/dist/commands/improve/content-hash.js +39 -0
  100. package/dist/commands/improve/distill/content-repair.js +4 -10
  101. package/dist/commands/improve/distill/promote-memory.js +89 -64
  102. package/dist/commands/improve/distill/quality-gate.js +118 -42
  103. package/dist/commands/improve/distill-guards.js +1 -1
  104. package/dist/commands/improve/distill-promotion-policy.js +33 -888
  105. package/dist/commands/improve/distill.js +607 -363
  106. package/dist/commands/improve/eligibility.js +165 -79
  107. package/dist/commands/improve/extract-cli.js +35 -126
  108. package/dist/commands/improve/extract-prompt.js +6 -35
  109. package/dist/commands/improve/extract.js +640 -391
  110. package/dist/commands/improve/feedback-valence.js +2 -12
  111. package/dist/commands/improve/improve-cli.js +134 -135
  112. package/dist/commands/improve/improve-result-file.js +30 -50
  113. package/dist/commands/improve/improve-run-types.js +4 -0
  114. package/dist/commands/improve/improve-strategies.js +135 -0
  115. package/dist/commands/improve/improve.js +904 -701
  116. package/dist/commands/improve/locks.js +64 -111
  117. package/dist/commands/improve/loop-stages.js +1110 -923
  118. package/dist/commands/improve/memory/derived-ref.js +124 -0
  119. package/dist/commands/improve/memory/memory-belief.js +79 -7
  120. package/dist/commands/improve/memory/memory-contradiction-detect.js +49 -52
  121. package/dist/commands/improve/memory/memory-improve.js +25 -37
  122. package/dist/commands/improve/outcome-loop.js +25 -88
  123. package/dist/commands/improve/preparation.js +1034 -813
  124. package/dist/commands/improve/proactive-maintenance.js +34 -9
  125. package/dist/commands/improve/proposal-envelope.js +31 -0
  126. package/dist/commands/improve/reflect.js +983 -794
  127. package/dist/commands/improve/run-context.js +119 -0
  128. package/dist/commands/improve/salience.js +24 -127
  129. package/dist/commands/improve/session-asset.js +7 -3
  130. package/dist/commands/improve/shared.js +14 -34
  131. package/dist/commands/improve/source-identity.js +28 -0
  132. package/dist/commands/improve/triage.js +20 -17
  133. package/dist/commands/lint/base-linter.js +340 -313
  134. package/dist/commands/lint/env-key-rules.js +31 -47
  135. package/dist/commands/lint/index.js +185 -30
  136. package/dist/commands/{events.js → log.js} +28 -38
  137. package/dist/commands/migrate-cli.js +54 -0
  138. package/dist/commands/migration-tool.js +55 -0
  139. package/dist/commands/observability-cli.js +70 -208
  140. package/dist/commands/proposal/diff-format.js +50 -0
  141. package/dist/commands/proposal/drain-policies.js +0 -6
  142. package/dist/commands/proposal/drain.js +91 -40
  143. package/dist/commands/proposal/proposal-cli.js +134 -132
  144. package/dist/commands/proposal/proposal-types.js +56 -0
  145. package/dist/commands/proposal/proposal.js +83 -65
  146. package/dist/commands/proposal/propose-cli.js +88 -0
  147. package/dist/commands/proposal/propose.js +105 -88
  148. package/dist/commands/proposal/repository.js +1303 -278
  149. package/dist/commands/proposal/validators/proposal-quality-validators.js +16 -6
  150. package/dist/commands/proposal/validators/proposal-validators.js +61 -12
  151. package/dist/commands/proposal/validators/proposals.js +6 -8
  152. package/dist/commands/read/curate.js +78 -73
  153. package/dist/commands/read/knowledge.js +510 -13
  154. package/dist/commands/read/registry-search.js +2 -2
  155. package/dist/commands/read/remember-cli.js +84 -15
  156. package/dist/commands/read/search-cli.js +203 -96
  157. package/dist/commands/read/search.js +126 -94
  158. package/dist/commands/read/show.js +226 -250
  159. package/dist/commands/registry-cli.js +34 -60
  160. package/dist/commands/remember.js +18 -57
  161. package/dist/commands/sources/add-cli.js +104 -49
  162. package/dist/commands/sources/bundle-cli.js +166 -0
  163. package/dist/commands/sources/bundle-config-ops.js +63 -0
  164. package/dist/commands/sources/info.js +27 -15
  165. package/dist/commands/sources/init.js +30 -40
  166. package/dist/commands/sources/installed-stashes.js +469 -172
  167. package/dist/commands/sources/migration-help.js +7 -4
  168. package/dist/commands/sources/schema-repair.js +10 -9
  169. package/dist/commands/sources/self-update.js +182 -121
  170. package/dist/commands/sources/source-add.js +169 -178
  171. package/dist/commands/sources/source-clone.js +144 -41
  172. package/dist/commands/sources/source-manage.js +94 -59
  173. package/dist/commands/sources/sources-cli.js +64 -205
  174. package/dist/commands/sources/stash-cli.js +91 -54
  175. package/dist/commands/sources/stash-skeleton.js +1 -1
  176. package/dist/commands/tasks/tasks-cli.js +106 -104
  177. package/dist/commands/tasks/tasks.js +445 -262
  178. package/dist/commands/workflow-cli.js +232 -121
  179. package/dist/core/action-contributors.js +1 -1
  180. package/dist/core/activation-policy.js +49 -0
  181. package/dist/core/adapter/adapters/agent-skills-adapter.js +181 -0
  182. package/dist/core/adapter/adapters/akm-adapter.js +528 -0
  183. package/dist/core/adapter/adapters/akm-lint.js +392 -0
  184. package/dist/core/adapter/adapters/akm-metadata.js +387 -0
  185. package/dist/core/adapter/adapters/akm-task-adapter.js +149 -0
  186. package/dist/core/adapter/adapters/akm-workflow-adapter.js +180 -0
  187. package/dist/core/adapter/adapters/claude-adapter.js +61 -0
  188. package/dist/core/adapter/adapters/dotenv-adapter.js +187 -0
  189. package/dist/core/adapter/adapters/generic-files-adapter.js +119 -0
  190. package/dist/core/adapter/adapters/index.js +80 -0
  191. package/dist/core/adapter/adapters/llm-wiki-adapter.js +419 -0
  192. package/dist/core/adapter/adapters/okf-adapter.js +391 -0
  193. package/dist/core/adapter/adapters/opencode-adapter.js +68 -0
  194. package/dist/core/adapter/adapters/shared.js +286 -0
  195. package/dist/core/adapter/adapters/tool-dir-shared.js +217 -0
  196. package/dist/core/adapter/adapters/website-snapshot-adapter.js +155 -0
  197. package/dist/core/adapter/bundle-adapter.js +4 -0
  198. package/dist/core/adapter/detect-adapter.js +17 -0
  199. package/dist/core/adapter/recognize-match.js +44 -0
  200. package/dist/core/adapter/registry.js +56 -0
  201. package/dist/core/adapter/types.js +4 -0
  202. package/dist/core/asset/akm-markdown.js +30 -0
  203. package/dist/core/asset/asset-placement.js +243 -0
  204. package/dist/core/asset/asset-ref.js +110 -79
  205. package/dist/core/asset/asset-serialize.js +20 -0
  206. package/dist/core/asset/frontmatter.js +28 -12
  207. package/dist/core/asset/markdown.js +40 -51
  208. package/dist/core/asset/resolve-ref.js +274 -0
  209. package/dist/core/asset/stash-meta.js +2 -2
  210. package/dist/core/bundle-id.js +51 -0
  211. package/dist/core/common.js +281 -86
  212. package/dist/core/config/config-io.js +42 -128
  213. package/dist/core/config/config-schema.js +233 -834
  214. package/dist/core/config/config-sources.js +162 -39
  215. package/dist/core/config/config-types.js +16 -11
  216. package/dist/core/config/config-version.js +29 -0
  217. package/dist/core/config/config-walker.js +126 -37
  218. package/dist/core/config/config.js +154 -331
  219. package/dist/core/config/deep-merge.js +41 -0
  220. package/dist/core/config/engine-semantics.js +28 -0
  221. package/dist/core/config/experimental.js +21 -0
  222. package/dist/core/config/schema/embedding.js +38 -0
  223. package/dist/core/config/schema/engines.js +116 -0
  224. package/dist/core/config/schema/experimental.js +47 -0
  225. package/dist/core/config/schema/feedback.js +31 -0
  226. package/dist/core/config/schema/improve-processes.js +389 -0
  227. package/dist/core/config/schema/improve.js +94 -0
  228. package/dist/core/config/schema/index-config.js +176 -0
  229. package/dist/core/config/schema/output.js +18 -0
  230. package/dist/core/config/schema/primitives.js +94 -0
  231. package/dist/core/config/schema/search.js +30 -0
  232. package/dist/core/config/schema/setup.js +18 -0
  233. package/dist/core/config/schema/sources-bundles.js +169 -0
  234. package/dist/core/config/schema/workflow.js +29 -0
  235. package/dist/core/env-secret-ref.js +155 -20
  236. package/dist/core/errors.js +17 -15
  237. package/dist/core/events-types.js +4 -0
  238. package/dist/core/events.js +46 -128
  239. package/dist/core/extra-params.js +62 -0
  240. package/dist/core/file-change.js +17 -0
  241. package/dist/core/file-lock.js +202 -57
  242. package/dist/core/fs-txn.js +392 -0
  243. package/dist/core/git-message.js +59 -0
  244. package/dist/core/improve-result.js +167 -0
  245. package/dist/core/json-schema.js +142 -0
  246. package/dist/core/lesson-lint.js +1 -17
  247. package/dist/core/logs-db.js +1 -1
  248. package/dist/core/maintenance-barrier.js +135 -0
  249. package/dist/core/migration-operation.js +44 -0
  250. package/dist/core/mutation-target.js +78 -0
  251. package/dist/core/paths.js +22 -25
  252. package/dist/core/platform.js +10 -0
  253. package/dist/core/recognition-util.js +128 -0
  254. package/dist/core/redaction.js +392 -0
  255. package/dist/core/standards/resolve-standards-context.js +36 -65
  256. package/dist/core/standards/resolve-stash-standards.js +2 -2
  257. package/dist/core/standards/resolve-type-conventions.js +5 -5
  258. package/dist/core/state/migrations.js +242 -11
  259. package/dist/core/state-db.js +98 -10
  260. package/dist/core/structured.js +1 -1
  261. package/dist/core/subprocess.js +303 -0
  262. package/dist/core/text-truncation.js +9 -5
  263. package/dist/core/time.js +20 -0
  264. package/dist/core/type-presentation.js +130 -0
  265. package/dist/core/warn.js +0 -3
  266. package/dist/core/write-source.js +834 -118
  267. package/dist/indexer/bundle-identity-guard.js +92 -0
  268. package/dist/indexer/db/graph-db.js +1 -25
  269. package/dist/indexer/db/llm-cache.js +1 -1
  270. package/dist/indexer/ensure-index.js +30 -9
  271. package/dist/indexer/graph/graph-boost.js +9 -30
  272. package/dist/indexer/graph/graph-extraction.js +41 -27
  273. package/dist/indexer/graph/graph-types.js +4 -0
  274. package/dist/indexer/index-writer-lock.js +93 -49
  275. package/dist/indexer/index-written-assets.js +100 -53
  276. package/dist/indexer/indexer.js +746 -329
  277. package/dist/indexer/init.js +18 -25
  278. package/dist/indexer/installations.js +142 -0
  279. package/dist/indexer/passes/dir-staleness.js +18 -10
  280. package/dist/indexer/passes/memory-inference.js +25 -15
  281. package/dist/indexer/passes/metadata.js +412 -243
  282. package/dist/indexer/scan/doc-to-entry.js +160 -0
  283. package/dist/indexer/scan/drain-dir.js +134 -0
  284. package/dist/indexer/search/db-search.js +292 -108
  285. package/dist/indexer/search/fts-query.js +64 -0
  286. package/dist/indexer/search/ranking-contributors.js +145 -25
  287. package/dist/indexer/search/ranking-types.js +4 -0
  288. package/dist/indexer/search/ranking.js +28 -71
  289. package/dist/indexer/search/search-attribution.js +67 -0
  290. package/dist/indexer/search/search-fields.js +18 -3
  291. package/dist/indexer/search/search-hit-enrichers.js +30 -40
  292. package/dist/indexer/search/search-source.js +157 -111
  293. package/dist/indexer/search/semantic-status.js +4 -1
  294. package/dist/indexer/usage/usage-events.js +10 -30
  295. package/dist/indexer/walk/file-context.js +3 -45
  296. package/dist/indexer/walk/matchers.js +42 -34
  297. package/dist/indexer/walk/path-resolver.js +11 -5
  298. package/dist/indexer/walk/walker.js +42 -14
  299. package/dist/integrations/agent/builder-shared.js +7 -0
  300. package/dist/integrations/agent/builders.js +5 -56
  301. package/dist/integrations/agent/config.js +3 -143
  302. package/dist/integrations/agent/detect.js +17 -2
  303. package/dist/integrations/agent/engine-resolution.js +231 -0
  304. package/dist/integrations/agent/index.js +1 -2
  305. package/dist/integrations/agent/model-aliases.js +16 -2
  306. package/dist/integrations/agent/profiles.js +36 -62
  307. package/dist/integrations/agent/prompts.js +46 -18
  308. package/dist/integrations/agent/runner-dispatch.js +93 -4
  309. package/dist/integrations/agent/runner.js +76 -208
  310. package/dist/integrations/agent/spawn.js +88 -196
  311. package/dist/integrations/harnesses/aider/agent-builder.js +114 -0
  312. package/dist/integrations/harnesses/aider/index.js +48 -0
  313. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  314. package/dist/integrations/harnesses/amazonq/agent-builder.js +147 -0
  315. package/dist/integrations/harnesses/amazonq/index.js +45 -0
  316. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  317. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  318. package/dist/integrations/harnesses/claude/config-import.js +1 -3
  319. package/dist/integrations/harnesses/claude/index.js +24 -35
  320. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  321. package/dist/integrations/harnesses/claude/session-log.js +27 -75
  322. package/dist/integrations/harnesses/codex/agent-builder.js +138 -0
  323. package/dist/integrations/harnesses/codex/index.js +52 -0
  324. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  325. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  326. package/dist/integrations/harnesses/copilot/index.js +48 -0
  327. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  328. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  329. package/dist/integrations/harnesses/gemini/index.js +48 -0
  330. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  331. package/dist/integrations/harnesses/ids.js +24 -0
  332. package/dist/integrations/harnesses/index.js +54 -34
  333. package/dist/integrations/harnesses/opencode/agent-builder.js +23 -5
  334. package/dist/integrations/harnesses/opencode/config-import.js +1 -3
  335. package/dist/integrations/harnesses/opencode/index.js +14 -32
  336. package/dist/integrations/harnesses/opencode/session-log.js +67 -125
  337. package/dist/integrations/harnesses/opencode-sdk/harness.js +51 -0
  338. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  339. package/dist/integrations/harnesses/openhands/agent-builder.js +128 -0
  340. package/dist/integrations/harnesses/openhands/index.js +48 -0
  341. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  342. package/dist/integrations/harnesses/pi/agent-builder.js +97 -0
  343. package/dist/integrations/harnesses/pi/index.js +45 -0
  344. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  345. package/dist/integrations/harnesses/shared.js +17 -0
  346. package/dist/integrations/harnesses/types.js +43 -32
  347. package/dist/integrations/lockfile.js +211 -24
  348. package/dist/integrations/session-logs/index.js +36 -39
  349. package/dist/integrations/session-logs/provider-base.js +113 -0
  350. package/dist/llm/client.js +182 -110
  351. package/dist/llm/embedders/deterministic.js +2 -2
  352. package/dist/llm/embedders/remote.js +21 -9
  353. package/dist/llm/feature-gate.js +17 -57
  354. package/dist/llm/graph-extract.js +12 -13
  355. package/dist/llm/index-passes.js +8 -42
  356. package/dist/llm/memory-infer.js +144 -1
  357. package/dist/llm/metadata-enhance.js +45 -30
  358. package/dist/llm/structured-call.js +16 -8
  359. package/dist/llm/usage-persist.js +30 -5
  360. package/dist/llm/usage-telemetry.js +59 -6
  361. package/dist/output/cli-hints.js +1 -2
  362. package/dist/output/command-registry.js +27 -0
  363. package/dist/output/context.js +22 -7
  364. package/dist/output/format-exempt.js +80 -0
  365. package/dist/output/generic-render.js +251 -0
  366. package/dist/output/html-render.js +11 -16
  367. package/dist/output/render-registry.js +57 -0
  368. package/dist/output/renderers.js +14 -279
  369. package/dist/output/shapes/curate.js +10 -1
  370. package/dist/output/shapes/events.js +12 -7
  371. package/dist/output/shapes/helpers.js +58 -84
  372. package/dist/output/shapes/passthrough.js +11 -39
  373. package/dist/output/shapes/proposal/producer.js +15 -7
  374. package/dist/output/shapes/registry.js +12 -6
  375. package/dist/output/shapes.js +0 -9
  376. package/dist/output/text/{init.js → bundle-create.js} +3 -1
  377. package/dist/output/text/bundle-show.js +7 -0
  378. package/dist/output/text/command-format.js +562 -0
  379. package/dist/output/text/env.js +1 -3
  380. package/dist/output/text/events.js +8 -7
  381. package/dist/output/text/helpers.js +15 -1164
  382. package/dist/output/text/proposal/producer.js +4 -2
  383. package/dist/output/text/proposal-format.js +202 -0
  384. package/dist/output/text/registry-commands.js +1 -2
  385. package/dist/output/text/registry.js +12 -6
  386. package/dist/output/text/show-directives.js +117 -0
  387. package/dist/output/text/show-format.js +103 -0
  388. package/dist/output/text/sync.js +5 -0
  389. package/dist/output/text/workflow-format.js +332 -0
  390. package/dist/output/text/workflow.js +3 -2
  391. package/dist/output/text.js +10 -19
  392. package/dist/registry/factory.js +4 -6
  393. package/dist/registry/origin-resolve.js +16 -27
  394. package/dist/registry/providers/skills-sh.js +3 -3
  395. package/dist/registry/providers/static-index.js +15 -25
  396. package/dist/registry/resolve.js +43 -94
  397. package/dist/registry/semver.js +43 -0
  398. package/dist/runtime.js +81 -12
  399. package/dist/scripts/akm-migrate.js +35529 -0
  400. package/dist/setup/detect.js +5 -7
  401. package/dist/setup/detected-engines.js +136 -0
  402. package/dist/setup/engine-config.js +100 -0
  403. package/dist/setup/registry-stash-loader.js +3 -3
  404. package/dist/setup/semantic-assets.js +12 -9
  405. package/dist/setup/setup.js +444 -208
  406. package/dist/setup/steps/connection-shared.js +120 -0
  407. package/dist/setup/steps/connection.js +108 -305
  408. package/dist/setup/steps/platforms.js +13 -12
  409. package/dist/setup/steps/semantic.js +15 -3
  410. package/dist/setup/steps/sources.js +21 -15
  411. package/dist/setup/steps/stashdir.js +6 -4
  412. package/dist/setup/steps/tasks.js +236 -119
  413. package/dist/setup/steps.js +3 -2
  414. package/dist/sources/freshness.js +39 -0
  415. package/dist/sources/provider-factory.js +11 -17
  416. package/dist/sources/providers/filesystem.js +2 -3
  417. package/dist/sources/providers/git-install.js +278 -34
  418. package/dist/sources/providers/git-provider.js +54 -56
  419. package/dist/sources/providers/git-stash.js +420 -91
  420. package/dist/sources/providers/git.js +2 -2
  421. package/dist/sources/providers/npm.js +16 -19
  422. package/dist/sources/providers/provider-utils.js +47 -22
  423. package/dist/sources/providers/sync-from-ref.js +3 -9
  424. package/dist/sources/providers/website.js +2 -2
  425. package/dist/sources/resolve.js +11 -10
  426. package/dist/sources/snapshot-fetchers/types.js +4 -0
  427. package/dist/sources/{website-ingest.js → snapshot-fetchers/website-ingest.js} +110 -41
  428. package/dist/storage/database.js +60 -4
  429. package/dist/storage/engines/sqlite-migrations.js +156 -5
  430. package/dist/storage/locations.js +1 -2
  431. package/dist/storage/repositories/canaries-repository.js +1 -1
  432. package/dist/storage/repositories/events-repository.js +51 -11
  433. package/dist/storage/repositories/improve-runs-repository.js +6 -32
  434. package/dist/storage/repositories/index-connection.js +79 -0
  435. package/dist/storage/repositories/index-db.js +4 -3
  436. package/dist/storage/repositories/index-entries-repository.js +863 -0
  437. package/dist/{indexer/db/entry-mapper.js → storage/repositories/index-entry-mapper.js} +19 -2
  438. package/dist/storage/repositories/index-entry-types.js +4 -0
  439. package/dist/storage/repositories/index-fts-repository.js +167 -0
  440. package/dist/storage/repositories/index-llm-cache-repository.js +108 -0
  441. package/dist/storage/repositories/index-meta-repository.js +49 -0
  442. package/dist/{indexer/db/schema.js → storage/repositories/index-schema.js} +226 -100
  443. package/dist/storage/repositories/index-sql.js +12 -0
  444. package/dist/storage/repositories/index-utility-repository.js +356 -0
  445. package/dist/storage/repositories/index-vec-repository.js +250 -0
  446. package/dist/storage/repositories/outcome-repository.js +119 -0
  447. package/dist/storage/repositories/proposals-repository.js +317 -75
  448. package/dist/storage/repositories/registry-cache.js +1 -1
  449. package/dist/storage/repositories/salience-repository.js +172 -0
  450. package/dist/storage/repositories/task-history-repository.js +110 -3
  451. package/dist/storage/repositories/workflow-runs-repository.js +240 -19
  452. package/dist/tasks/backends/cron.js +169 -46
  453. package/dist/tasks/backends/exec-utils.js +76 -3
  454. package/dist/tasks/backends/index.js +6 -9
  455. package/dist/tasks/backends/launchd.js +292 -55
  456. package/dist/tasks/backends/schtasks.js +557 -70
  457. package/dist/tasks/backends/types.js +4 -0
  458. package/dist/tasks/command-executable.js +93 -0
  459. package/dist/tasks/embedded.js +56 -38
  460. package/dist/tasks/parser.js +156 -64
  461. package/dist/tasks/resolve-akm-bin.js +144 -51
  462. package/dist/tasks/runner.js +377 -209
  463. package/dist/tasks/schedule.js +108 -19
  464. package/dist/tasks/scheduler-invocation.js +296 -0
  465. package/dist/tasks/schema.js +1 -1
  466. package/dist/tasks/task-id.js +35 -0
  467. package/dist/tasks/validator.js +30 -16
  468. package/dist/text-import-hook.mjs +1 -1
  469. package/dist/workflows/authoring/authoring.js +104 -43
  470. package/dist/workflows/authoring/scope-key.js +1 -1
  471. package/dist/workflows/cli.js +0 -16
  472. package/dist/workflows/concurrency-policy.js +15 -0
  473. package/dist/workflows/exec/brief.js +450 -0
  474. package/dist/workflows/exec/frozen-judge.js +47 -0
  475. package/dist/workflows/exec/native-executor.js +1038 -0
  476. package/dist/workflows/exec/param-secrets.js +115 -0
  477. package/dist/workflows/exec/report.js +1460 -0
  478. package/dist/workflows/exec/run-workflow.js +602 -0
  479. package/dist/workflows/exec/scheduler.js +71 -0
  480. package/dist/workflows/exec/step-work.js +1190 -0
  481. package/dist/workflows/exec/unit-writer.js +23 -0
  482. package/dist/workflows/exec/workflow-engine-gate.js +67 -0
  483. package/dist/workflows/exec/worktree.js +171 -0
  484. package/dist/workflows/ir/compile.js +246 -0
  485. package/dist/workflows/ir/freeze.js +233 -0
  486. package/dist/workflows/ir/params.js +54 -0
  487. package/dist/workflows/ir/plan-hash.js +68 -0
  488. package/dist/workflows/ir/schema.js +540 -0
  489. package/dist/workflows/parser.js +878 -304
  490. package/dist/workflows/program/expressions.js +181 -0
  491. package/dist/workflows/program/schema.js +51 -0
  492. package/dist/workflows/renderer.js +100 -45
  493. package/dist/workflows/resource-limits.js +22 -0
  494. package/dist/workflows/runtime/agent-identity.js +59 -14
  495. package/dist/workflows/runtime/checkin.js +1 -1
  496. package/dist/workflows/runtime/plan-classifier.js +131 -0
  497. package/dist/workflows/runtime/runs.js +376 -119
  498. package/dist/workflows/runtime/unit-checkin.js +45 -0
  499. package/dist/workflows/runtime/unit-phases.js +20 -0
  500. package/dist/workflows/runtime/workflow-asset-loader.js +241 -40
  501. package/dist/workflows/schema.js +1 -11
  502. package/dist/workflows/validate-summary.js +2 -3
  503. package/dist/workflows/validator.js +52 -30
  504. package/docs/README.md +42 -78
  505. package/docs/migration/README.md +8 -0
  506. package/docs/migration/release-notes/0.6.0.md +1 -1
  507. package/docs/migration/release-notes/0.7.0.md +9 -8
  508. package/docs/migration/release-notes/0.9.0.md +158 -14
  509. package/docs/migration/v0.7-to-v0.8.md +46 -47
  510. package/docs/migration/v0.8-to-v0.9.md +844 -0
  511. package/docs/reference/README.md +12 -0
  512. package/docs/reference/data-and-telemetry.md +333 -0
  513. package/package.json +21 -17
  514. package/schemas/akm-asset-envelope.json +93 -0
  515. package/schemas/akm-config.json +4636 -0
  516. package/schemas/akm-task.json +87 -0
  517. package/schemas/akm-workflow.json +373 -0
  518. package/dist/akm-migrate-storage +0 -38
  519. package/dist/assets/help/help-accept.md +0 -12
  520. package/dist/assets/help/help-improve.md +0 -84
  521. package/dist/assets/help/help-proposals.md +0 -17
  522. package/dist/assets/help/help-propose.md +0 -17
  523. package/dist/assets/help/help-reject.md +0 -11
  524. package/dist/assets/profiles/frequent.json +0 -13
  525. package/dist/assets/profiles/recombine-only.json +0 -21
  526. package/dist/assets/profiles/reflect-distill.json +0 -30
  527. package/dist/assets/profiles/synthesize.json +0 -15
  528. package/dist/assets/prompts/procedural-system.md +0 -44
  529. package/dist/assets/prompts/recombine-system.md +0 -40
  530. package/dist/assets/prompts/staleness-detect-system.md +0 -6
  531. package/dist/assets/tasks/core/backup.yml +0 -4
  532. package/dist/assets/tasks/graph-refresh-weekly.yml +0 -10
  533. package/dist/assets/templates/html/default.html +0 -78
  534. package/dist/assets/templates/html/vendor/echarts.min.js +0 -45
  535. package/dist/assets/wiki/index-template.md +0 -12
  536. package/dist/assets/wiki/ingest-workflow-template.md +0 -83
  537. package/dist/assets/wiki/log-template.md +0 -8
  538. package/dist/assets/wiki/schema-template.md +0 -61
  539. package/dist/cli/config-migrate.js +0 -150
  540. package/dist/cli/config-validate.js +0 -39
  541. package/dist/commands/graph/graph-cli.js +0 -124
  542. package/dist/commands/graph/graph.js +0 -487
  543. package/dist/commands/improve/calibration.js +0 -161
  544. package/dist/commands/improve/dedup.js +0 -482
  545. package/dist/commands/improve/extract-watch.js +0 -140
  546. package/dist/commands/improve/hot-probation.js +0 -45
  547. package/dist/commands/improve/improve-auto-accept.js +0 -276
  548. package/dist/commands/improve/improve-profiles.js +0 -168
  549. package/dist/commands/improve/procedural.js +0 -398
  550. package/dist/commands/improve/recombine.js +0 -818
  551. package/dist/commands/improve/schema-similarity-gate.js +0 -168
  552. package/dist/commands/lint/agent-linter.js +0 -44
  553. package/dist/commands/lint/command-linter.js +0 -44
  554. package/dist/commands/lint/default-linter.js +0 -16
  555. package/dist/commands/lint/fact-linter.js +0 -39
  556. package/dist/commands/lint/knowledge-linter.js +0 -16
  557. package/dist/commands/lint/memory-linter.js +0 -61
  558. package/dist/commands/lint/registry.js +0 -41
  559. package/dist/commands/lint/skill-linter.js +0 -45
  560. package/dist/commands/lint/task-linter.js +0 -50
  561. package/dist/commands/lint/workflow-linter.js +0 -81
  562. package/dist/commands/proposal/legacy-import.js +0 -115
  563. package/dist/commands/sources/history.js +0 -196
  564. package/dist/commands/tasks/default-tasks.js +0 -186
  565. package/dist/commands/wiki-cli.js +0 -292
  566. package/dist/core/asset/asset-registry.js +0 -76
  567. package/dist/core/asset/asset-spec.js +0 -259
  568. package/dist/core/config/config-migration.js +0 -602
  569. package/dist/core/deep-merge.js +0 -38
  570. package/dist/core/eval/rank-metrics.js +0 -113
  571. package/dist/core/ripgrep/install.js +0 -163
  572. package/dist/core/ripgrep/resolve.js +0 -81
  573. package/dist/indexer/db/db.js +0 -1413
  574. package/dist/indexer/manifest.js +0 -170
  575. package/dist/indexer/passes/metadata-contributors.js +0 -31
  576. package/dist/indexer/usage/unmigrated-vaults-guard.js +0 -94
  577. package/dist/integrations/harnesses/opencode-sdk/index.js +0 -49
  578. package/dist/llm/call-ai.js +0 -62
  579. package/dist/llm/memory-infer-impl.js +0 -138
  580. package/dist/output/shapes/distill.js +0 -14
  581. package/dist/output/shapes/history.js +0 -11
  582. package/dist/output/text/distill.js +0 -6
  583. package/dist/output/text/enable-disable.js +0 -8
  584. package/dist/output/text/history.js +0 -6
  585. package/dist/output/text/wiki.js +0 -16
  586. package/dist/registry/build-index.js +0 -386
  587. package/dist/scripts/migrate-storage.js +0 -19108
  588. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +0 -9411
  589. package/dist/scripts/migrations/v16-to-v17.js +0 -141
  590. package/dist/setup/legacy-config.js +0 -106
  591. package/dist/storage/repositories/consolidation-repository.js +0 -38
  592. package/dist/storage/repositories/recombine-repository.js +0 -213
  593. package/dist/wiki/wiki-templates.js +0 -15
  594. package/dist/wiki/wiki.js +0 -1012
  595. package/dist/workflows/db.js +0 -215
  596. package/docs/data-and-telemetry.md +0 -226
  597. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/registry.js +0 -0
  598. /package/dist/sources/{wiki-fetchers → snapshot-fetchers}/youtube.js +0 -0
@@ -11,7 +11,7 @@
11
11
  * content. Pull recent feedback (`feedback` events for that ref) and
12
12
  * lesson-lint findings to surface as schema hints.
13
13
  * 3. Build the prompt via {@link buildReflectPrompt}.
14
- * 4. Spawn the configured agent profile via {@link runAgent}.
14
+ * 4. Dispatch the selected named engine via {@link executeRunner}.
15
15
  * 5. Parse the agent's stdout into a {@link AgentProposalPayload}.
16
16
  * 6. Insert into the proposal queue via {@link createProposal} with
17
17
  * `source: "reflect"`.
@@ -24,32 +24,36 @@
24
24
  import fs from "node:fs";
25
25
  import os from "node:os";
26
26
  import path from "node:path";
27
- import { parseAssetRef } from "../../core/asset/asset-ref.js";
28
27
  import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
29
28
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
30
- import { stripMarkdownFences } from "../../core/asset/markdown.js";
29
+ import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
31
30
  import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
32
- import { resolveStashDir } from "../../core/common.js";
33
- import { getImproveProcessConfig, loadConfig } from "../../core/config/config.js";
34
- import { ConfigError, UsageError } from "../../core/errors.js";
31
+ import { loadConfig } from "../../core/config/config.js";
32
+ import { ConfigError } from "../../core/errors.js";
35
33
  import { appendEvent, readEvents } from "../../core/events.js";
36
34
  import { lintLessonContent } from "../../core/lesson-lint.js";
35
+ import { parseEmbeddedJsonResponse } from "../../core/parse.js";
36
+ import { redactSensitiveText } from "../../core/redaction.js";
37
37
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
38
- import { warn } from "../../core/warn.js";
39
38
  import { lookup } from "../../indexer/indexer.js";
40
- import { runAgent, } from "../../integrations/agent/index.js";
41
- import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
42
- import { buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
43
- import { resolveDefaultLlmRunner, resolveImproveProcessRunnerFromProfile, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
44
- import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
45
- import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
46
- import { chatCompletion } from "../../llm/client.js";
47
- import { isLlmFeatureEnabled } from "../../llm/feature-gate.js";
48
- import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "../agent/agent-support.js";
49
- import { createProposal, isProposalSkipped, listProposals, } from "../proposal/repository.js";
39
+ import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
40
+ import { resolveEngine } from "../../integrations/agent/engine-resolution.js";
41
+ import { buildReflectOutputRepairPrompt, buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
42
+ import { materializeLlmRunnerConnection, resolveImproveProcessRunner, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
43
+ import { collectDispatchSensitiveValues, executeRunner } from "../../integrations/agent/runner-dispatch.js";
44
+ import { LlmCallError } from "../../llm/client.js";
45
+ import { callStructured } from "../../llm/structured-call.js";
46
+ import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
47
+ import { isProposalSkipped, listProposals, proposalContent, } from "../proposal/repository.js";
50
48
  import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
51
- import { deriveLessonRef, runLessonQualityJudge } from "./distill.js";
49
+ import { deriveLessonRef } from "./distill.js";
50
+ import { runReflectQualityJudge } from "./distill/quality-gate.js";
51
+ import { findAssetFilePath } from "./eligibility.js";
52
+ import { emitProposal } from "./proposal-envelope.js";
52
53
  import { classifyReflectChange } from "./reflect-noise.js";
54
+ import { createRunContext, resolveRunStashDir } from "./run-context.js";
55
+ import { MAX_REJECTED_PROPOSALS } from "./shared.js";
56
+ import { durableImproveRef, improveStateReadRefs } from "./source-identity.js";
53
57
  const MAX_FEEDBACK_LINES = 10;
54
58
  const MAX_GLOBAL_FEEDBACK_LINES = 20;
55
59
  /**
@@ -60,10 +64,10 @@ const MAX_GLOBAL_FEEDBACK_LINES = 20;
60
64
  */
61
65
  function readRecentFeedback(ref) {
62
66
  try {
63
- const result = readEvents({ type: "feedback", ...(ref ? { ref } : {}) });
67
+ const events = readEvents({ type: "feedback", ...(ref ? { ref } : {}) }).events;
64
68
  const lines = [];
65
69
  const limit = ref ? MAX_FEEDBACK_LINES : MAX_GLOBAL_FEEDBACK_LINES;
66
- for (const event of result.events.slice(-limit)) {
70
+ for (const event of events.slice(-limit)) {
67
71
  const md = (event.metadata ?? {});
68
72
  const signal = typeof md.signal === "string" ? md.signal : "?";
69
73
  const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : "";
@@ -76,7 +80,6 @@ function readRecentFeedback(ref) {
76
80
  return [];
77
81
  }
78
82
  }
79
- const MAX_REJECTED_PROPOSALS = 3;
80
83
  /**
81
84
  * Asset types that reflect is allowed to operate on.
82
85
  *
@@ -97,7 +100,6 @@ export const REFLECT_ALLOWED_TYPES = new Set([
97
100
  "knowledge",
98
101
  "memory",
99
102
  "lesson",
100
- "wiki",
101
103
  "skill",
102
104
  "agent",
103
105
  "command",
@@ -112,7 +114,7 @@ export const REFLECT_ALLOWED_TYPES = new Set([
112
114
  * LLM tried to rewrite them.
113
115
  *
114
116
  * Observed regression: proposal `26941510` (May 2026) renamed
115
- * `skill:openpalm-stack-diagnostics`'s `name` field to `"diagnostic-checklist"`.
117
+ * `skills/openpalm-stack-diagnostics`'s `name` field to `"diagnostic-checklist"`.
116
118
  */
117
119
  const PROTECTED_FRONTMATTER_FIELDS = new Set(["name", "ref", "id", "slug", "type"]);
118
120
  /**
@@ -132,7 +134,7 @@ function readRejectedProposals(stash, ref) {
132
134
  .map((p) => ({
133
135
  ref: p.ref,
134
136
  reason: p.review?.reason ?? "no reason given",
135
- contentPreview: p.payload.content.slice(0, 500),
137
+ contentPreview: proposalContent(p).slice(0, 500),
136
138
  }));
137
139
  }
138
140
  catch {
@@ -142,19 +144,20 @@ function readRejectedProposals(stash, ref) {
142
144
  /**
143
145
  * Synthesize a tmp draft-file path for the agent/sdk file-write contract.
144
146
  *
145
- * Mirrors `src/commands/propose.ts:163-178` — when the runner is agent-CLI or
146
- * the OpenCode SDK, we instruct the agent to write the proposal body directly
147
- * to this file instead of inlining it in JSON on stdout. This bypasses two
147
+ * Mirrors the draft-path synthesis in `src/commands/proposal/propose.ts` —
148
+ * when the runner is agent-CLI or the OpenCode SDK, we instruct the agent to
149
+ * write the proposal body directly to this file instead of inlining it in
150
+ * JSON on stdout. This bypasses two
148
151
  * known failure modes for long assets: (a) ARG_MAX truncation on prompt
149
152
  * round-trips through fenced JSON, and (b) embedded-JSON parser brittleness
150
- * on multi-KB bodies (e.g. the `knowledge:systems/KOKORO_USAGE_GUIDE` 8.4KB
153
+ * on multi-KB bodies (e.g. the `knowledge/systems/KOKORO_USAGE_GUIDE` 8.4KB
151
154
  * payload that produced 4/5 `parse_error` in May 2026 reflect validation).
152
155
  *
153
156
  * The path lives under {@link os.tmpdir} and embeds the (sanitized) ref +
154
157
  * timestamp + random suffix so concurrent reflect calls cannot collide.
155
158
  *
156
- * Returns `undefined` for the LLM HTTP runner — the chat-completion transport
157
- * has no filesystem access (see warning at `src/llm/call-ai.ts:64-71`).
159
+ * The LLM HTTP runner cannot use this path because chat-completion transport
160
+ * has no filesystem access.
158
161
  */
159
162
  function synthesizeReflectDraftPath(ref) {
160
163
  const safeRef = (ref ?? "no-ref").replace(/[^a-z0-9_-]/gi, "_");
@@ -190,22 +193,27 @@ function hasRelatedSkillSource(content, skillRef) {
190
193
  const sources = parsed.data.sources;
191
194
  return Array.isArray(sources) && sources.some((source) => typeof source === "string" && source.trim() === skillRef);
192
195
  }
193
- async function readRelatedLessons(stash, ref, parsedRef) {
196
+ async function readRelatedLessons(ctx, stash, ref, parsedRef, itemRef) {
194
197
  if (parsedRef.type !== "skill")
195
198
  return [];
196
199
  const related = new Map();
197
200
  const derivedLessonRef = deriveLessonRef(ref);
198
201
  const candidateRefs = new Set([derivedLessonRef]);
199
- const derivedLessonPath = path.join(stash, "lessons", `${derivedLessonRef.slice("lesson:".length)}.md`);
202
+ const derivedLessonPath = path.join(stash, "lessons", `${parseRefInput(derivedLessonRef).name}.md`);
200
203
  if (fs.existsSync(derivedLessonPath)) {
201
- related.set(derivedLessonRef, { ref: derivedLessonRef, content: fs.readFileSync(derivedLessonPath, "utf8") });
204
+ // WI-9.10: genuine content read — routed through the per-invocation asset
205
+ // memo (D6). No write to this same path happens later in this invocation,
206
+ // so memoizing is safe (see run-context.ts's D6 seam docblock).
207
+ related.set(derivedLessonRef, { ref: derivedLessonRef, content: ctx.readAsset(derivedLessonPath) });
202
208
  }
203
209
  try {
204
- const feedbackEvents = readEvents({ type: "distill_invoked", ref }).events;
210
+ // Match events using the candidate's single durable state key.
211
+ const distillInvokedKeys = new Set(improveStateReadRefs(ref, itemRef));
212
+ const feedbackEvents = readEvents({ type: "distill_invoked" }).events.filter((event) => event.ref !== undefined && distillInvokedKeys.has(event.ref));
205
213
  for (const event of feedbackEvents) {
206
- const lessonRef = typeof event.metadata?.lessonRef === "string" ? event.metadata.lessonRef : undefined;
207
- if (lessonRef?.startsWith("lesson:"))
208
- candidateRefs.add(lessonRef);
214
+ const proposalRef = typeof event.metadata?.proposalRef === "string" ? event.metadata.proposalRef : undefined;
215
+ if (proposalRef && lenientRefType(proposalRef) === "lesson")
216
+ candidateRefs.add(proposalRef);
209
217
  }
210
218
  }
211
219
  catch {
@@ -213,10 +221,10 @@ async function readRelatedLessons(stash, ref, parsedRef) {
213
221
  }
214
222
  for (const candidateRef of candidateRefs) {
215
223
  try {
216
- const entry = await lookup(parseAssetRef(candidateRef));
217
- if (!entry?.filePath || !fs.existsSync(entry.filePath))
224
+ const filePath = await findAssetFilePath(durableImproveRef(candidateRef), stash);
225
+ if (!filePath || !fs.existsSync(filePath))
218
226
  continue;
219
- const content = fs.readFileSync(entry.filePath, "utf8");
227
+ const content = ctx.readAsset(filePath);
220
228
  related.set(candidateRef, { ref: candidateRef, content });
221
229
  }
222
230
  catch {
@@ -229,11 +237,11 @@ async function readRelatedLessons(stash, ref, parsedRef) {
229
237
  for (const fileName of fs.readdirSync(lessonsDir)) {
230
238
  if (!fileName.endsWith(".md"))
231
239
  continue;
232
- const content = fs.readFileSync(path.join(lessonsDir, fileName), "utf8");
240
+ const content = ctx.readAsset(path.join(lessonsDir, fileName));
233
241
  if (!hasRelatedSkillSource(content, ref))
234
242
  continue;
235
243
  const lessonName = fileName.slice(0, -3);
236
- const lessonRef = `lesson:${lessonName}`;
244
+ const lessonRef = conceptIdFromTypeName("lesson", lessonName);
237
245
  if (!related.has(lessonRef)) {
238
246
  related.set(lessonRef, { ref: lessonRef, content });
239
247
  }
@@ -283,97 +291,38 @@ async function readRelatedLessons(stash, ref, parsedRef) {
283
291
  /**
284
292
  * Returns true only when `stdout` is a recognised AKM proposal-skip signal.
285
293
  *
286
- * Two accepted forms:
287
- * 1. Structured JSON: `{ skipped: true }` or `{ reason: "<known-skip-reason>" }`
288
- * 2. Legacy text: any line matching `/proposal skipped/i`
289
- *
290
- * The previous regex `/cooldown/i` was intentionally broadened to avoid
291
- * false-positives on real agent error messages that incidentally contain the
292
- * word "cooldown" (e.g. "rate limit cooldown exceeded"). Only the tightly
293
- * scoped forms above are treated as legitimate skip signals.
294
+ * Accepted forms are structured JSON: `{ skipped: true }` or
295
+ * `{ reason: "<known-skip-reason>" }`.
294
296
  */
295
297
  function isStructuredCooldownSignal(stdout) {
296
298
  try {
297
299
  const parsed = JSON.parse(stdout.trim());
298
300
  if (parsed?.skipped === true)
299
301
  return true;
300
- if (typeof parsed?.reason === "string" &&
301
- ["duplicate_pending", "content_hash_match", "cooldown", "below_threshold"].includes(parsed.reason))
302
+ if (typeof parsed?.reason === "string" && ["fingerprint_match", "rejection_backoff"].includes(parsed.reason))
302
303
  return true;
303
304
  }
304
305
  catch {
305
306
  // Non-JSON stdout is never a structured cooldown signal.
306
307
  }
307
- // Legacy text signal emitted by older proposal output lines.
308
- return /proposal skipped/i.test(stdout);
308
+ return false;
309
309
  }
310
310
  /**
311
- * Fallback payload parser for reflect agent stdout (R-6 / #375).
312
- *
313
- * When the agent does not emit valid JSON (old-style agents, SDK mode without
314
- * structured output support), this function attempts to recover a proposal
315
- * payload from the raw markdown output. The parser is deliberately strict —
316
- * it requires the content to have a complete proposal structure (frontmatter
317
- * with required fields or a full heading + body).
318
- *
319
- * Strictness rationale: The previous implementation accepted any markdown
320
- * starting with `#` or `---`, which admitted malformed / hallucinated content
321
- * as valid proposals. Anthropic agent best practices recommend structured
322
- * output when the SDK supports it; this tighter fallback is the safety net.
323
- *
324
- * When `sdkMode === true`, structured output (tool-call schema) should be used
325
- * instead of this fallback. That wiring is tracked separately (full SDK
326
- * structured-output integration); for now this tighter parser applies to all
327
- * modes and is the primary R-6 deliverable.
311
+ * Best-effort asset type for a maybe-ref string, in the 0.9.0 `[bundle//]conceptId`
312
+ * grammar (`""` when it does not parse). Replaces the pre-0.9.0 `ref.split(":")[0]`
313
+ * type-extraction, which yielded the whole conceptId (`lessons/my-lesson`) instead
314
+ * of the type once refs stopped carrying a `type:` prefix (ref-grammar decision
315
+ * D-R3). Lenient by design — the callers degrade gracefully on an empty type.
328
316
  */
329
- function fallbackPayloadFromRawContent(stdout, ref, sdkMode = false) {
317
+ function lenientRefType(ref) {
330
318
  if (!ref)
331
- return undefined;
332
- const trimmed = stripMarkdownFences(stdout).trim();
333
- if (!trimmed)
334
- return undefined;
335
- const targetType = ref.split(":")[0];
336
- if (!looksLikeAssetContent(trimmed, sdkMode, targetType))
337
- return undefined;
338
- return { ref, content: trimmed };
339
- }
340
- /**
341
- * Determine whether raw agent output looks like a valid asset payload (R-6 / #375).
342
- *
343
- * Tightened from the previous `startsWith("#") || startsWith("---")`:
344
- *
345
- * - YAML frontmatter (`---`): must contain a `description:` field (the only
346
- * required frontmatter key in v1 spec). This eliminates empty `---\n---\n`
347
- * blocks and pure delimiter sequences as valid payloads.
348
- * - Heading start (`#`): must have at least 3 non-blank lines after the heading,
349
- * to ensure there is actual body content and not just a title stub.
350
- * - In SDK mode (`sdkMode === true`): additionally requires `when_to_use:` for
351
- * lesson types (full structured output will replace this in a future PR).
352
- */
353
- function looksLikeAssetContent(value, sdkMode = false, targetType) {
354
- if (value.startsWith("---")) {
355
- // YAML frontmatter must contain at least a description field.
356
- const fmEnd = value.indexOf("\n---", 4);
357
- if (fmEnd === -1)
358
- return false;
359
- const fmBlock = value.slice(0, fmEnd + 4);
360
- const hasDescription = /^description\s*:/m.test(fmBlock);
361
- if (!hasDescription)
362
- return false;
363
- // In SDK mode, lesson assets additionally require a when_to_use field.
364
- // Use the target ref type rather than frontmatter type: (which is non-standard).
365
- if (sdkMode && targetType === "lesson") {
366
- return /^when_to_use\s*:/m.test(fmBlock);
367
- }
368
- return true;
319
+ return "";
320
+ try {
321
+ return parseRefInput(ref).type;
369
322
  }
370
- if (value.startsWith("#")) {
371
- // Heading + at least 2 non-blank lines (heading + at least one body line).
372
- // This rejects pure title stubs (`# Title\n`) but accepts minimal valid content.
373
- const lines = value.split("\n").filter((l) => l.trim().length > 0);
374
- return lines.length >= 2;
323
+ catch {
324
+ return "";
375
325
  }
376
- return false;
377
326
  }
378
327
  /**
379
328
  * Split a markdown blob into `[frontmatterText, bodyText]`.
@@ -565,7 +514,7 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
565
514
  // - a present-but-otherwise-invalid description exists (too short, a heading
566
515
  // fragment) — overwriting authored content is out of scope; the prompt
567
516
  // instruction handles improving it instead.
568
- const refType = targetRef.includes(":") ? (targetRef.split(":")[0] ?? "") : "";
517
+ const refType = lenientRefType(targetRef);
569
518
  const mergedDesc = mergedFm.description;
570
519
  const descIsMissing = typeof mergedDesc !== "string" || mergedDesc.trim().length === 0;
571
520
  const sourceHadFrontmatter = sourceFmText !== null && Object.keys(sourceFm).length > 0;
@@ -616,298 +565,936 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
616
565
  /**
617
566
  * JSON Schema for structured reflect output. Passed to `chatCompletion` when
618
567
  * the connection has `supportsJsonSchema: true` so the model returns a strict
619
- * JSON object matching {@link AgentProposalPayload}.
568
+ * JSON object containing only the target-scoped fields AKM cannot derive.
620
569
  */
570
+ const REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA = {
571
+ type: "object",
572
+ required: ["description", "when_to_use"],
573
+ additionalProperties: false,
574
+ properties: {
575
+ description: { type: ["string", "null"] },
576
+ when_to_use: { type: ["string", "null"] },
577
+ },
578
+ };
621
579
  export const REFLECT_JSON_SCHEMA = {
622
580
  type: "object",
623
- required: ["ref", "content"],
581
+ required: ["content", "confidence", "frontmatterPatch"],
624
582
  additionalProperties: false,
625
583
  properties: {
626
- ref: { type: "string", description: "Asset ref in type:name format (e.g. lesson:my-lesson)." },
627
- content: { type: "string", description: "Full markdown content for the asset." },
628
- frontmatter: {
629
- type: "object",
630
- description: "Optional frontmatter key-value pairs to merge into the asset.",
631
- additionalProperties: true,
632
- },
584
+ content: { type: "string", description: "Complete improved markdown body without YAML frontmatter." },
633
585
  // Phase 6A (Advantage D6a): self-reported confidence in [0, 1]. When the
634
586
  // LLM is well-calibrated, scores at or above the configured threshold
635
587
  // (default 0.8) drive auto-accept in `akm improve`. Out-of-range or
636
- // non-finite values are clamped/dropped by the parser — the schema keeps
637
- // the field optional so older agents that don't emit a score still work.
588
+ // non-finite values are rejected by direct-output extraction. Agent and SDK
589
+ // confidence remains optional on their separate existing contracts.
590
+ confidence: {
591
+ type: "number",
592
+ minimum: 0,
593
+ maximum: 1,
594
+ description: "Self-reported quality confidence in [0, 1]. Persisted on the proposal for reviewers and the triage judge to read during adjudication.",
595
+ },
596
+ frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
597
+ },
598
+ };
599
+ const REFLECT_UNSCOPED_JSON_SCHEMA = {
600
+ type: "object",
601
+ required: ["ref", "content", "confidence", "frontmatterPatch"],
602
+ additionalProperties: false,
603
+ properties: {
604
+ ref: { type: "string", description: "Selected asset ref as a subdir-qualified conceptId." },
605
+ content: { type: "string", description: "Complete improved markdown body without YAML frontmatter." },
638
606
  confidence: {
639
607
  type: "number",
640
608
  minimum: 0,
641
609
  maximum: 1,
642
- description: "Optional self-reported quality confidence in [0, 1]. Proposals with confidence >= the active threshold (default 0.8) may be auto-accepted by `akm improve`.",
610
+ description: "Self-reported quality confidence in [0, 1].",
643
611
  },
612
+ frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
644
613
  },
645
614
  };
646
615
  /** Critique prompt injected between prior draft and refinement request (Self-Refine loop). */
647
- const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Please review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved JSON proposal.";
616
+ const REFLECT_CRITIQUE_PROMPT = "Your previous proposal is shown above. Review it critically and provide an improved version that is more specific, actionable, and avoids any issues with the previous attempt. Return only the improved response using the output contract from the original prompt.";
617
+ function reflectLlmTelemetry(result) {
618
+ if (!result.parsed || typeof result.parsed !== "object" || Array.isArray(result.parsed))
619
+ return undefined;
620
+ const parsed = result.parsed;
621
+ if (parsed.outputMode !== "json_schema" && parsed.outputMode !== "framed_markdown")
622
+ return undefined;
623
+ if (typeof parsed.repairAttempts !== "number")
624
+ return undefined;
625
+ return { outputMode: parsed.outputMode, repairAttempts: parsed.repairAttempts };
626
+ }
627
+ function reflectLlmPriorDraft(result) {
628
+ if (!result.parsed || typeof result.parsed !== "object" || Array.isArray(result.parsed))
629
+ return undefined;
630
+ const priorDraft = result.parsed.priorDraft;
631
+ return typeof priorDraft === "string" ? priorDraft : undefined;
632
+ }
633
+ function parseReflectConfidence(value) {
634
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
635
+ throw new Error('direct reflect response missing required number field "confidence" in [0, 1]');
636
+ }
637
+ return value;
638
+ }
639
+ function parseReflectFrontmatterPatch(value) {
640
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
641
+ throw new Error('direct reflect response missing required object field "frontmatterPatch"');
642
+ }
643
+ const patch = value;
644
+ const keys = Object.keys(patch).sort();
645
+ if (keys.length !== 2 || keys[0] !== "description" || keys[1] !== "when_to_use") {
646
+ throw new Error("direct reflect frontmatterPatch fields must be exactly: description, when_to_use");
647
+ }
648
+ const frontmatter = {};
649
+ for (const field of ["description", "when_to_use"]) {
650
+ const fieldValue = patch[field];
651
+ if (fieldValue === null)
652
+ continue;
653
+ if (typeof fieldValue !== "string" || !fieldValue.trim() || /[\r\n]/.test(fieldValue)) {
654
+ throw new Error(`direct reflect frontmatterPatch.${field} must be a non-empty single-line string or null`);
655
+ }
656
+ frontmatter[field] = fieldValue.trim();
657
+ }
658
+ return Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
659
+ }
660
+ function parseSchemaReflectOutput(raw, targetRef) {
661
+ const parsed = parseEmbeddedJsonResponse(raw);
662
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
663
+ throw new Error("direct reflect response was not valid JSON");
664
+ }
665
+ const expectedKeys = targetRef
666
+ ? ["confidence", "content", "frontmatterPatch"]
667
+ : ["confidence", "content", "frontmatterPatch", "ref"];
668
+ const actualKeys = Object.keys(parsed).sort();
669
+ if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) {
670
+ throw new Error(`direct reflect response fields must be exactly: ${expectedKeys.join(", ")}`);
671
+ }
672
+ if (typeof parsed.content !== "string" || !parsed.content.trim()) {
673
+ throw new Error('direct reflect response missing required string field "content"');
674
+ }
675
+ const ref = targetRef ?? (typeof parsed.ref === "string" ? parsed.ref.trim() : "");
676
+ if (!ref)
677
+ throw new Error('direct reflect response missing required string field "ref"');
678
+ const frontmatter = parseReflectFrontmatterPatch(parsed.frontmatterPatch);
679
+ return {
680
+ ref,
681
+ content: parsed.content,
682
+ confidence: parseReflectConfidence(parsed.confidence),
683
+ ...(frontmatter ? { frontmatter } : {}),
684
+ };
685
+ }
686
+ function parseFramedReflectOutput(raw, targetRef) {
687
+ const normalized = raw.replaceAll("\r\n", "\n").trim();
688
+ const beginMarker = "AKM_REFLECT_CONTENT_BEGIN\n";
689
+ const endMarker = "\nAKM_REFLECT_CONTENT_END";
690
+ const beginIndex = normalized.indexOf(beginMarker);
691
+ if (beginIndex < 0 || (beginIndex > 0 && normalized[beginIndex - 1] !== "\n")) {
692
+ throw new Error("direct reflect response missing AKM_REFLECT_CONTENT_BEGIN marker");
693
+ }
694
+ const contentStart = beginIndex + beginMarker.length;
695
+ const endIndex = normalized.lastIndexOf(endMarker);
696
+ if (endIndex < contentStart || normalized.slice(endIndex + endMarker.length).trim()) {
697
+ throw new Error("direct reflect response missing terminal AKM_REFLECT_CONTENT_END marker");
698
+ }
699
+ const headerLines = normalized.slice(0, beginIndex).trim().split("\n").filter(Boolean);
700
+ const confidenceLine = headerLines.find((line) => line.startsWith("AKM_REFLECT_CONFIDENCE:"));
701
+ const refLine = headerLines.find((line) => line.startsWith("AKM_REFLECT_REF:"));
702
+ const patchLine = headerLines.find((line) => line.startsWith("AKM_REFLECT_FRONTMATTER_PATCH:"));
703
+ const expectedHeaderLines = targetRef ? 2 : 3;
704
+ const invalidRefLine = targetRef ? refLine !== undefined : refLine === undefined;
705
+ if (headerLines.length !== expectedHeaderLines || !confidenceLine || !patchLine || invalidRefLine) {
706
+ throw new Error("direct reflect response contained invalid frame metadata");
707
+ }
708
+ const confidenceText = confidenceLine.slice("AKM_REFLECT_CONFIDENCE:".length).trim();
709
+ if (!/^(?:0(?:\.\d+)?|1(?:\.0+)?)$/.test(confidenceText)) {
710
+ throw new Error("direct reflect frame confidence must be a decimal number in [0, 1]");
711
+ }
712
+ const confidence = parseReflectConfidence(Number(confidenceText));
713
+ const ref = targetRef ?? refLine?.slice("AKM_REFLECT_REF:".length).trim() ?? "";
714
+ if (!ref)
715
+ throw new Error("direct reflect response contained an empty AKM_REFLECT_REF value");
716
+ const content = normalized.slice(contentStart, endIndex);
717
+ if (!content.trim())
718
+ throw new Error("direct reflect response contained empty framed content");
719
+ const patchText = patchLine.slice("AKM_REFLECT_FRONTMATTER_PATCH:".length).trim();
720
+ let parsedPatch;
721
+ try {
722
+ parsedPatch = JSON.parse(patchText);
723
+ }
724
+ catch {
725
+ throw new Error("direct reflect response contained invalid frontmatter patch JSON");
726
+ }
727
+ const frontmatter = parseReflectFrontmatterPatch(parsedPatch);
728
+ return { ref, content, confidence, ...(frontmatter ? { frontmatter } : {}) };
729
+ }
730
+ function parseDirectReflectOutput(raw, mode, targetRef) {
731
+ return mode === "json_schema" ? parseSchemaReflectOutput(raw, targetRef) : parseFramedReflectOutput(raw, targetRef);
732
+ }
648
733
  /**
649
734
  * Run a single reflect iteration directly via the LLM API (v2 config path).
650
735
  *
651
736
  * Returns an {@link AgentRunResult}-shaped object so it can slot into the same
652
- * dispatch loop as agent-based runners. On success, `stdout` contains the raw
653
- * LLM response (unparsed JSON or prose). On failure, the error is captured
654
- * into the result rather than thrown.
737
+ * dispatch loop as agent-based runners. Production calls extract the selected
738
+ * direct-LLM contract and normalize it to proposal JSON in `stdout`. Errors
739
+ * are captured into the result rather than thrown.
655
740
  */
656
741
  export async function runReflectViaLlm(opts) {
657
742
  const start = Date.now();
743
+ let repairAttempts = 0;
658
744
  const messages = [{ role: "user", content: opts.prompt ?? "" }];
745
+ const configuredTimeout = Object.hasOwn(opts, "timeoutMs")
746
+ ? (opts.timeoutMs ?? null)
747
+ : Object.hasOwn(opts.connection, "timeoutMs")
748
+ ? (opts.connection.timeoutMs ?? null)
749
+ : DEFAULT_LLM_TIMEOUT_MS;
750
+ const deadline = typeof configuredTimeout === "number" ? start + configuredTimeout : undefined;
659
751
  if (opts.priorDraft !== undefined && opts.iteration > 0) {
660
752
  messages.push({ role: "assistant", content: opts.priorDraft });
661
753
  messages.push({ role: "user", content: REFLECT_CRITIQUE_PROMPT });
662
754
  }
755
+ const call = async (callMessages, repairTimeoutMs) => callStructured({
756
+ feature: "reflect_proposal",
757
+ config: opts.connection,
758
+ messages: callMessages,
759
+ request: {
760
+ ...(repairTimeoutMs !== undefined
761
+ ? { timeoutMs: repairTimeoutMs }
762
+ : Object.hasOwn(opts, "timeoutMs")
763
+ ? { timeoutMs: opts.timeoutMs }
764
+ : {}),
765
+ ...(opts.signal ? { signal: opts.signal } : {}),
766
+ ...(opts.responseSchema !== undefined ? { responseSchema: opts.responseSchema } : {}),
767
+ ...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
768
+ // Reflect requires a machine-readable payload. Visible chain-of-thought
769
+ // can consume the output cap before the model reaches the envelope.
770
+ enableThinking: false,
771
+ ...(opts.chat ? { chat: opts.chat } : {}),
772
+ },
773
+ parse: (raw) => raw ?? "",
774
+ // Unreachable on the ungated path (errors propagate to the catch below).
775
+ onError: () => "",
776
+ fallback: "",
777
+ });
778
+ const failure = (err, reason, repairAttempts, stdout = "", exitCode = 1) => {
779
+ const msg = err instanceof Error ? err.message : String(err);
780
+ return {
781
+ ok: false,
782
+ stdout,
783
+ stderr: msg,
784
+ durationMs: Date.now() - start,
785
+ exitCode,
786
+ reason,
787
+ error: msg,
788
+ parsed: { outputMode: opts.outputMode, repairAttempts },
789
+ };
790
+ };
663
791
  try {
664
- let stdout;
665
- if (opts.chat) {
666
- // Test seam: injected chat function (two-arg signature, no responseSchema).
667
- stdout = await opts.chat(opts.connection, messages);
792
+ if (opts.signal?.aborted)
793
+ throw new Error("Reflect request aborted");
794
+ const stdout = await call(messages);
795
+ let payload;
796
+ let acceptedOutput = stdout;
797
+ try {
798
+ payload = parseDirectReflectOutput(stdout, opts.outputMode, opts.targetRef);
668
799
  }
669
- else {
670
- // Production path: full chatCompletion with optional structured-output schema
671
- // and optional hard max_tokens cap (derived from source body size).
672
- stdout = await chatCompletion(opts.connection, messages, {
673
- ...(opts.responseSchema !== undefined ? { responseSchema: opts.responseSchema } : {}),
674
- ...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
675
- });
800
+ catch (err) {
801
+ if (opts.allowRepair === false)
802
+ return failure(err, "parse_error", 0, stdout, 0);
803
+ if (opts.signal?.aborted)
804
+ return failure(new Error("Reflect request aborted"), "aborted", 0, stdout);
805
+ const remaining = deadline === undefined ? undefined : deadline - Date.now();
806
+ if (remaining !== undefined && remaining <= 0) {
807
+ return failure(new LlmCallError("Reflect request timed out before output repair", "timeout"), "timeout", 0, stdout);
808
+ }
809
+ repairAttempts = 1;
810
+ const repairMessages = [
811
+ ...messages,
812
+ { role: "assistant", content: stdout },
813
+ {
814
+ role: "user",
815
+ content: buildReflectOutputRepairPrompt(opts.outputMode, opts.targetRef !== undefined),
816
+ },
817
+ ];
818
+ const repaired = await call(repairMessages, remaining);
819
+ acceptedOutput = repaired;
820
+ try {
821
+ payload = parseDirectReflectOutput(repaired, opts.outputMode, opts.targetRef);
822
+ }
823
+ catch (err) {
824
+ return failure(err, "parse_error", repairAttempts, repaired, 0);
825
+ }
676
826
  }
677
827
  return {
678
828
  ok: true,
679
- stdout,
829
+ stdout: JSON.stringify(payload),
680
830
  stderr: "",
681
831
  durationMs: Date.now() - start,
682
832
  exitCode: 0,
833
+ parsed: { outputMode: opts.outputMode, repairAttempts, priorDraft: acceptedOutput },
683
834
  };
684
835
  }
685
836
  catch (err) {
686
- const msg = err instanceof Error ? err.message : String(err);
687
- return {
688
- ok: false,
689
- stdout: "",
690
- stderr: msg,
691
- durationMs: Date.now() - start,
692
- exitCode: 1,
693
- reason: "non_zero_exit",
694
- error: msg,
695
- };
837
+ const reason = opts.signal?.aborted
838
+ ? "aborted"
839
+ : err instanceof LlmCallError && err.code === "timeout"
840
+ ? "timeout"
841
+ : "non_zero_exit";
842
+ return failure(err, reason, repairAttempts);
696
843
  }
697
844
  }
698
- function failureEnvelope(result, ref, fallbackReason = "non_zero_exit") {
845
+ function failureEnvelope(result, ref, engine, fallbackReason = "non_zero_exit") {
699
846
  return {
700
847
  ...baseFailureFields(result, fallbackReason),
848
+ schemaVersion: 2,
701
849
  ...(ref ? { ref } : {}),
850
+ ...(engine ? { engine } : {}),
702
851
  };
703
852
  }
704
- export async function akmReflect(options = {}) {
705
- const stash = options.stashDir ?? resolveStashDir();
706
- // 1. Always emit `reflect_invoked` at command entry — observers see the
707
- // attempt regardless of downstream success/failure.
708
- appendEvent({
709
- eventType: "reflect_invoked",
710
- ...(options.ref ? { ref: options.ref } : {}),
711
- metadata: {
712
- ...(options.task ? { task: options.task } : {}),
713
- ...(options.profile ? { profile: options.profile } : {}),
714
- // Attribution tagging: stamp the eligibility lane so reflect_invoked can be
715
- // sliced by lane downstream. See EligibilitySource.
716
- ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
717
- },
718
- });
719
- // Fix #3 (observability 0.8.0): every failure path below MUST emit
720
- // `reflect_completed` so observers can close the invoke/complete loop. The
721
- // three success-side `reflect_completed` emit sites carry rich metadata
722
- // (qualityRejected, sanitized, proposalId, etc.); the failure-side emits
723
- // carry `{ok: false, reason}` plus the ref when known. Stable failure
724
- // reasons line up with `AgentFailureReason`: "parse_error", "non_zero_exit",
725
- // "cooldown", "timeout", "spawn_failed", "llm_*", plus the synthetic
726
- // "ref_mismatch" / "enoent" / "draft_missing" subtypes for cases the agent
727
- // surface conflates as "parse_error". Sub-reasons land in `subreason`.
728
- const emitReflectFailed = (reason, subreason, ref, extra) => {
853
+ /**
854
+ * Reflect content-preservation + proposal creation: restore/reset protected
855
+ * frontmatter and reject unsafe body-size ratios (sanitizeReflectPayload), the
856
+ * #580 noise gate, the optional quality judge, then create the proposal (with
857
+ * the R-4/#373 lesson provenance stamp) and emit `reflect_completed`. Extracted
858
+ * verbatim from `akmReflect`; every reject/skip envelope and event is
859
+ * byte-identical.
860
+ */
861
+ async function finalizeReflectProposal(args) {
862
+ const { assetContent, result, options, engineName, config, activeStrategy, runnerSpec, feedback, stash, emitReflectFailed, } = args;
863
+ let payload = args.payload;
864
+ const outputTelemetry = reflectLlmTelemetry(result);
865
+ // 7. Reflect content-preservation rails:
866
+ // - Restore source frontmatter so reflect can never strip indexable
867
+ // fields (`description`, `when_to_use`, `tags`, ...).
868
+ // - Reset protected identity fields (`name`, `ref`, `id`, `slug`,
869
+ // `type`) the LLM tried to change.
870
+ // - Reject proposals that shrink/expand the body past safe ratios.
871
+ //
872
+ // See REFLECT_ALLOWED_TYPES / sanitizeReflectPayload for the underlying
873
+ // hypotheses + observed regressions (`8737ab63`, `26941510`, and the
874
+ // catastrophic-shrinkage cases from the May 2026 review).
875
+ const sanitizeOutcome = sanitizeReflectPayload({ content: payload.content, ...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}) }, assetContent, payload.ref);
876
+ if (sanitizeOutcome.reject) {
729
877
  appendEvent({
730
878
  eventType: "reflect_completed",
731
- ...(ref ? { ref } : {}),
879
+ ref: payload.ref,
732
880
  metadata: {
733
881
  source: "reflect",
734
- ok: false,
735
- reason,
736
- subreason,
737
- ...(extra ?? {}),
882
+ sanitized: true,
883
+ rejected: true,
884
+ rejectReason: sanitizeOutcome.reject.error,
885
+ ...(sanitizeOutcome.warnings.length > 0 ? { sanitizerWarnings: sanitizeOutcome.warnings } : {}),
886
+ ...(outputTelemetry ?? {}),
738
887
  },
739
- });
888
+ }, options.eventsCtx);
889
+ return {
890
+ schemaVersion: 2,
891
+ ok: false,
892
+ reason: sanitizeOutcome.reject.reason,
893
+ error: sanitizeOutcome.reject.error,
894
+ ...(options.ref ? { ref: options.ref } : {}),
895
+ engine: engineName,
896
+ exitCode: result.exitCode,
897
+ };
898
+ }
899
+ payload = {
900
+ ...payload,
901
+ content: sanitizeOutcome.content,
902
+ ...(sanitizeOutcome.frontmatter ? { frontmatter: sanitizeOutcome.frontmatter } : {}),
740
903
  };
741
- // 2. Resolve target asset content (if a ref is supplied).
742
- let assetContent;
743
- let parsedRef;
744
- if (options.ref) {
745
- parsedRef = parseAssetRef(options.ref);
746
- // 2a. Type guard — reflect only operates on asset types whose canonical
747
- // shape is `frontmatter + markdown body`. Refuse non-markdown types
748
- // (script / env / task) up-front so reflect never prepends YAML to a
749
- // `.ts` file or rewrites a `.env` blob as prose. See REFLECT_ALLOWED_TYPES.
750
- if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type)) {
751
- // Deterministic type-guard rejection — the LLM is never invoked. Emit
752
- // with reason `unsupported_type` so the improve loop can route this to
753
- // the `reflect-skipped` action bucket instead of `reflect-failed`. See
754
- // `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a
755
- // ("Reflect refused asset type" — ~9% of reflect-failed events).
756
- emitReflectFailed("unsupported_type", "unsupported_type", options.ref, { type: parsedRef.type });
904
+ // 7c. Noise gate (#580): never queue a proposal whose sanitized content is
905
+ // identical to the current asset (empty diff) or differs only cosmetically
906
+ // (whitespace reflow, code-fence language hints, YAML scalar re-folding).
907
+ // Pure deterministic text comparison — see `reflect-noise.ts`. Skipped when
908
+ // there is no source asset (new-asset proposals have nothing to diff against).
909
+ if (assetContent !== undefined) {
910
+ const changeKind = classifyReflectChange(assetContent, payload.content);
911
+ // 'low-value' is config-gated (#639). DEFAULT OFF — absent = byte-identical
912
+ // pre-#639 behaviour (low-value treated the same as substantive). Resolved
913
+ // by the caller from the active improve strategy's
914
+ // `processes.reflect.lowValueFilter.enabled` and passed via options, so the
915
+ // running strategy decides.
916
+ const lowValueFilterEnabled = options.lowValueFilter === true;
917
+ const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
918
+ if (isDeferred) {
919
+ const subreason = changeKind === "noop"
920
+ ? "reflect_skipped_noop"
921
+ : changeKind === "low-value"
922
+ ? "reflect_skipped_low_value"
923
+ : "reflect_skipped_cosmetic";
924
+ emitReflectFailed("no_change", subreason, options.ref, { changeKind, ...(outputTelemetry ?? {}) });
757
925
  return {
758
- schemaVersion: 1,
926
+ schemaVersion: 2,
759
927
  ok: false,
760
- reason: "unsupported_type",
761
- error: `Reflect refused: asset type "${parsedRef.type}" is not supported by reflect (only markdown-canonical types are allowed: ${[...REFLECT_ALLOWED_TYPES].sort().join(", ")}). Use \`akm propose\` or edit the file directly.`,
762
- ref: options.ref,
763
- exitCode: null,
928
+ reason: "no_change",
929
+ error: changeKind === "noop"
930
+ ? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
931
+ : changeKind === "low-value"
932
+ ? `Reflect skipped: proposed content for ${payload.ref} is a low-value prose micro-rewrite (few changed tokens, no structural changes); no proposal created.`
933
+ : `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
934
+ ...(options.ref ? { ref: options.ref } : {}),
935
+ engine: engineName,
936
+ exitCode: result.exitCode,
764
937
  };
765
938
  }
766
- if (options.assetContent !== undefined) {
767
- // Test seam — caller pre-loaded the source content.
768
- assetContent = options.assetContent;
769
- }
770
- else {
771
- try {
772
- const entry = await lookup(parsedRef);
773
- if (entry?.filePath && fs.existsSync(entry.filePath)) {
774
- assetContent = fs.readFileSync(entry.filePath, "utf8");
775
- }
776
- }
777
- catch {
778
- // Index miss is non-fatal — the agent can still propose a fresh asset.
779
- }
939
+ }
940
+ // 7c. Judge the exact sanitized content that can be persisted. Fail closed
941
+ // on cancellation, transport failure, malformed output, or an invalid score.
942
+ const qualityGateEnabled = (activeStrategy?.processes?.reflect?.qualityGate?.enabled ?? false) ||
943
+ (activeStrategy?.processes?.distill?.qualityGate?.enabled ?? true);
944
+ if (qualityGateEnabled) {
945
+ const judgeResult = await runReflectQualityJudge(config, payload.content, assetContent ?? "", feedback, options.chat, {
946
+ ...(runnerIsLlm(runnerSpec) ? { llmConfig: materializeLlmRunnerConnection(runnerSpec) } : {}),
947
+ ...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
948
+ ...(options.signal ? { signal: options.signal } : {}),
949
+ });
950
+ if (!judgeResult.pass) {
951
+ appendEvent({
952
+ eventType: "reflect_completed",
953
+ ref: payload.ref,
954
+ metadata: {
955
+ source: "reflect",
956
+ qualityRejected: true,
957
+ qualityScore: judgeResult.score,
958
+ qualityReason: judgeResult.reason,
959
+ ...(outputTelemetry ?? {}),
960
+ },
961
+ }, options.eventsCtx);
962
+ return {
963
+ schemaVersion: 2,
964
+ ok: false,
965
+ reason: "parse_error",
966
+ error: `Reflect proposal quality gate rejected: score=${judgeResult.score}, reason="${judgeResult.reason}"`,
967
+ ...(options.ref ? { ref: options.ref } : {}),
968
+ engine: engineName,
969
+ exitCode: result.exitCode,
970
+ };
780
971
  }
781
972
  }
782
- // 3. Resolve agent profile. ConfigError surfaces as a thrown error so the
783
- // CLI dispatcher renders the standard envelope.
973
+ return createReflectProposal({
974
+ payload,
975
+ options,
976
+ stash,
977
+ engineName,
978
+ durationMs: result.durationMs,
979
+ emitReflectFailed,
980
+ outputTelemetry,
981
+ });
982
+ }
983
+ /**
984
+ * Create the reflect proposal from sanitized+judged payload: stamp the R-4/#373
985
+ * lesson provenance marker, call `createProposal`, and emit the terminal
986
+ * `reflect_completed` (or a cooldown skip envelope). Extracted verbatim from
987
+ * `akmReflect`'s finalize tail.
988
+ */
989
+ function createReflectProposal(args) {
990
+ const { payload, options, stash, engineName, durationMs, emitReflectFailed, outputTelemetry } = args;
991
+ // 8. Create the proposal. The proposal queue is the ONLY thing reflect
992
+ // writes — promotion to a real asset is gated by `akm proposal accept`.
784
993
  //
785
- // When an explicit --profile flag is given, honour it directly (existing
786
- // behaviour). Otherwise use resolveProcessAgentProfile so that per-process
787
- // agent config (agent.processes["reflect"]) is picked up automatically.
788
- let profile;
789
- let resolvedTimeoutMs = options.timeoutMs;
790
- let runnerSpec;
791
- try {
792
- if (options.agentProfile) {
793
- // Test seam: injected profile bypasses all config.
794
- profile = options.agentProfile;
795
- }
796
- else if (options.runner) {
797
- // Caller-provided RunnerSpec (used in tests and --dry-run-resolve).
798
- runnerSpec = options.runner;
799
- }
800
- else {
801
- const cfg = options.config ?? loadConfig();
802
- const reflectProcess = getImproveProcessConfig(cfg, "reflect", options.improveProfile);
803
- // Resolve the runner from the improve profile's reflect entry when present.
804
- runnerSpec = resolveImproveProcessRunnerFromProfile(reflectProcess, cfg) ?? undefined;
805
- if (runnerSpec) {
806
- if (resolvedTimeoutMs === undefined && runnerSpec.timeoutMs !== undefined) {
807
- resolvedTimeoutMs = runnerSpec.timeoutMs;
808
- }
809
- }
810
- else {
811
- if (options.profile) {
812
- // Explicit --profile flag wins over process config.
813
- profile = resolveAgentProfile(options);
814
- }
815
- else {
816
- // Use per-process config resolution (falls back to defaults.agent).
817
- const agent = options.agentConfig ?? loadAgentConfigFromDisk();
818
- const processName = options.agentProcess ?? "reflect";
819
- const resolved = resolveProcessAgentProfile(processName, agent);
820
- profile = resolved.profile;
821
- // Only apply process-resolved timeoutMs when caller didn't supply one.
822
- if (resolvedTimeoutMs === undefined) {
823
- resolvedTimeoutMs = resolved.timeoutMs;
824
- }
825
- }
826
- }
994
+ // R-4 / #373: Stamp `derived_from_reflect: true` in the frontmatter of any
995
+ // lesson proposal generated by reflect. This provenance marker lets
996
+ // `readRelatedLessons` exclude echo-chamber lessons (lessons that originate
997
+ // from prior reflect runs on the same skill) unless independent feedback
998
+ // evidence exists. ExpeL arXiv:2308.10144 — reject rules without success/
999
+ // failure differential from independent evidence.
1000
+ const isLessonProposal = (() => {
1001
+ try {
1002
+ return parseRefInput(payload.ref).type === "lesson";
827
1003
  }
828
- }
829
- catch (err) {
830
- if (err instanceof ConfigError || err instanceof UsageError)
831
- throw err;
832
- throw err;
833
- }
834
- // P1.3 (meta-review 07, Chain G): unattended `akm improve` must never hand
835
- // reflect a tool-capable runner. Agent-CLI/SDK runners have filesystem
836
- // access, so a poisoned asset body flowing through the scheduled reflect
837
- // prompt could steer an agent with tools; the tool-less LLM HTTP runner
838
- // corrupts output text at worst (src/llm/call-ai.ts). An llm RunnerSpec
839
- // resolved from the process block is honored; anything else — an agent/sdk
840
- // spec, a resolved agent profile, or the default-agent fallback below — is
841
- // pinned to `defaults.llm`, failing CLOSED when that is unset.
842
- // `options.agentProfile` stays exempt: it is a direct-injection test seam
843
- // the cron path (loop-stages) never sets.
844
- if (options.eventSource === "improve" && !options.agentProfile && (!runnerSpec || !runnerIsLlm(runnerSpec))) {
845
- const cfg = options.config ?? loadConfig();
846
- const pinned = resolveDefaultLlmRunner(cfg, resolvedTimeoutMs);
847
- if (!pinned) {
848
- throw new ConfigError("Unattended improve pins reflect to the tool-less LLM runner, but the config resolves a tool-capable runner and no defaults.llm profile exists to pin to.", "LLM_NOT_CONFIGURED", 'Set processes.reflect.mode to "llm" on the active improve profile, or configure defaults.llm.');
1004
+ catch {
1005
+ return false;
849
1006
  }
850
- warn(runnerSpec
851
- ? `[akm] reflect: unattended improve resolved a tool-capable "${runnerSpec.kind}" runner — pinned to the tool-less LLM runner (07 Chain-G).`
852
- : "[akm] reflect: unattended improve would have fallen back to an agent profile — pinned to the tool-less LLM runner (07 Chain-G).");
853
- runnerSpec = pinned;
854
- profile = undefined;
855
- }
856
- // Ensure profile is set for agent/sdk runners that don't use runnerSpec
857
- if (!runnerSpec && !profile) {
858
- const agent = options.agentConfig ?? loadAgentConfigFromDisk();
859
- profile = resolveAgentProfile({ ...options, agentConfig: agent });
860
- }
861
- // Derive a display name for logging — either from the resolved profile or the runnerSpec.
862
- const resolvedProfileName = profile?.name ??
863
- (runnerSpec && runnerIsLlm(runnerSpec)
864
- ? `llm:${runnerSpec.connection.model}`
865
- : runnerSpec
866
- ? `${runnerSpec.kind}:${runnerSpec.profile.name ?? "unknown"}`
867
- : "unknown");
868
- // 4. Build the shared prompt inputs — feedback, hints, lessons, rejected
869
- // proposals. These are stable across refinement iterations; only the
870
- // `priorDraft` field changes per-iteration (R-1 / #372).
871
- const feedback = readRecentFeedback(options.ref);
872
- const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
873
- const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(stash, options.ref, parsedRef) : [];
874
- // Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
875
- // reproducing proposals that have already been reviewed and refused.
876
- const rejectedProposals = readRejectedProposals(stash, options.ref);
877
- // Standards "rulebook" for this target — wiki schema (wiki page) or stash
878
- // convention/meta facts (non-wiki asset); empty when neither fires.
879
- const standardsContext = resolveStandardsContext(options.ref, stash);
880
- // 5. Spawn the agent — with optional Self-Refine loop (R-1 / #372).
881
- //
882
- // maxRefineIters controls how many agent invocations are made:
883
- // - 1 (default): single-shot, same as pre-R-1 behaviour
884
- // - 2–3: on each subsequent pass, the prior draft is injected back into
885
- // the prompt as Self-Refine critique context (arXiv:2303.17651)
886
- //
887
- // The loop exits early when the agent returns the same content as before
888
- // (no-op refinement) to avoid wasting tokens on identical iterations.
889
- const MAX_REFINE_ITERS = 3;
890
- const maxRefineIters = Math.min(Math.max(1, options.maxRefineIters ?? 1), MAX_REFINE_ITERS);
891
- const agentEnv = options.eventSource === "improve" ? { AKM_EVENT_SOURCE: "improve" } : {};
1007
+ })();
1008
+ const basePayloadFrontmatter = payload.frontmatter ?? {};
1009
+ const payloadFrontmatterWithProvenance = isLessonProposal
1010
+ ? { ...basePayloadFrontmatter, derived_from_reflect: true }
1011
+ : basePayloadFrontmatter;
1012
+ const createInput = {
1013
+ ref: payload.ref,
1014
+ ...(options.target ? { target: options.target } : {}),
1015
+ source: "reflect",
1016
+ sourceRun: `reflect-${Date.now()}`,
1017
+ payload: {
1018
+ content: payload.content,
1019
+ ...(Object.keys(payloadFrontmatterWithProvenance).length > 0
1020
+ ? { frontmatter: payloadFrontmatterWithProvenance }
1021
+ : {}),
1022
+ },
1023
+ // Phase 6A: forward LLM-reported confidence into the proposal record.
1024
+ // `parseAgentProposalPayload` already clamps to [0, 1] and drops non-
1025
+ // finite values; `createProposal` runs its own sanitizer as a safety net.
1026
+ ...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
1027
+ // Attribution tagging: persist the eligibility lane on the proposal so it
1028
+ // survives to accept/reject/revert time even across runs. See EligibilitySource.
1029
+ ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
1030
+ // §23.6 fingerprint model-id term (WI-6.4): the engine that generated
1031
+ // this draft (reflect resolves engines, not bare model ids).
1032
+ modelId: engineName,
1033
+ };
1034
+ const proposalResult = emitProposal({ stashDir: stash, proposalsCtx: options.ctx }, createInput);
1035
+ if (isProposalSkipped(proposalResult)) {
1036
+ // Dedup/cooldown guard fired — surface as a "cooldown" reason (not "parse_error")
1037
+ // so the improve orchestrator can distinguish legitimate skips from real failures
1038
+ // and exclude them from recentErrors/avoidPatterns injection.
1039
+ emitReflectFailed("cooldown", "proposal_skipped", options.ref, {
1040
+ proposalSkipReason: proposalResult.reason,
1041
+ ...(outputTelemetry ?? {}),
1042
+ });
1043
+ return {
1044
+ schemaVersion: 2,
1045
+ ok: false,
1046
+ reason: "cooldown",
1047
+ error: `Proposal skipped (${proposalResult.reason}): ${proposalResult.message}`,
1048
+ ...(options.ref ? { ref: options.ref } : {}),
1049
+ engine: engineName,
1050
+ exitCode: null,
1051
+ };
1052
+ }
1053
+ const proposal = proposalResult;
1054
+ appendEvent({
1055
+ eventType: "reflect_completed",
1056
+ ref: proposal.ref,
1057
+ metadata: {
1058
+ proposalId: proposal.id,
1059
+ source: "reflect",
1060
+ engine: engineName,
1061
+ ...(outputTelemetry ?? {}),
1062
+ },
1063
+ }, options.eventsCtx);
1064
+ return {
1065
+ schemaVersion: 2,
1066
+ ok: true,
1067
+ proposal,
1068
+ ref: proposal.ref,
1069
+ engine: engineName,
1070
+ durationMs,
1071
+ };
1072
+ }
1073
+ /**
1074
+ * Resolve the agent's proposal payload from a successful run: the file-write
1075
+ * contract path (read `lastDraftPath`, extract self-rated confidence) or the
1076
+ * JSON-stdout path used by direct LLM runners. Returns the payload or a terminal
1077
+ * failure envelope.
1078
+ */
1079
+ function resolveReflectPayload(args) {
1080
+ const { result, lastDraftPath, sensitiveValues, options, engineName, emitReflectFailed } = args;
1081
+ // 6. Resolve the proposal content.
1082
+ //
1083
+ // Path A (file-write contract — preferred for agent/sdk runners on long
1084
+ // assets): the agent wrote the body to `lastDraftPath` and printed
1085
+ // `DRAFT_WRITTEN` on stdout. Load the body from disk and synthesize a
1086
+ // payload. The `EXCESSIVE_EXPANSION`/schema-shape gates downstream still
1087
+ // apply — they validate content, not transport.
1088
+ //
1089
+ // Path B (JSON stdout): the direct LLM runner cannot honour file-write.
1090
+ const draftFileExists = lastDraftPath !== undefined && fs.existsSync(lastDraftPath) && fs.statSync(lastDraftPath).size > 0;
1091
+ const draftSignaled = stdoutSignalsDraftWritten(result.stdout);
1092
+ if (draftSignaled && lastDraftPath && !draftFileExists) {
1093
+ // Agent claimed to write the draft but the file is missing or empty.
1094
+ // Surface as a parse_error rather than silently falling through — the
1095
+ // alternative would be parsing the `DRAFT_WRITTEN` sentinel as JSON,
1096
+ // which is guaranteed to fail with a confusing message.
1097
+ emitReflectFailed("parse_error", "draft_missing", options.ref, {
1098
+ ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1099
+ });
1100
+ return {
1101
+ failure: {
1102
+ schemaVersion: 2,
1103
+ ok: false,
1104
+ reason: "parse_error",
1105
+ error: `Agent emitted DRAFT_WRITTEN but draft file is missing or empty (${lastDraftPath}). The file-write contract failed; either the agent's file tools are broken or the path was unwritable.`,
1106
+ ...(options.ref ? { ref: options.ref } : {}),
1107
+ engine: engineName,
1108
+ exitCode: result.exitCode,
1109
+ stdout: result.stdout,
1110
+ ...(result.stderr ? { stderr: result.stderr } : {}),
1111
+ },
1112
+ };
1113
+ }
1114
+ if (draftFileExists && lastDraftPath) {
1115
+ // Happy path: agent wrote the body to disk. Use the ref the caller
1116
+ // supplied (or a placeholder when omitted — the R-3 ref-mismatch guard
1117
+ // below has no effect when there is no expected ref).
1118
+ const fileContent = redactSensitiveText(fs.readFileSync(lastDraftPath, "utf8"), sensitiveValues);
1119
+ // Phase 6A: file-write contract carries self-rated confidence on the
1120
+ // `DRAFT_WRITTEN confidence=<n>` sentinel line. Extract it so the
1121
+ // file-write path is on equal footing with the JSON-stdout path for
1122
+ // auto-accept gating in `akm improve`.
1123
+ const draftConfidence = extractDraftConfidence(result.stdout);
1124
+ return {
1125
+ payload: {
1126
+ ref: options.ref ?? "",
1127
+ content: fileContent,
1128
+ ...(draftConfidence !== undefined ? { confidence: draftConfidence } : {}),
1129
+ },
1130
+ };
1131
+ }
1132
+ try {
1133
+ return { payload: parseAgentProposalPayload(result.stdout ?? "") };
1134
+ }
1135
+ catch (err) {
1136
+ // Reclassify cooldown/skip messages that arrive as stdout text instead of
1137
+ // valid proposal JSON. These are legitimate skip signals, not parse failures,
1138
+ // and should not pollute reflectFailedActions or recentErrors injection.
1139
+ const stdoutText = result.stdout ?? "";
1140
+ const isCooldownSignal = isStructuredCooldownSignal(stdoutText);
1141
+ const reason = isCooldownSignal ? "cooldown" : "parse_error";
1142
+ emitReflectFailed(reason, isCooldownSignal ? "stdout_cooldown_signal" : "parse_error", options.ref, {
1143
+ ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1144
+ ...(reflectLlmTelemetry(result) ?? {}),
1145
+ });
1146
+ return {
1147
+ failure: {
1148
+ schemaVersion: 2,
1149
+ ok: false,
1150
+ reason,
1151
+ error: err instanceof Error ? err.message : String(err),
1152
+ ...(options.ref ? { ref: options.ref } : {}),
1153
+ engine: engineName,
1154
+ exitCode: result.exitCode,
1155
+ stdout: result.stdout,
1156
+ ...(result.stderr ? { stderr: result.stderr } : {}),
1157
+ },
1158
+ };
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Resolve the single named engine for a reflect invocation (standalone --engine
1163
+ * / defaults.engine, or the improve strategy's LLM-only process overlay),
1164
+ * throwing on any incompatible or missing engine, and validating the unattended
1165
+ * LLM requirement. Extracted verbatim from `akmReflect`.
1166
+ */
1167
+ function resolveReflectRunner(options) {
1168
+ const config = options.config ?? loadConfig();
1169
+ const activeStrategy = options.improveProfile ?? config.improve?.strategies?.[config.defaults?.improveStrategy ?? "default"];
1170
+ let runnerSpec;
1171
+ if (options.runner) {
1172
+ runnerSpec = options.runner;
1173
+ }
1174
+ else if (Object.hasOwn(options, "runner")) {
1175
+ throw new ConfigError("Reflect requires an LLM engine for the active improve invocation.", "LLM_NOT_CONFIGURED", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine.");
1176
+ }
1177
+ else if (options.engine) {
1178
+ runnerSpec = resolveEngine(options.engine, config);
1179
+ }
1180
+ else if (options.improveProfile) {
1181
+ const processRunner = resolveImproveProcessRunner(activeStrategy, "reflect", config);
1182
+ if (!processRunner) {
1183
+ throw new ConfigError("Reflect requires an LLM engine for the active improve strategy.", "LLM_NOT_CONFIGURED", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine.");
1184
+ }
1185
+ runnerSpec = processRunner;
1186
+ }
1187
+ else {
1188
+ const defaultEngine = config.defaults?.engine;
1189
+ if (!defaultEngine) {
1190
+ throw new ConfigError("reflect requires --engine or defaults.engine.", "INVALID_CONFIG_FILE");
1191
+ }
1192
+ runnerSpec = resolveEngine(defaultEngine, config);
1193
+ }
1194
+ if (options.eventSource === "improve" && !runnerIsLlm(runnerSpec)) {
1195
+ throw new ConfigError(`Unattended improve requires an LLM engine for reflect; engine "${runnerSpec.engine ?? options.engine ?? "unknown"}" is tool-capable.`, "INVALID_CONFIG_FILE", "Set defaults.llmEngine or improve.strategies.<name>.processes.reflect.engine to an LLM engine.");
1196
+ }
1197
+ const engineName = runnerSpec.engine ?? options.engine;
1198
+ if (!engineName) {
1199
+ throw new ConfigError("Reflect requires a named engine.", "INVALID_CONFIG_FILE");
1200
+ }
1201
+ return { config, activeStrategy, runnerSpec, engineName };
1202
+ }
1203
+ /**
1204
+ * Resolve the reflect target's parsed ref + current on-disk content: enforce the
1205
+ * REFLECT_ALLOWED_TYPES markdown-canonical type guard (returning a terminal
1206
+ * `unsupported_type` failure), honour the `options.assetContent` test seam, else
1207
+ * best-effort load via the local file path / index lookup. Extracted verbatim
1208
+ * from `akmReflect`.
1209
+ */
1210
+ async function resolveReflectSource(options, stash, emitReflectFailed) {
1211
+ let assetContent;
1212
+ let parsedRef;
1213
+ if (options.ref) {
1214
+ parsedRef = parseRefInput(options.ref);
1215
+ // 2a. Type guard — reflect only operates on asset types whose canonical
1216
+ // shape is `frontmatter + markdown body`. Refuse non-markdown types
1217
+ // (script / env / task) up-front so reflect never prepends YAML to a
1218
+ // `.ts` file or rewrites a `.env` blob as prose. See REFLECT_ALLOWED_TYPES.
1219
+ if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type)) {
1220
+ // Deterministic type-guard rejection — the LLM is never invoked. Emit
1221
+ // with reason `unsupported_type` so the improve loop can route this to
1222
+ // the `reflect-skipped` action bucket instead of `reflect-failed`. See
1223
+ // `/tmp/akm-health-investigations/metrics-taxonomy-review.md` §1a
1224
+ // ("Reflect refused asset type" — ~9% of reflect-failed events).
1225
+ emitReflectFailed("unsupported_type", "unsupported_type", options.ref, { type: parsedRef.type });
1226
+ return {
1227
+ failure: {
1228
+ schemaVersion: 2,
1229
+ ok: false,
1230
+ reason: "unsupported_type",
1231
+ error: `Reflect refused: asset type "${parsedRef.type}" is not supported by reflect (only markdown-canonical types are allowed: ${[...REFLECT_ALLOWED_TYPES].sort().join(", ")}). Use \`akm proposal new\` or edit the file directly.`,
1232
+ ref: options.ref,
1233
+ exitCode: null,
1234
+ },
1235
+ };
1236
+ }
1237
+ if (options.assetContent !== undefined) {
1238
+ // Test seam — caller pre-loaded the source content.
1239
+ assetContent = options.assetContent;
1240
+ }
1241
+ else {
1242
+ try {
1243
+ // Resolve the source by item_ref when planning supplied one, otherwise
1244
+ // use the input conceptId.
1245
+ const qualifiedRef = options.itemRef ?? durableImproveRef(options.ref);
1246
+ const localFilePath = await findAssetFilePath(qualifiedRef, stash);
1247
+ if (localFilePath && fs.existsSync(localFilePath)) {
1248
+ assetContent = fs.readFileSync(localFilePath, "utf8");
1249
+ }
1250
+ else {
1251
+ const entry = await lookup(parseRefInput(qualifiedRef));
1252
+ if (entry?.filePath && fs.existsSync(entry.filePath)) {
1253
+ assetContent = fs.readFileSync(entry.filePath, "utf8");
1254
+ }
1255
+ }
1256
+ }
1257
+ catch {
1258
+ // Index miss is non-fatal — the agent can still propose a fresh asset.
1259
+ }
1260
+ }
1261
+ }
1262
+ return { assetContent, parsedRef };
1263
+ }
1264
+ /**
1265
+ * Run the agent with the optional Self-Refine loop (R-1 / #372): up to
1266
+ * MAX_REFINE_ITERS invocations, each injecting the prior draft as self-critique
1267
+ * context and exiting early on a no-op refinement. Synthesizes per-iteration
1268
+ * draft paths into `draftPathsToCleanup` (mutated) and returns the final agent
1269
+ * result + last draft path. Extracted verbatim from `akmReflect`.
1270
+ */
1271
+ async function runReflectRefineIterations(args) {
1272
+ const { options, parsedRef, assetContent, feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext, runnerSpec, agentEnv, draftPathsToCleanup, } = args;
1273
+ const MAX_REFINE_ITERS = 3;
1274
+ const maxRefineIters = Math.min(Math.max(1, options.maxRefineIters ?? 1), MAX_REFINE_ITERS);
892
1275
  // Determine whether this dispatch can honour the file-write contract.
893
1276
  // Agent CLI + OpenCode SDK runners both have filesystem access; the direct
894
- // LLM HTTP runner does NOT (see `src/llm/call-ai.ts:64-71`). The v1
895
- // `profile.sdkMode` fallback also runs the SDK so it counts as file-writable.
896
- // Test seams (`options.runAgentOptions.spawn`) emulate agent CLI behaviour so
897
- // they participate as well — tests opt out by simply not writing the file.
898
- const canRunnerWriteFile = runnerSpec ? runnerSupportsFileWrite(runnerSpec) : true;
1277
+ // LLM HTTP runner does NOT.
1278
+ const canRunnerWriteFile = runnerSupportsFileWrite(runnerSpec);
1279
+ const outputMode = runnerIsLlm(runnerSpec)
1280
+ ? runnerSpec.connection.supportsJsonSchema
1281
+ ? "json_schema"
1282
+ : "framed_markdown"
1283
+ : undefined;
899
1284
  // Initialized to a sentinel; always overwritten in the first loop iteration
900
- // (maxRefineIters is clamped to >= 1 above). TypeScript cannot prove a
901
- // for-loop always runs at least once, so we use a type assertion here.
1285
+ // (maxRefineIters is clamped to >= 1 above).
902
1286
  let result = {};
903
1287
  let priorDraft;
1288
+ let lastDraftPath;
1289
+ let repairAttempts = 0;
1290
+ for (let iter = 0; iter < maxRefineIters; iter++) {
1291
+ // Synthesize a fresh tmp path per iteration so refinement passes never
1292
+ // clobber an earlier draft (and so reading back is unambiguous).
1293
+ const iterDraftPath = canRunnerWriteFile ? synthesizeReflectDraftPath(options.ref) : undefined;
1294
+ if (iterDraftPath) {
1295
+ draftPathsToCleanup.push(iterDraftPath);
1296
+ lastDraftPath = iterDraftPath;
1297
+ }
1298
+ const { prompt, maxOutputChars } = buildReflectPrompt({
1299
+ ...(options.ref ? { ref: options.ref } : {}),
1300
+ ...(parsedRef?.type ? { type: parsedRef.type } : {}),
1301
+ ...(parsedRef?.name ? { name: parsedRef.name } : {}),
1302
+ ...(assetContent !== undefined ? { assetContent } : {}),
1303
+ ...(feedback.length > 0 ? { feedback } : {}),
1304
+ ...(schemaHints.length > 0 ? { schemaHints } : {}),
1305
+ ...(relatedLessons.length > 0 ? { relatedLessons } : {}),
1306
+ ...(options.task ? { task: options.task } : {}),
1307
+ ...(standardsContext.trim() ? { standardsContext } : {}),
1308
+ ...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
1309
+ ...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
1310
+ // R-1: inject prior draft as self-critique target on iterations > 0
1311
+ ...(priorDraft !== undefined ? { priorDraft } : {}),
1312
+ // Issue A (#reflect-pipeline file-write contract): when the runner can
1313
+ // touch the filesystem, instruct the agent to write the proposal body
1314
+ // to a tmp file instead of inlining it in JSON. Avoids parse failures
1315
+ // on long bodies (e.g. knowledge/systems/KOKORO_USAGE_GUIDE 8.4KB).
1316
+ ...(iterDraftPath ? { draftFilePath: iterDraftPath } : {}),
1317
+ ...(outputMode ? { outputMode } : {}),
1318
+ });
1319
+ // Convert char ceiling → token cap for the LLM path: divide by 3 chars/token
1320
+ // (conservative — most models are 3.5–4) and add 500-char overhead for the
1321
+ // JSON wrapper and frontmatter block that surround the body in the response.
1322
+ const maxTokensForLlm = maxOutputChars !== undefined ? Math.ceil((maxOutputChars + 500) / 3) : undefined;
1323
+ // Every engine kind crosses the same dispatch seam. Injected spawn/timer
1324
+ // functions remain ordinary run options for deterministic tests.
1325
+ const runOptions = {
1326
+ stdio: "captured",
1327
+ parseOutput: "text",
1328
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
1329
+ ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
1330
+ ...(options.runAgentOptions ?? {}),
1331
+ };
1332
+ const iterResult = await executeRunner(runnerSpec, prompt ?? "", runOptions, {
1333
+ llm: async (spec, _prompt, opts) =>
1334
+ // LLM HTTP runners cannot honor the file-write contract, so they
1335
+ // return structured JSON through stdout.
1336
+ runReflectViaLlm({
1337
+ prompt,
1338
+ connection: spec.connection,
1339
+ ...(Object.hasOwn(opts, "timeoutMs") ? { timeoutMs: opts.timeoutMs } : {}),
1340
+ ...(options.signal ? { signal: options.signal } : {}),
1341
+ priorDraft,
1342
+ iteration: iter,
1343
+ ...(spec.connection.supportsJsonSchema
1344
+ ? { responseSchema: options.ref ? REFLECT_JSON_SCHEMA : REFLECT_UNSCOPED_JSON_SCHEMA }
1345
+ : {}),
1346
+ outputMode: spec.connection.supportsJsonSchema ? "json_schema" : "framed_markdown",
1347
+ ...(options.ref ? { targetRef: options.ref } : {}),
1348
+ allowRepair: repairAttempts === 0,
1349
+ chat: options.chat,
1350
+ ...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
1351
+ }),
1352
+ });
1353
+ const iterTelemetry = reflectLlmTelemetry(iterResult);
1354
+ if (iterTelemetry)
1355
+ repairAttempts += iterTelemetry.repairAttempts;
1356
+ result = iterTelemetry
1357
+ ? {
1358
+ ...iterResult,
1359
+ parsed: {
1360
+ ...iterResult.parsed,
1361
+ ...iterTelemetry,
1362
+ repairAttempts,
1363
+ },
1364
+ }
1365
+ : iterResult;
1366
+ if (!result.ok)
1367
+ break; // surface failure after loop
1368
+ // On success, extract the draft content for the next iteration.
1369
+ // If the agent returns the same content as the prior draft, stop early
1370
+ // (no-op refinement) to avoid wasting tokens on identical iterations.
1371
+ if (iter < maxRefineIters - 1) {
1372
+ const nextDraft = reflectLlmPriorDraft(result) ?? result.stdout ?? "";
1373
+ if (priorDraft !== undefined && nextDraft === priorDraft)
1374
+ break;
1375
+ priorDraft = nextDraft;
1376
+ }
1377
+ }
1378
+ return { result, lastDraftPath };
1379
+ }
1380
+ /**
1381
+ * WI-9.10: build one `akm reflect` invocation's {@link RunContext} purely
1382
+ * from values `akmReflect` has already resolved by the time it calls this
1383
+ * (stash, config, runnerSpec) plus the caller-supplied seams on `options` —
1384
+ * no second config load, no new db handle. reflect has no `dryRun` option
1385
+ * (it never writes source assets directly, only the proposal queue — see the
1386
+ * module docblock) so `dryRun` is always `false` here. reflect also has no
1387
+ * `sourceRun` option; the value below mirrors the same `reflect-${Date.now()}`
1388
+ * convention already used inline at proposal creation time (see
1389
+ * `createInput` further down this file), as a fresh, independent token —
1390
+ * nothing yet reads `ctx.sourceRun`.
1391
+ */
1392
+ function buildReflectRunContext(args) {
1393
+ const { options, stash, config, runnerSpec } = args;
1394
+ return createRunContext({
1395
+ stashDir: stash,
1396
+ config,
1397
+ eventsCtx: options.eventsCtx ?? {},
1398
+ // Not yet wired into any proposal call site this stage (mirrors
1399
+ // buildImproveRunContext's proposalsCtx comment in improve.ts).
1400
+ proposalsCtx: options.ctx ?? {},
1401
+ chat: options.chat,
1402
+ getLlmConfig: () => (runnerIsLlm(runnerSpec) ? materializeLlmRunnerConnection(runnerSpec) : null),
1403
+ sourceRun: `reflect-${Date.now()}`,
1404
+ dryRun: false,
1405
+ signal: options.signal,
1406
+ });
1407
+ }
1408
+ /**
1409
+ * Emit `reflect_invoked` at command entry, then build the `reflect_completed`
1410
+ * failure emitter every failure path in `akmReflect` uses (Fix #3 /
1411
+ * observability 0.8.0). Extracted verbatim (fn-size decomposition, R31) — see
1412
+ * the original inline comments preserved below for the "why".
1413
+ *
1414
+ * Fix #3 (observability 0.8.0): every failure path below MUST emit
1415
+ * `reflect_completed` so observers can close the invoke/complete loop. The
1416
+ * three success-side `reflect_completed` emit sites carry rich metadata
1417
+ * (qualityRejected, sanitized, proposalId, etc.); the failure-side emits
1418
+ * carry `{ok: false, reason}` plus the ref when known. Stable failure
1419
+ * reasons line up with `AgentFailureReason`: "parse_error", "non_zero_exit",
1420
+ * "cooldown", "timeout", "spawn_failed", "llm_*", plus the synthetic
1421
+ * "ref_mismatch" / "enoent" / "draft_missing" subtypes for cases the agent
1422
+ * surface conflates as "parse_error". Sub-reasons land in `subreason`.
1423
+ */
1424
+ function emitReflectInvokedAndBuildFailureEmitter(options) {
1425
+ // Always emit `reflect_invoked` at command entry — observers see the
1426
+ // attempt regardless of downstream success/failure.
1427
+ appendEvent({
1428
+ eventType: "reflect_invoked",
1429
+ // Key on item_ref when planning supplied one, otherwise the conceptId.
1430
+ ...(options.ref ? { ref: options.itemRef ?? durableImproveRef(options.ref) } : {}),
1431
+ metadata: {
1432
+ ...(options.task ? { task: options.task } : {}),
1433
+ ...(options.engine ? { engine: options.engine } : {}),
1434
+ // Attribution tagging: stamp the eligibility lane so reflect_invoked can be
1435
+ // sliced by lane downstream. See EligibilitySource.
1436
+ ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
1437
+ },
1438
+ }, options.eventsCtx);
1439
+ return (reason, subreason, ref, extra) => {
1440
+ appendEvent({
1441
+ eventType: "reflect_completed",
1442
+ ...(ref ? { ref } : {}),
1443
+ metadata: {
1444
+ source: "reflect",
1445
+ ok: false,
1446
+ reason,
1447
+ subreason,
1448
+ ...(extra ?? {}),
1449
+ },
1450
+ }, options.eventsCtx);
1451
+ };
1452
+ }
1453
+ export async function akmReflect(options = {}) {
1454
+ const stash = resolveRunStashDir(options.stashDir);
1455
+ // 1. Emit reflect_invoked + build the reflect_completed failure emitter
1456
+ // every failure path below uses.
1457
+ const emitReflectFailed = emitReflectInvokedAndBuildFailureEmitter(options);
1458
+ // 2. Resolve target asset content (if a ref is supplied).
1459
+ const sourceResolved = await resolveReflectSource(options, stash, emitReflectFailed);
1460
+ if ("failure" in sourceResolved)
1461
+ return sourceResolved.failure;
1462
+ const { assetContent, parsedRef } = sourceResolved;
1463
+ // 3. Resolve exactly one named engine. Standalone reflect uses --engine or
1464
+ // defaults.engine; improve resolves its LLM-only strategy/process overlay.
1465
+ // An incompatible explicit engine is an error and never falls through.
1466
+ const { config, activeStrategy, runnerSpec, engineName } = resolveReflectRunner(options);
1467
+ // WI-9.10: RunContext, built only once config/runnerSpec exist so engine
1468
+ // resolution's existing error-priority ordering is undisturbed (see
1469
+ // buildReflectRunContext's docblock). D6: assetCtx is a fresh,
1470
+ // per-invocation memo — readRelatedLessons below is its genuine
1471
+ // content-read consumer.
1472
+ const ctx = buildReflectRunContext({ options, stash, config, runnerSpec });
1473
+ const assetCtx = ctx.withFreshAssetMemo();
1474
+ // 4. Build the shared prompt inputs — feedback, hints, lessons, rejected
1475
+ // proposals. These are stable across refinement iterations; only the
1476
+ // `priorDraft` field changes per-iteration (R-1 / #372).
1477
+ const feedback = readRecentFeedback(options.ref ? (options.itemRef ?? durableImproveRef(options.ref)) : undefined);
1478
+ const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
1479
+ const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(assetCtx, stash, options.ref, parsedRef, options.itemRef) : [];
1480
+ // Reflexion-style verbal-RL: inject rejected proposals so the agent avoids
1481
+ // reproducing proposals that have already been reviewed and refused.
1482
+ const rejectedProposals = readRejectedProposals(stash, options.ref);
1483
+ // Standards "rulebook" for this target — stash convention/meta facts; empty
1484
+ // when none fire.
1485
+ const standardsContext = resolveStandardsContext(options.ref, stash);
1486
+ // 5. Spawn the agent — with the optional Self-Refine loop (R-1 / #372),
1487
+ // extracted to {@link runReflectRefineIterations}.
1488
+ const agentEnv = options.eventSource === "improve" ? { AKM_EVENT_SOURCE: "improve" } : {};
1489
+ const sensitiveValues = collectDispatchSensitiveValues(runnerSpec, {
1490
+ ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
1491
+ ...(options.runAgentOptions ?? {}),
1492
+ });
904
1493
  // Track every draft file path we synthesize so cleanup can remove them on
905
1494
  // every return path (success and failure). Mirrors propose's unlink pattern
906
- // in `src/commands/propose.ts:215-226` but generalised to N refinement
1495
+ // in `src/commands/proposal/propose.ts` but generalised to N refinement
907
1496
  // iterations. Always called via {@link cleanupDrafts} below.
908
1497
  const draftPathsToCleanup = [];
909
- // Last iteration's draft path — read back if the agent wrote it.
910
- let lastDraftPath;
911
1498
  // Best-effort unlink: tolerate already-deleted files (we may have unlinked
912
1499
  // an intermediate iteration's draft) and unwritable paths. Never throws —
913
1500
  // the proposal result is the source of truth for the caller.
@@ -922,127 +1509,28 @@ export async function akmReflect(options = {}) {
922
1509
  }
923
1510
  }
924
1511
  };
925
- // `payload` is populated inside the try (either by reading the draft file
926
- // or parsing stdout JSON). Hoisted here so the post-try sections (R-3 ref
927
- // guard, quality gate, sanitizer, createProposal) can use it after the
928
- // drafts have been cleaned up.
1512
+ // `result` / `lastDraftPath` / `payload` are populated inside the try. Hoisted
1513
+ // here so the post-try sections (R-3 ref guard, sanitizer, quality gate,
1514
+ // createProposal) can use them after the drafts have been cleaned up.
1515
+ let result = {};
1516
+ let lastDraftPath;
929
1517
  let payload;
930
1518
  try {
931
- for (let iter = 0; iter < maxRefineIters; iter++) {
932
- // Synthesize a fresh tmp path per iteration so refinement passes never
933
- // clobber an earlier draft (and so reading back is unambiguous).
934
- const iterDraftPath = canRunnerWriteFile ? synthesizeReflectDraftPath(options.ref) : undefined;
935
- if (iterDraftPath) {
936
- draftPathsToCleanup.push(iterDraftPath);
937
- lastDraftPath = iterDraftPath;
938
- }
939
- const { prompt, maxOutputChars } = buildReflectPrompt({
940
- ...(options.ref ? { ref: options.ref } : {}),
941
- ...(parsedRef?.type ? { type: parsedRef.type } : {}),
942
- ...(parsedRef?.name ? { name: parsedRef.name } : {}),
943
- ...(assetContent !== undefined ? { assetContent } : {}),
944
- ...(feedback.length > 0 ? { feedback } : {}),
945
- ...(schemaHints.length > 0 ? { schemaHints } : {}),
946
- ...(relatedLessons.length > 0 ? { relatedLessons } : {}),
947
- ...(options.task ? { task: options.task } : {}),
948
- ...(standardsContext.trim() ? { standardsContext } : {}),
949
- ...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
950
- ...(rejectedProposals.length > 0 ? { rejectedProposals } : {}),
951
- // R-1: inject prior draft as self-critique target on iterations > 0
952
- ...(priorDraft !== undefined ? { priorDraft } : {}),
953
- // Issue A (#reflect-pipeline file-write contract): when the runner can
954
- // touch the filesystem, instruct the agent to write the proposal body
955
- // to a tmp file instead of inlining it in JSON. Avoids parse failures
956
- // on long bodies (e.g. knowledge:systems/KOKORO_USAGE_GUIDE 8.4KB).
957
- ...(iterDraftPath ? { draftFilePath: iterDraftPath } : {}),
958
- });
959
- // Convert char ceiling → token cap for the LLM path: divide by 3 chars/token
960
- // (conservative — most models are 3.5–4) and add 500-char overhead for the
961
- // JSON wrapper and frontmatter block that surround the body in the response.
962
- const maxTokensForLlm = maxOutputChars !== undefined ? Math.ceil((maxOutputChars + 500) / 3) : undefined;
963
- let iterResult;
964
- if (options.runAgentOptions?.spawn) {
965
- // Test seam: use raw runAgent with injected spawn so tests remain deterministic.
966
- const resolvedProfile = profile;
967
- if (!resolvedProfile) {
968
- throw new Error("internal: reflect test-seam path requires a resolved agent profile");
969
- }
970
- const runOptions = {
971
- stdio: "captured",
972
- parseOutput: "text",
973
- ...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
974
- ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
975
- ...(options.runAgentOptions ?? {}),
976
- };
977
- iterResult = await runAgent(resolvedProfile, prompt, runOptions);
978
- }
979
- else if (runnerSpec) {
980
- // v2: dispatch through the unified RunnerSpec seam (X3). The `agent` /
981
- // `sdk` arms route to the default profile runners; the `llm` arm is
982
- // reflect-specific (wraps `runReflectViaLlm` — its bespoke iteration
983
- // shape) so it is supplied as the `llm` handler.
984
- const runOptions = {
985
- stdio: "captured",
986
- parseOutput: "text",
987
- ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
988
- };
989
- iterResult = await executeRunner(runnerSpec, prompt ?? "", runOptions, {
990
- llm: async (spec) =>
991
- // LLM HTTP path — `draftFilePath` is accepted for type symmetry
992
- // (see `RunReflectViaLlmOptions.draftFilePath` docstring) but is
993
- // intentionally a no-op. The prompt builder above also did not
994
- // include the file-write contract for this kind, so the LLM is
995
- // still asked for JSON via stdout.
996
- runReflectViaLlm({
997
- prompt,
998
- connection: spec.connection,
999
- timeoutMs: spec.timeoutMs ?? (typeof resolvedTimeoutMs === "number" ? resolvedTimeoutMs : undefined),
1000
- priorDraft,
1001
- iteration: iter,
1002
- responseSchema: REFLECT_JSON_SCHEMA,
1003
- chat: options.chat,
1004
- ...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
1005
- }),
1006
- // The `agent` arm (and only the agent arm — preserving prior behavior)
1007
- // overlays `spec.timeoutMs` onto the base run options.
1008
- runAgent: (profile, p, opts) => runAgent(profile, p, {
1009
- ...opts,
1010
- ...(runnerSpec.timeoutMs !== undefined ? { timeoutMs: runnerSpec.timeoutMs } : {}),
1011
- }),
1012
- });
1013
- }
1014
- else {
1015
- // Production path (v1): dispatch directly to the appropriate runner.
1016
- // The fallback at the end of step 3 guarantees `profile` is set whenever
1017
- // `runnerSpec` is undefined, but TS can't prove that across the loop +
1018
- // await boundary — narrow into a const.
1019
- const resolvedProfile = profile;
1020
- if (!resolvedProfile) {
1021
- throw new Error("internal: reflect v1 dispatch reached without a resolved agent profile or runnerSpec");
1022
- }
1023
- const runOptions = {
1024
- stdio: "captured",
1025
- parseOutput: "text",
1026
- ...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
1027
- ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
1028
- };
1029
- iterResult = resolvedProfile.sdkMode
1030
- ? await runOpencodeSdk(resolvedProfile, prompt ?? "", runOptions)
1031
- : await runAgent(resolvedProfile, prompt, runOptions);
1032
- }
1033
- result = iterResult;
1034
- if (!iterResult.ok)
1035
- break; // surface failure after loop
1036
- // On success, extract the draft content for the next iteration.
1037
- // If the agent returns the same content as the prior draft, stop early
1038
- // (no-op refinement) to avoid wasting tokens on identical iterations.
1039
- if (iter < maxRefineIters - 1) {
1040
- const nextDraft = iterResult.stdout ?? "";
1041
- if (priorDraft !== undefined && nextDraft === priorDraft)
1042
- break;
1043
- priorDraft = nextDraft;
1044
- }
1045
- }
1519
+ const iterated = await runReflectRefineIterations({
1520
+ options,
1521
+ parsedRef,
1522
+ assetContent,
1523
+ feedback,
1524
+ schemaHints,
1525
+ relatedLessons,
1526
+ rejectedProposals,
1527
+ standardsContext,
1528
+ runnerSpec,
1529
+ agentEnv,
1530
+ draftPathsToCleanup,
1531
+ });
1532
+ result = iterated.result;
1533
+ lastDraftPath = iterated.lastDraftPath;
1046
1534
  const finalResult = result;
1047
1535
  if (!finalResult.ok) {
1048
1536
  // B3: ENOENT / not-found gives an actionable hint.
@@ -1051,102 +1539,30 @@ export async function akmReflect(options = {}) {
1051
1539
  ...(finalResult.exitCode !== undefined ? { exitCode: finalResult.exitCode } : {}),
1052
1540
  });
1053
1541
  return {
1054
- ...failureEnvelope(finalResult, options.ref),
1055
- error: enoentHintMessage(profile?.bin ?? resolvedProfileName),
1542
+ ...failureEnvelope(finalResult, options.ref, engineName),
1543
+ error: enoentHintMessage(runnerIsLlm(runnerSpec) ? engineName : runnerSpec.profile.bin),
1056
1544
  };
1057
1545
  }
1058
- const envelope = failureEnvelope(finalResult, options.ref);
1059
- emitReflectFailed(envelope.reason, "agent_crash", options.ref, {
1546
+ const envelope = failureEnvelope(finalResult, options.ref, engineName);
1547
+ emitReflectFailed(envelope.reason, envelope.reason === "parse_error" ? "parse_error" : "agent_crash", options.ref, {
1060
1548
  ...(envelope.exitCode !== null ? { exitCode: envelope.exitCode } : {}),
1549
+ ...(reflectLlmTelemetry(finalResult) ?? {}),
1061
1550
  });
1062
1551
  return envelope;
1063
1552
  }
1064
1553
  // Re-alias to `result` for the downstream code that references it.
1065
1554
  result = finalResult;
1066
- // 6. Resolve the proposal content.
1067
- //
1068
- // Path A (file-write contract — preferred for agent/sdk runners on long
1069
- // assets): the agent wrote the body to `lastDraftPath` and printed
1070
- // `DRAFT_WRITTEN` on stdout. Load the body from disk and synthesize a
1071
- // payload. The `EXCESSIVE_EXPANSION`/schema-shape gates downstream still
1072
- // apply — they validate content, not transport.
1073
- //
1074
- // Path B (legacy JSON stdout): the agent inlined the proposal body in
1075
- // JSON on stdout. Falls through to `parseAgentProposalPayload`. Also the
1076
- // path used by the LLM HTTP runner, which cannot honour file-write.
1077
- const draftFileExists = lastDraftPath !== undefined && fs.existsSync(lastDraftPath) && fs.statSync(lastDraftPath).size > 0;
1078
- const draftSignaled = stdoutSignalsDraftWritten(result.stdout);
1079
- if (draftSignaled && lastDraftPath && !draftFileExists) {
1080
- // Agent claimed to write the draft but the file is missing or empty.
1081
- // Surface as a parse_error rather than silently falling through — the
1082
- // alternative would be parsing the `DRAFT_WRITTEN` sentinel as JSON,
1083
- // which is guaranteed to fail with a confusing message.
1084
- emitReflectFailed("parse_error", "draft_missing", options.ref, {
1085
- ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1086
- });
1087
- return {
1088
- schemaVersion: 1,
1089
- ok: false,
1090
- reason: "parse_error",
1091
- error: `Agent emitted DRAFT_WRITTEN but draft file is missing or empty (${lastDraftPath}). The file-write contract failed; either the agent's file tools are broken or the path was unwritable.`,
1092
- ...(options.ref ? { ref: options.ref } : {}),
1093
- exitCode: result.exitCode,
1094
- stdout: result.stdout,
1095
- ...(result.stderr ? { stderr: result.stderr } : {}),
1096
- };
1097
- }
1098
- if (draftFileExists && lastDraftPath) {
1099
- // Happy path: agent wrote the body to disk. Use the ref the caller
1100
- // supplied (or a placeholder when omitted — the R-3 ref-mismatch guard
1101
- // below has no effect when there is no expected ref).
1102
- const fileContent = fs.readFileSync(lastDraftPath, "utf8");
1103
- // Phase 6A: file-write contract carries self-rated confidence on the
1104
- // `DRAFT_WRITTEN confidence=<n>` sentinel line. Extract it so the
1105
- // file-write path is on equal footing with the JSON-stdout path for
1106
- // auto-accept gating in `akm improve`.
1107
- const draftConfidence = extractDraftConfidence(result.stdout);
1108
- payload = {
1109
- ref: options.ref ?? "",
1110
- content: fileContent,
1111
- ...(draftConfidence !== undefined ? { confidence: draftConfidence } : {}),
1112
- };
1113
- // The agent followed the file-write contract — `payload.ref` mirrors the
1114
- // caller's expected ref, so the R-3 guard below cannot fire. The agent
1115
- // had no opportunity to retarget the proposal. If the ref was omitted
1116
- // entirely, downstream `createProposal` will reject the empty ref.
1117
- }
1118
- else {
1119
- try {
1120
- payload = parseAgentProposalPayload(result.stdout ?? "");
1121
- }
1122
- catch (err) {
1123
- const fallback = fallbackPayloadFromRawContent(result.stdout ?? "", options.ref, profile?.sdkMode ?? false);
1124
- if (fallback) {
1125
- payload = fallback;
1126
- }
1127
- else {
1128
- // Reclassify cooldown/skip messages that arrive as stdout text instead of
1129
- // valid proposal JSON. These are legitimate skip signals, not parse failures,
1130
- // and should not pollute reflectFailedActions or recentErrors injection.
1131
- const stdoutText = result.stdout ?? "";
1132
- const isCooldownSignal = isStructuredCooldownSignal(stdoutText);
1133
- const reason = isCooldownSignal ? "cooldown" : "parse_error";
1134
- emitReflectFailed(reason, isCooldownSignal ? "stdout_cooldown_signal" : "parse_error", options.ref, {
1135
- ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1136
- });
1137
- return {
1138
- schemaVersion: 1,
1139
- ok: false,
1140
- reason,
1141
- error: err instanceof Error ? err.message : String(err),
1142
- ...(options.ref ? { ref: options.ref } : {}),
1143
- exitCode: result.exitCode,
1144
- stdout: result.stdout,
1145
- ...(result.stderr ? { stderr: result.stderr } : {}),
1146
- };
1147
- }
1148
- }
1149
- }
1555
+ const resolved = resolveReflectPayload({
1556
+ result,
1557
+ lastDraftPath,
1558
+ sensitiveValues,
1559
+ options,
1560
+ engineName,
1561
+ emitReflectFailed,
1562
+ });
1563
+ if ("failure" in resolved)
1564
+ return resolved.failure;
1565
+ payload = resolved.payload;
1150
1566
  }
1151
1567
  finally {
1152
1568
  // Always remove tmp draft files — success, failure, or exception. Returns
@@ -1155,28 +1571,31 @@ export async function akmReflect(options = {}) {
1155
1571
  // draft paths.
1156
1572
  cleanupDrafts();
1157
1573
  }
1574
+ payload = { ...payload, content: redactSensitiveText(payload.content, sensitiveValues) };
1158
1575
  // 6b. Validate payload.ref === options.ref (R-3 / #366).
1159
1576
  // A hallucinating agent can silently retarget proposals to a different ref.
1160
- // This guard normalises both refs through parseAssetRef so origin-prefix
1161
- // differences do not cause false positives, then rejects mismatches.
1577
+ // Parse both current refs so an optional bundle prefix does not cause a false
1578
+ // positive, then reject genuine concept mismatches.
1162
1579
  // References: CRITIC (arXiv:2305.11738), CoVe (arXiv:2309.11495).
1163
1580
  if (options.ref) {
1164
1581
  try {
1165
- const expectedParsed = parseAssetRef(options.ref);
1166
- const actualParsed = parseAssetRef(payload.ref);
1582
+ const expectedParsed = parseRefInput(options.ref);
1583
+ const actualParsed = parseRefInput(payload.ref);
1167
1584
  // Compare type + name (drop origin — agent may omit origin prefix).
1168
1585
  if (expectedParsed.type !== actualParsed.type || expectedParsed.name !== actualParsed.name) {
1169
1586
  emitReflectFailed("parse_error", "ref_mismatch", options.ref, {
1170
1587
  expectedRef: options.ref,
1171
1588
  actualRef: payload.ref,
1172
1589
  ...(result.exitCode !== null ? { exitCode: result.exitCode } : {}),
1590
+ ...(reflectLlmTelemetry(result) ?? {}),
1173
1591
  });
1174
1592
  return {
1175
- schemaVersion: 1,
1593
+ schemaVersion: 2,
1176
1594
  ok: false,
1177
1595
  reason: "parse_error",
1178
1596
  error: `Agent retargeted proposal: expected ref "${options.ref}" but got "${payload.ref}". Proposal rejected to prevent silent ref hallucination.`,
1179
1597
  ref: options.ref,
1598
+ engine: engineName,
1180
1599
  exitCode: result.exitCode,
1181
1600
  stdout: result.stdout,
1182
1601
  ...(result.stderr ? { stderr: result.stderr } : {}),
@@ -1184,251 +1603,21 @@ export async function akmReflect(options = {}) {
1184
1603
  }
1185
1604
  }
1186
1605
  catch {
1187
- // parseAssetRef failure means the agent returned a malformed ref — already
1606
+ // parseRefInput failure means the agent returned a malformed ref — already
1188
1607
  // caught downstream by createProposal; allow it to surface naturally.
1189
1608
  }
1190
1609
  }
1191
- // 7. R-5 / #374: Apply the proposal quality gate when enabled.
1192
- // Mirrors the lesson quality gate on distill proposals. The gate uses
1193
- // `runLessonQualityJudge` from distill.ts and is gated behind either
1194
- // `profiles.improve.default.processes.reflect.qualityGate.enabled` or
1195
- // `profiles.improve.default.processes.distill.qualityGate.enabled` (the
1196
- // `lesson_quality_gate` flag name is the legacy alias still accepted by
1197
- // `isLlmFeatureEnabled`). Fail-CLOSED (07 P0-2): a judge error / no-LLM /
1198
- // parse failure rejects the proposal rather than passing it through.
1199
- // G-Eval (arXiv:2303.16634) — quality judgment before admission.
1200
- const runtimeConfig = options.config ??
1201
- (() => {
1202
- try {
1203
- return loadConfig();
1204
- }
1205
- catch {
1206
- return undefined;
1207
- }
1208
- })();
1209
- const chatFn = options.chat ?? chatCompletion;
1210
- const qualityGateEnabled = isLlmFeatureEnabled(runtimeConfig, "proposal_quality_gate") ||
1211
- isLlmFeatureEnabled(runtimeConfig, "lesson_quality_gate");
1212
- if (qualityGateEnabled && runtimeConfig) {
1213
- const assetContent = (() => {
1214
- if (!options.ref)
1215
- return null;
1216
- try {
1217
- const refParsed = parseAssetRef(options.ref);
1218
- const candidates = [
1219
- path.join(stash, `${refParsed.type}s`, `${refParsed.name}.md`),
1220
- path.join(stash, `${refParsed.type}s`, refParsed.name, "index.md"),
1221
- ];
1222
- for (const p of candidates) {
1223
- if (fs.existsSync(p))
1224
- return fs.readFileSync(p, "utf8");
1225
- }
1226
- return null;
1227
- }
1228
- catch {
1229
- return null;
1230
- }
1231
- })();
1232
- const judgeResult = await runLessonQualityJudge(runtimeConfig, payload.content, assetContent ?? "", chatFn);
1233
- if (!judgeResult.pass) {
1234
- // Quality gate rejected the proposal — surface as parse_error so the
1235
- // improve orchestrator can log it and move on without crashing.
1236
- appendEvent({
1237
- eventType: "reflect_completed",
1238
- ref: payload.ref,
1239
- metadata: {
1240
- source: "reflect",
1241
- qualityRejected: true,
1242
- qualityScore: judgeResult.score,
1243
- qualityReason: judgeResult.reason,
1244
- },
1245
- });
1246
- return {
1247
- schemaVersion: 1,
1248
- ok: false,
1249
- reason: "parse_error",
1250
- error: `Reflect proposal quality gate rejected: score=${judgeResult.score}, reason="${judgeResult.reason}"`,
1251
- ...(options.ref ? { ref: options.ref } : {}),
1252
- exitCode: result.exitCode,
1253
- };
1254
- }
1255
- }
1256
- // 7b. Reflect content-preservation rails:
1257
- // - Restore source frontmatter so reflect can never strip indexable
1258
- // fields (`description`, `when_to_use`, `tags`, ...).
1259
- // - Reset protected identity fields (`name`, `ref`, `id`, `slug`,
1260
- // `type`) the LLM tried to change.
1261
- // - Reject proposals that shrink/expand the body past safe ratios.
1262
- //
1263
- // See REFLECT_ALLOWED_TYPES / sanitizeReflectPayload for the underlying
1264
- // hypotheses + observed regressions (`8737ab63`, `26941510`, and the
1265
- // catastrophic-shrinkage cases from the May 2026 review).
1266
- const sanitizeOutcome = sanitizeReflectPayload({ content: payload.content, ...(payload.frontmatter ? { frontmatter: payload.frontmatter } : {}) }, assetContent, payload.ref);
1267
- if (sanitizeOutcome.reject) {
1268
- appendEvent({
1269
- eventType: "reflect_completed",
1270
- ref: payload.ref,
1271
- metadata: {
1272
- source: "reflect",
1273
- sanitized: true,
1274
- rejected: true,
1275
- rejectReason: sanitizeOutcome.reject.error,
1276
- ...(sanitizeOutcome.warnings.length > 0 ? { sanitizerWarnings: sanitizeOutcome.warnings } : {}),
1277
- },
1278
- });
1279
- return {
1280
- schemaVersion: 1,
1281
- ok: false,
1282
- reason: sanitizeOutcome.reject.reason,
1283
- error: sanitizeOutcome.reject.error,
1284
- ...(options.ref ? { ref: options.ref } : {}),
1285
- exitCode: result.exitCode,
1286
- };
1287
- }
1288
- payload = {
1289
- ...payload,
1290
- content: sanitizeOutcome.content,
1291
- ...(sanitizeOutcome.frontmatter ? { frontmatter: sanitizeOutcome.frontmatter } : {}),
1292
- };
1293
- // 7c. Noise gate (#580): never queue a proposal whose sanitized content is
1294
- // identical to the current asset (empty diff) or differs only cosmetically
1295
- // (whitespace reflow, code-fence language hints, YAML scalar re-folding).
1296
- // Pure deterministic text comparison — see `reflect-noise.ts`. Runs before
1297
- // the draftMode branch so self-consistency sampling never votes a no-op
1298
- // candidate into the queue either. Skipped when there is no source asset
1299
- // (new-asset proposals have nothing to diff against).
1300
- if (assetContent !== undefined) {
1301
- const changeKind = classifyReflectChange(assetContent, payload.content);
1302
- // 'low-value' is config-gated (#639). DEFAULT OFF — absent = byte-identical
1303
- // pre-#639 behaviour (low-value treated the same as substantive). Resolved
1304
- // by the caller from the ACTIVE improve profile's
1305
- // `processes.reflect.lowValueFilter.enabled` and passed via options, so the
1306
- // running profile (not a hardcoded `profiles.improve.default`) decides.
1307
- const lowValueFilterEnabled = options.lowValueFilter === true;
1308
- const isDeferred = changeKind === "noop" || changeKind === "cosmetic" || (changeKind === "low-value" && lowValueFilterEnabled);
1309
- if (isDeferred) {
1310
- const subreason = changeKind === "noop"
1311
- ? "reflect_skipped_noop"
1312
- : changeKind === "low-value"
1313
- ? "reflect_skipped_low_value"
1314
- : "reflect_skipped_cosmetic";
1315
- emitReflectFailed("no_change", subreason, options.ref, { changeKind });
1316
- return {
1317
- schemaVersion: 1,
1318
- ok: false,
1319
- reason: "no_change",
1320
- error: changeKind === "noop"
1321
- ? `Reflect skipped: proposed content for ${payload.ref} is identical to the current asset (empty diff); no proposal created.`
1322
- : changeKind === "low-value"
1323
- ? `Reflect skipped: proposed content for ${payload.ref} is a low-value prose micro-rewrite (few changed tokens, no structural changes); no proposal created.`
1324
- : `Reflect skipped: proposed content for ${payload.ref} is a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes); no proposal created.`,
1325
- ...(options.ref ? { ref: options.ref } : {}),
1326
- exitCode: result.exitCode,
1327
- };
1328
- }
1329
- }
1330
- // 8. Create the proposal. The proposal queue is the ONLY thing reflect
1331
- // writes — promotion to a real asset is gated by `akm proposal accept`.
1332
- //
1333
- // R-4 / #373: Stamp `derived_from_reflect: true` in the frontmatter of any
1334
- // lesson proposal generated by reflect. This provenance marker lets
1335
- // `readRelatedLessons` exclude echo-chamber lessons (lessons that originate
1336
- // from prior reflect runs on the same skill) unless independent feedback
1337
- // evidence exists. ExpeL arXiv:2308.10144 — reject rules without success/
1338
- // failure differential from independent evidence.
1339
- const isLessonProposal = (() => {
1340
- try {
1341
- return parseAssetRef(payload.ref).type === "lesson";
1342
- }
1343
- catch {
1344
- return false;
1345
- }
1346
- })();
1347
- const basePayloadFrontmatter = payload.frontmatter ?? {};
1348
- const payloadFrontmatterWithProvenance = isLessonProposal
1349
- ? { ...basePayloadFrontmatter, derived_from_reflect: true }
1350
- : basePayloadFrontmatter;
1351
- // Draft mode: skip DB persistence — the SC sampling loop in improve.ts persists
1352
- // only the majority-vote winner (R-2 / #389). Return a synthetic proposal so
1353
- // pickMajorityVote can compare content via Jaccard similarity.
1354
- if (options.draftMode) {
1355
- const draftProposal = {
1356
- id: `sc-draft-${Date.now()}`,
1357
- ref: payload.ref,
1358
- source: "reflect",
1359
- status: "pending",
1360
- createdAt: new Date().toISOString(),
1361
- updatedAt: new Date().toISOString(),
1362
- payload: {
1363
- content: payload.content,
1364
- ...(Object.keys(payloadFrontmatterWithProvenance).length > 0
1365
- ? { frontmatter: payloadFrontmatterWithProvenance }
1366
- : {}),
1367
- },
1368
- // Phase 6A: preserve confidence on the synthetic draft so the SC majority
1369
- // winner carries the score through to the persisted proposal.
1370
- ...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
1371
- };
1372
- return {
1373
- schemaVersion: 1,
1374
- ok: true,
1375
- proposal: draftProposal,
1376
- ref: draftProposal.ref,
1377
- agentProfile: resolvedProfileName,
1378
- durationMs: result.durationMs,
1379
- };
1380
- }
1381
- const createInput = {
1382
- ref: payload.ref,
1383
- source: "reflect",
1384
- sourceRun: `reflect-${Date.now()}`,
1385
- payload: {
1386
- content: payload.content,
1387
- ...(Object.keys(payloadFrontmatterWithProvenance).length > 0
1388
- ? { frontmatter: payloadFrontmatterWithProvenance }
1389
- : {}),
1390
- },
1391
- // Phase 6A: forward LLM-reported confidence into the proposal record.
1392
- // `parseAgentProposalPayload` already clamps to [0, 1] and drops non-
1393
- // finite values; `createProposal` runs its own sanitizer as a safety net.
1394
- ...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
1395
- // Attribution tagging: persist the eligibility lane on the proposal so it
1396
- // survives to accept/reject/revert time even across runs. See EligibilitySource.
1397
- ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
1398
- };
1399
- const proposalResult = createProposal(stash, createInput, options.ctx);
1400
- if (isProposalSkipped(proposalResult)) {
1401
- // Dedup/cooldown guard fired — surface as a "cooldown" reason (not "parse_error")
1402
- // so the improve orchestrator can distinguish legitimate skips from real failures
1403
- // and exclude them from recentErrors/avoidPatterns injection.
1404
- emitReflectFailed("cooldown", "proposal_skipped", options.ref, {
1405
- proposalSkipReason: proposalResult.reason,
1406
- });
1407
- return {
1408
- schemaVersion: 1,
1409
- ok: false,
1410
- reason: "cooldown",
1411
- error: `Proposal skipped (${proposalResult.reason}): ${proposalResult.message}`,
1412
- ...(options.ref ? { ref: options.ref } : {}),
1413
- exitCode: null,
1414
- };
1415
- }
1416
- const proposal = proposalResult;
1417
- appendEvent({
1418
- eventType: "reflect_completed",
1419
- ref: proposal.ref,
1420
- metadata: {
1421
- proposalId: proposal.id,
1422
- source: "reflect",
1423
- agentProfile: resolvedProfileName,
1424
- },
1610
+ return finalizeReflectProposal({
1611
+ payload,
1612
+ assetContent,
1613
+ result,
1614
+ options,
1615
+ engineName,
1616
+ config,
1617
+ activeStrategy,
1618
+ runnerSpec,
1619
+ feedback,
1620
+ stash,
1621
+ emitReflectFailed,
1425
1622
  });
1426
- return {
1427
- schemaVersion: 1,
1428
- ok: true,
1429
- proposal,
1430
- ref: proposal.ref,
1431
- agentProfile: resolvedProfileName,
1432
- durationMs: result.durationMs,
1433
- };
1434
1623
  }