akm-cli 0.9.25-alpha.1 → 0.9.25-alpha.3

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 (68) hide show
  1. package/CHANGELOG.md +243 -280
  2. package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
  3. package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -9
  4. package/dist/assets/prompts/reflect-llm-schema-contract.md +1 -3
  5. package/dist/assets/prompts/reflect-output-repair.md +1 -1
  6. package/dist/cli.js +1 -1
  7. package/dist/commands/improve/consolidate/pair-pass.js +1 -0
  8. package/dist/commands/improve/consolidate.js +7 -2
  9. package/dist/commands/improve/execution.js +2 -3
  10. package/dist/commands/improve/extract-cli.js +3 -2
  11. package/dist/commands/improve/extract.js +2 -1
  12. package/dist/commands/improve/improve-cli.js +33 -1
  13. package/dist/commands/improve/loop-stages.js +3 -0
  14. package/dist/commands/improve/reflect-noise.js +125 -0
  15. package/dist/commands/improve/reflect.js +150 -333
  16. package/dist/commands/improve/retrieval-gate.js +7 -2
  17. package/dist/commands/improve/session-asset.js +6 -0
  18. package/dist/commands/improve/stage.js +31 -39
  19. package/dist/commands/proposal/drain.js +4 -7
  20. package/dist/commands/proposal/propose.js +2 -11
  21. package/dist/commands/proposal/validators/proposal-quality-validators.js +11 -5
  22. package/dist/commands/proposal/validators/proposal-validators.js +4 -5
  23. package/dist/commands/read/search-cli.js +0 -38
  24. package/dist/core/asset/asset-serialize.js +1 -1
  25. package/dist/core/config/schema/engines.js +15 -33
  26. package/dist/core/config/schema/improve-processes.js +16 -0
  27. package/dist/core/content-safety.js +0 -24
  28. package/dist/core/redaction.js +4 -0
  29. package/dist/core/spawn-env.js +25 -0
  30. package/dist/core/structured.js +1 -1
  31. package/dist/execution/source.js +8 -12
  32. package/dist/integrations/agent/config.js +1 -3
  33. package/dist/integrations/agent/engine-resolution.js +0 -3
  34. package/dist/integrations/agent/execution.js +14 -13
  35. package/dist/integrations/agent/index.js +1 -1
  36. package/dist/integrations/agent/model-map.js +15 -16
  37. package/dist/integrations/agent/profiles.js +2 -2
  38. package/dist/integrations/agent/prompts.js +51 -127
  39. package/dist/integrations/agent/request-lowering.js +9 -7
  40. package/dist/integrations/agent/runner-dispatch.js +25 -31
  41. package/dist/integrations/harnesses/aider/agent-builder.js +1 -2
  42. package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -2
  43. package/dist/integrations/harnesses/claude/agent-builder.js +4 -16
  44. package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
  45. package/dist/integrations/harnesses/codex/index.js +6 -11
  46. package/dist/integrations/harnesses/codex/session-log.js +211 -0
  47. package/dist/integrations/harnesses/ids.js +10 -16
  48. package/dist/integrations/harnesses/opencode/agent-builder.js +14 -24
  49. package/dist/integrations/harnesses/opencode/model-config.js +15 -62
  50. package/dist/integrations/harnesses/opencode/model-work-agent.js +71 -36
  51. package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -6
  52. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +32 -90
  53. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -2
  54. package/dist/integrations/harnesses/pi/agent-builder.js +1 -2
  55. package/dist/integrations/harnesses/types.js +3 -3
  56. package/dist/llm/feature-gate.js +2 -5
  57. package/dist/llm/index-passes.js +2 -2
  58. package/dist/llm/structured-call.js +5 -5
  59. package/dist/output/shapes/passthrough.js +1 -0
  60. package/dist/scripts/akm-migrate-node.js +381 -239
  61. package/dist/scripts/akm-migrate.js +381 -239
  62. package/dist/workflows/exec/unit-dispatch.js +4 -13
  63. package/docs/reference/cli.md +29 -16
  64. package/docs/reference/configuration.md +79 -85
  65. package/docs/reference/data-and-telemetry.md +2 -3
  66. package/docs/reference/workflow-schema.md +6 -9
  67. package/package.json +1 -1
  68. package/schemas/akm-config.json +108 -36
@@ -2,23 +2,24 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * `akm reflect [ref]` — ask an engine for a revised asset and queue it as a
6
- * proposal (`source: "reflect"`). Reflect never writes an asset: the proposal
7
- * queue is the only path, `akm proposal accept` the bridge.
5
+ * `akm reflect [ref]` — ask an engine whether an asset's `description`,
6
+ * `when_to_use` and title need a fix, and queue the asset with that fix as a
7
+ * proposal (`source: "reflect"`). The engine never writes the body: akm keeps
8
+ * it byte for byte. Reflect never writes an asset: the proposal queue is the
9
+ * only path, `akm proposal accept` the bridge.
8
10
  *
9
11
  * Every invocation closes with one `reflect_completed` event; `reflect_invoked`
10
12
  * is emitted once the dispatch has validated its credentials (deterministic
11
13
  * pre-dispatch refusals still emit both).
12
14
  */
13
15
  import fs from "node:fs";
14
- import path from "node:path";
15
- import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
16
+ import { serializeFrontmatter } from "../../core/asset/asset-serialize.js";
16
17
  import { parseFrontmatter } from "../../core/asset/frontmatter.js";
17
- import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-ref.js";
18
+ import { parseRefInput } from "../../core/asset/resolve-ref.js";
18
19
  import { DESCRIPTION_MAX_CHARS, requiresDescription } from "../../core/authoring-rules.js";
19
20
  import { resolveStashDir } from "../../core/common.js";
20
21
  import { loadConfig } from "../../core/config/config.js";
21
- import { generatedContentRejection, stripReflectPromptScaffolding } from "../../core/content-safety.js";
22
+ import { generatedContentRejection } from "../../core/content-safety.js";
22
23
  import { ConfigError, UsageError } from "../../core/errors.js";
23
24
  import { appendEvent, readEvents } from "../../core/events.js";
24
25
  import { lintLessonContent } from "../../core/lesson-lint.js";
@@ -26,23 +27,21 @@ import { parseEmbeddedJsonResponse } from "../../core/parse.js";
26
27
  import { redactSensitiveText } from "../../core/redaction.js";
27
28
  import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
28
29
  import { warn, warnOnce } from "../../core/warn.js";
29
- import { MODEL_WORK_TOOLS } from "../../execution/source.js";
30
30
  import { lookup } from "../../indexer/indexer.js";
31
- import { DEFAULT_MODEL_WORK_TIMEOUT_MS } from "../../integrations/agent/config.js";
31
+ import { DEFAULT_LLM_TIMEOUT_MS } from "../../integrations/agent/config.js";
32
32
  import { fallbackAnnouncement, NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, withEngineFallback, } from "../../integrations/agent/engine-fallback.js";
33
33
  import { buildExecution, resolveExecution } from "../../integrations/agent/execution.js";
34
- import { buildReflectOutputRepairPrompt, buildReflectPrompt, parseAgentProposalPayload, REFLECT_CONTENT_CAP, REFLECT_TRUNCATION_MARKER, } from "../../integrations/agent/prompts.js";
34
+ import { buildReflectOutputRepairPrompt, buildReflectPrompt, REFLECT_CONTENT_CAP, } from "../../integrations/agent/prompts.js";
35
35
  import { runnerIsLlm } from "../../integrations/agent/runner.js";
36
36
  import { assertRunnerCredentials, collectDispatchSensitiveValues, } from "../../integrations/agent/runner-dispatch.js";
37
37
  import { isJsonSchemaKnownUnsupported, LlmCallError } from "../../llm/client.js";
38
38
  import { baseFailureFields, enoentHintMessage, isEnoentFailure } from "../agent/agent-support.js";
39
- import { checkReflectSize, isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
39
+ import { isValidDescription } from "../proposal/validators/proposal-quality-validators.js";
40
40
  import { CHARS_PER_TOKEN, DEFAULT_CONTEXT_LENGTH_TOKENS } from "./consolidate/chunking.js";
41
- import { deriveLessonRef } from "./distill.js";
42
41
  import { findAssetFilePath } from "./eligibility.js";
43
42
  import { resolveImproveExecution } from "./execution.js";
44
43
  import { recordLedgerAttempt } from "./ledger.js";
45
- import { classifyReflectChange, splitFrontmatter } from "./reflect-noise.js";
44
+ import { classifyReflectChange, findReflectDefect, splitFrontmatter } from "./reflect-noise.js";
46
45
  import { loadRetrievalQueries, runRetrievalRegressionGate } from "./retrieval-gate.js";
47
46
  import { callStageOnce, errMessage, mintProposal, noticeSet, rejectedProposalContext, resolveQualityGateJudge, runReflectQualityJudge, } from "./stage.js";
48
47
  const MAX_FEEDBACK_LINES = 10;
@@ -67,7 +66,7 @@ function readRecentFeedback(ref, eventsCtx) {
67
66
  }
68
67
  }
69
68
  /**
70
- * Types reflect may rewrite: its output is frontmatter + markdown, which would
69
+ * Types reflect may patch: its output is frontmatter + markdown, which would
71
70
  * break a script or env file. Another type is allowed only when its current
72
71
  * content already has that shape; secrets are never read.
73
72
  */
@@ -81,99 +80,12 @@ export const REFLECT_ALLOWED_TYPES = new Set([
81
80
  "workflow",
82
81
  ]);
83
82
  const REFLECT_REFUSED_TYPES = new Set(["secret"]);
84
- /** Identity fields the model may never change (a renamed `name` breaks ref resolution). */
85
- const PROTECTED_FRONTMATTER_FIELDS = new Set(["name", "ref", "id", "slug", "type"]);
86
83
  /** Lesson lint findings for the prompt: a concrete starting point for the revision. */
87
84
  function buildSchemaHints(type, content) {
88
85
  if (!content || type !== "lesson")
89
86
  return [];
90
87
  return lintLessonContent(content, "reflect").findings.map((f) => `[${f.kind}] ${f.message}`);
91
88
  }
92
- /**
93
- * Lessons related to a skill: its derived lesson, lessons distilled from it,
94
- * and lessons citing it in `sources`. Without independent feedback on the skill,
95
- * lessons reflect itself produced are dropped so its own output is not fed
96
- * back as evidence.
97
- */
98
- async function readRelatedLessons(stash, ref, parsedRef, itemRef, eventsCtx) {
99
- if (parsedRef.type !== "skill")
100
- return [];
101
- const cache = new Map();
102
- const read = (filePath) => {
103
- const key = path.resolve(filePath);
104
- const cached = cache.get(key) ?? fs.readFileSync(filePath, "utf8");
105
- cache.set(key, cached);
106
- return cached;
107
- };
108
- const related = new Map();
109
- const derivedLessonRef = deriveLessonRef(ref);
110
- const candidateRefs = new Set([derivedLessonRef]);
111
- const derivedLessonPath = path.join(stash, "lessons", `${parseRefInput(derivedLessonRef).name}.md`);
112
- if (fs.existsSync(derivedLessonPath)) {
113
- related.set(derivedLessonRef, { ref: derivedLessonRef, content: read(derivedLessonPath) });
114
- }
115
- try {
116
- const keys = new Set([itemRef ?? ref]);
117
- for (const event of readEvents({ type: "distill_invoked" }, readOnlyEventsContext(eventsCtx)).events) {
118
- if (event.ref === undefined || !keys.has(event.ref))
119
- continue;
120
- const proposalRef = typeof event.metadata?.proposalRef === "string" ? event.metadata.proposalRef : undefined;
121
- if (proposalRef && lenientRefType(proposalRef) === "lesson")
122
- candidateRefs.add(proposalRef);
123
- }
124
- }
125
- catch {
126
- // best-effort
127
- }
128
- for (const candidateRef of candidateRefs) {
129
- try {
130
- const filePath = await findAssetFilePath(candidateRef, stash);
131
- if (filePath && fs.existsSync(filePath))
132
- related.set(candidateRef, { ref: candidateRef, content: read(filePath) });
133
- }
134
- catch {
135
- // An index miss is not fatal.
136
- }
137
- }
138
- try {
139
- const lessonsDir = path.join(stash, "lessons");
140
- if (fs.existsSync(lessonsDir)) {
141
- for (const fileName of fs.readdirSync(lessonsDir)) {
142
- if (!fileName.endsWith(".md"))
143
- continue;
144
- const content = read(path.join(lessonsDir, fileName));
145
- const sources = parseFrontmatter(content).data.sources;
146
- if (!Array.isArray(sources) || !sources.some((s) => typeof s === "string" && s.trim() === ref))
147
- continue;
148
- const lessonRef = conceptIdFromTypeName("lesson", fileName.slice(0, -3));
149
- if (!related.has(lessonRef))
150
- related.set(lessonRef, { ref: lessonRef, content });
151
- }
152
- }
153
- }
154
- catch {
155
- // best-effort
156
- }
157
- let hasIndependentFeedback = true;
158
- try {
159
- hasIndependentFeedback = readEvents({ type: "feedback", ref }, readOnlyEventsContext(eventsCtx)).events.length > 0;
160
- }
161
- catch {
162
- // Unknown: keep every lesson.
163
- }
164
- if (!hasIndependentFeedback) {
165
- for (const [lessonRef, lesson] of related) {
166
- try {
167
- if (parseFrontmatter(lesson.content).data.derived_from_reflect === true)
168
- related.delete(lessonRef);
169
- }
170
- catch {
171
- // Unparseable frontmatter: keep it.
172
- }
173
- }
174
- }
175
- return [...related.values()];
176
- }
177
89
  /** The asset type of a maybe-ref, or `""` when it does not parse. */
178
90
  function lenientRefType(ref) {
179
91
  if (!ref)
@@ -185,35 +97,21 @@ function lenientRefType(ref) {
185
97
  return "";
186
98
  }
187
99
  }
188
- /**
189
- * Cut a duplicate frontmatter block the model appended after its rewrite.
190
- * Requires a balanced fence AND `key:` lines so thematic breaks survive.
191
- */
192
- function stripAppendedFrontmatter(body) {
193
- const match = body.match(/\n---\r?\n([\s\S]*?)\n---\r?\n/);
194
- if (!match || !/^\w[\w-]*:/m.test(match[1]))
195
- return body;
196
- return body.slice(0, body.indexOf(match[0])).replace(/\s+$/, "");
197
- }
198
100
  /**
199
101
  * A description derived from existing metadata (title, first heading, first
200
102
  * prose sentence) that passes `isValidDescription`, or `undefined`. Never
201
103
  * free-form invention.
202
104
  */
203
- function deriveDescriptionFromAsset(title, proposedBody, sourceBody, targetRef) {
105
+ function deriveDescriptionFromAsset(title, body, targetRef) {
204
106
  const candidates = [];
205
107
  if (typeof title === "string" && title.trim())
206
108
  candidates.push({ text: title.trim(), kind: "fragment" });
207
- for (const body of [proposedBody, sourceBody]) {
208
- const heading = body.match(/^#{1,6}\s+(.+?)\s*$/m)?.[1];
209
- if (heading)
210
- candidates.push({ text: heading.trim(), kind: "fragment" });
211
- }
212
- for (const body of [proposedBody, sourceBody]) {
213
- const sentence = firstProseSentence(body);
214
- if (sentence)
215
- candidates.push({ text: sentence, kind: "prose" });
216
- }
109
+ const heading = body.match(/^#{1,6}\s+(.+?)\s*$/m)?.[1];
110
+ if (heading)
111
+ candidates.push({ text: heading.trim(), kind: "fragment" });
112
+ const sentence = firstProseSentence(body);
113
+ if (sentence)
114
+ candidates.push({ text: sentence, kind: "prose" });
217
115
  for (const { text, kind } of candidates) {
218
116
  const normalized = text
219
117
  .replace(/`/g, "")
@@ -242,81 +140,71 @@ function firstProseSentence(body) {
242
140
  return "";
243
141
  }
244
142
  /**
245
- * Reflect's content rails: the source frontmatter is restored and the model's
246
- * frontmatter merged on top except identity fields; a stray or appended
247
- * frontmatter block and echoed run-only guidance are stripped; a missing
248
- * required description is derived deterministically; a body outside the size
249
- * ratios or echoing the truncation notice is flagged for review.
143
+ * The asset with the patch applied, or `undefined` when the patch changes
144
+ * nothing (or there is no asset to patch). The body is the source's own, byte
145
+ * for byte; the one thing akm adds to it is a `# title` heading it lacks. A
146
+ * required description that neither the source nor the patch has is derived
147
+ * from the asset's own text (#636). Only the changed keys' frontmatter lines are
148
+ * rewritten: every other line is kept as it is, so a source whose YAML the
149
+ * parser reads only in part (a broken description beside a list) loses nothing.
250
150
  */
251
- export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
252
- const warnings = [];
253
- const { fmText: sourceFmText, body: sourceBody } = sourceContent
254
- ? splitFrontmatter(sourceContent)
255
- : { fmText: null, body: "" };
256
- const sourceFm = sourceFmText !== null ? parseFrontmatter(sourceContent ?? "").data : {};
257
- const { fmText: llmFmText, body: rawLlmBody } = splitFrontmatter(payload.content);
258
- let llmFm = {};
259
- if (llmFmText !== null) {
260
- warnings.push("LLM emitted frontmatter in content; stripped and merged through identity guard.");
261
- try {
262
- llmFm = parseFrontmatter(payload.content).data;
263
- }
264
- catch {
265
- llmFm = {};
266
- }
267
- }
268
- if (payload.frontmatter && typeof payload.frontmatter === "object")
269
- llmFm = { ...llmFm, ...payload.frontmatter };
270
- for (const field of PROTECTED_FRONTMATTER_FIELDS) {
271
- if (field in llmFm && llmFm[field] !== sourceFm[field]) {
272
- warnings.push(`LLM attempted to change protected frontmatter field "${field}"; restored from source.`);
273
- delete llmFm[field];
274
- }
275
- }
276
- const mergedFm = { ...sourceFm, ...llmFm };
277
- for (const field of PROTECTED_FRONTMATTER_FIELDS)
278
- if (field in sourceFm)
279
- mergedFm[field] = sourceFm[field];
280
- const scaffolding = stripReflectPromptScaffolding(stripAppendedFrontmatter(rawLlmBody.replace(/^\s+/, "")));
281
- const cleanedBody = scaffolding.content;
282
- if (scaffolding.stripped) {
283
- warnings.push('Removed echoed run-only "Avoid These Patterns" guidance from the proposed asset body (#963).');
284
- }
151
+ export function applyReflectPatch(patch, sourceContent, targetRef) {
152
+ if (!sourceContent.trim())
153
+ return undefined;
154
+ const { fmText, body: sourceBody } = splitFrontmatter(sourceContent);
155
+ // A fence that is opened and never closed (`---` fused onto the last value)
156
+ // would leave the old block in the body under a new one: a person fixes it.
157
+ if (fmText === null && /^---\r?\n/.test(sourceContent))
158
+ return undefined;
159
+ const sourceFm = fmText !== null ? parseFrontmatter(sourceContent).data : {};
160
+ const { title, ...changes } = patch;
161
+ const addTitle = title !== undefined && !/^#[ \t]+\S/m.test(sourceBody);
162
+ const body = addTitle ? `# ${title}\n\n${sourceBody.replace(/^(\r?\n)+/, "")}` : sourceBody;
285
163
  // Only a source that already has frontmatter but no description gets one:
286
164
  // injecting a whole block, or overwriting an authored one, is out of scope.
287
165
  const refType = lenientRefType(targetRef);
288
- const desc = mergedFm.description;
289
- const sourceHadFrontmatter = sourceFmText !== null && Object.keys(sourceFm).length > 0;
166
+ const desc = changes.description ?? sourceFm.description;
290
167
  if (refType &&
291
168
  requiresDescription(refType) &&
292
169
  (typeof desc !== "string" || desc.trim().length === 0) &&
293
- sourceHadFrontmatter) {
294
- const derived = deriveDescriptionFromAsset(mergedFm.title, cleanedBody, sourceBody, targetRef);
295
- if (derived) {
296
- mergedFm.description = derived;
297
- warnings.push("Synthesized a deterministic `description` from title/heading (#636) — source and proposal lacked one.");
170
+ Object.keys(sourceFm).length > 0) {
171
+ const derived = deriveDescriptionFromAsset(sourceFm.title, body, targetRef);
172
+ if (derived)
173
+ changes.description = derived;
174
+ }
175
+ for (const key of Object.keys(changes))
176
+ if (changes[key] === sourceFm[key])
177
+ delete changes[key];
178
+ if (!addTitle && Object.keys(changes).length === 0)
179
+ return undefined;
180
+ // No frontmatter at all stays body-only unless the patch adds a field. The
181
+ // blank line after a closing fence is part of the source body, so only a
182
+ // block or heading akm adds brings its own.
183
+ if (fmText === null) {
184
+ if (Object.keys(changes).length === 0)
185
+ return { content: body };
186
+ const content = `---\n${serializeFrontmatter(changes)}\n---\n\n${body}`;
187
+ return { content, frontmatter: parseFrontmatter(content).data };
188
+ }
189
+ const content = `---\n${patchFrontmatterLines(fmText, changes)}\n---\n${addTitle ? "\n" : ""}${body}`;
190
+ return { content, frontmatter: parseFrontmatter(content).data };
191
+ }
192
+ /** The frontmatter text with each changed key's lines (the key line and its indented continuation) replaced, or appended. */
193
+ function patchFrontmatterLines(fmText, changes) {
194
+ const lines = fmText.split(/\r?\n/);
195
+ for (const [key, value] of Object.entries(changes)) {
196
+ const replacement = serializeFrontmatter({ [key]: value }).split("\n");
197
+ const start = lines.findIndex((line) => line.startsWith(`${key}:`));
198
+ if (start === -1) {
199
+ lines.push(...replacement);
200
+ continue;
298
201
  }
202
+ let end = start + 1;
203
+ while (end < lines.length && /^[ \t]/.test(lines[end] ?? ""))
204
+ end++;
205
+ lines.splice(start, end - start, ...replacement);
299
206
  }
300
- const size = checkReflectSize(sourceBody, cleanedBody);
301
- let sizeGuardRatio;
302
- if (!size.ok) {
303
- const shrink = size.code === "EXCESSIVE_SHRINKAGE";
304
- warnings.push(`${size.code} — proposed body is ${(size.ratio * 100).toFixed(0)}% of source (${shrink ? "minimum 50%" : "maximum 250%"}) for ref ${targetRef}. ${shrink ? "Concrete content was likely deleted." : "Speculative material was likely added."} Flagged for review.`);
305
- sizeGuardRatio = { code: size.code, ratio: size.ratio };
306
- }
307
- const truncationMarkerLeaked = cleanedBody.includes(REFLECT_TRUNCATION_MARKER);
308
- if (truncationMarkerLeaked) {
309
- warnings.push(`Proposed body for ref ${targetRef} contains the truncation-notice text the model was shown for a capped source asset ("${REFLECT_TRUNCATION_MARKER}"). The model likely echoed the notice instead of writing real content. Flagged for review.`);
310
- }
311
- // No frontmatter at all stays body-only, never gaining a stray `---`.
312
- const hasFrontmatter = Object.keys(mergedFm).length > 0;
313
- return {
314
- content: hasFrontmatter ? assembleAssetFromString(serializeFrontmatter(mergedFm), cleanedBody) : cleanedBody,
315
- ...(hasFrontmatter ? { frontmatter: mergedFm } : {}),
316
- warnings,
317
- ...(sizeGuardRatio ? { sizeGuardRatio } : {}),
318
- ...(truncationMarkerLeaked ? { truncationMarkerLeaked } : {}),
319
- };
207
+ return lines.join("\n");
320
208
  }
321
209
  // ── Output contract ──────────────────────────────────────────────────────────
322
210
  //
@@ -326,11 +214,12 @@ export function sanitizeReflectPayload(payload, sourceContent, targetRef) {
326
214
  // repaired once, whatever engine produced it.
327
215
  const REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA = {
328
216
  type: "object",
329
- required: ["description", "when_to_use"],
217
+ required: ["description", "when_to_use", "title"],
330
218
  additionalProperties: false,
331
219
  properties: {
332
220
  description: { type: ["string", "null"] },
333
221
  when_to_use: { type: ["string", "null"] },
222
+ title: { type: ["string", "null"] },
334
223
  },
335
224
  };
336
225
  const REFLECT_CONFIDENCE_SCHEMA = {
@@ -341,21 +230,19 @@ const REFLECT_CONFIDENCE_SCHEMA = {
341
230
  };
342
231
  export const REFLECT_JSON_SCHEMA = {
343
232
  type: "object",
344
- required: ["content", "confidence", "frontmatterPatch"],
233
+ required: ["confidence", "frontmatterPatch"],
345
234
  additionalProperties: false,
346
235
  properties: {
347
- content: { type: "string", description: "Complete improved markdown body without YAML frontmatter." },
348
236
  confidence: REFLECT_CONFIDENCE_SCHEMA,
349
237
  frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
350
238
  },
351
239
  };
352
240
  const REFLECT_UNSCOPED_JSON_SCHEMA = {
353
241
  type: "object",
354
- required: ["ref", "content", "confidence", "frontmatterPatch"],
242
+ required: ["ref", "confidence", "frontmatterPatch"],
355
243
  additionalProperties: false,
356
244
  properties: {
357
245
  ref: { type: "string", description: "Selected asset ref as a subdir-qualified conceptId." },
358
- content: { type: "string", description: "Complete improved markdown body without YAML frontmatter." },
359
246
  confidence: { type: "number", minimum: 0, maximum: 1, description: "Self-reported quality confidence in [0, 1]." },
360
247
  frontmatterPatch: REFLECT_FRONTMATTER_PATCH_JSON_SCHEMA,
361
248
  },
@@ -388,67 +275,48 @@ function parseReflectConfidence(value) {
388
275
  }
389
276
  return value;
390
277
  }
278
+ const REFLECT_PATCH_FIELDS = ["description", "when_to_use", "title"];
391
279
  function parseReflectFrontmatterPatch(value) {
392
280
  if (!value || typeof value !== "object" || Array.isArray(value)) {
393
281
  throw new Error('direct reflect response missing required object field "frontmatterPatch"');
394
282
  }
395
- const patch = value;
396
- const keys = Object.keys(patch).sort();
397
- if (keys.length !== 2 || keys[0] !== "description" || keys[1] !== "when_to_use") {
398
- throw new Error("direct reflect frontmatterPatch fields must be exactly: description, when_to_use");
283
+ const fields = value;
284
+ if (Object.keys(fields).length !== REFLECT_PATCH_FIELDS.length || REFLECT_PATCH_FIELDS.some((f) => !(f in fields))) {
285
+ throw new Error(`direct reflect frontmatterPatch fields must be exactly: ${REFLECT_PATCH_FIELDS.join(", ")}`);
399
286
  }
400
- const frontmatter = {};
401
- for (const field of ["description", "when_to_use"]) {
402
- const fieldValue = patch[field];
287
+ const patch = {};
288
+ for (const field of REFLECT_PATCH_FIELDS) {
289
+ const fieldValue = fields[field];
403
290
  if (fieldValue === null)
404
291
  continue;
405
292
  if (typeof fieldValue !== "string" || !fieldValue.trim() || /[\r\n]/.test(fieldValue)) {
406
293
  throw new Error(`direct reflect frontmatterPatch.${field} must be a non-empty single-line string or null`);
407
294
  }
408
- frontmatter[field] = fieldValue.trim();
295
+ patch[field] = fieldValue.trim();
409
296
  }
410
- return Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
297
+ return patch;
411
298
  }
412
299
  function parseSchemaReflectOutput(raw, targetRef) {
413
300
  const parsed = parseEmbeddedJsonResponse(raw);
414
301
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
415
302
  throw new Error("direct reflect response was not valid JSON");
416
303
  }
417
- const expectedKeys = targetRef
418
- ? ["confidence", "content", "frontmatterPatch"]
419
- : ["confidence", "content", "frontmatterPatch", "ref"];
304
+ const expectedKeys = targetRef ? ["confidence", "frontmatterPatch"] : ["confidence", "frontmatterPatch", "ref"];
420
305
  const actualKeys = Object.keys(parsed).sort();
421
306
  if (actualKeys.length !== expectedKeys.length || actualKeys.some((key, index) => key !== expectedKeys[index])) {
422
307
  throw new Error(`direct reflect response fields must be exactly: ${expectedKeys.join(", ")}`);
423
308
  }
424
- if (typeof parsed.content !== "string" || !parsed.content.trim()) {
425
- throw new Error('direct reflect response missing required string field "content"');
426
- }
427
309
  const ref = targetRef ?? (typeof parsed.ref === "string" ? parsed.ref.trim() : "");
428
310
  if (!ref)
429
311
  throw new Error('direct reflect response missing required string field "ref"');
430
- const frontmatter = parseReflectFrontmatterPatch(parsed.frontmatterPatch);
431
312
  return {
432
313
  ref,
433
- content: parsed.content,
434
314
  confidence: parseReflectConfidence(parsed.confidence),
435
- ...(frontmatter ? { frontmatter } : {}),
315
+ patch: parseReflectFrontmatterPatch(parsed.frontmatterPatch),
436
316
  };
437
317
  }
438
318
  function parseFramedReflectOutput(raw, targetRef) {
439
- const normalized = raw.replaceAll("\r\n", "\n").trim();
440
- const beginMarker = "AKM_REFLECT_CONTENT_BEGIN\n";
441
- const endMarker = "\nAKM_REFLECT_CONTENT_END";
442
- const beginIndex = normalized.indexOf(beginMarker);
443
- if (beginIndex < 0 || (beginIndex > 0 && normalized[beginIndex - 1] !== "\n")) {
444
- throw new Error("direct reflect response missing AKM_REFLECT_CONTENT_BEGIN marker");
445
- }
446
- const contentStart = beginIndex + beginMarker.length;
447
- const endIndex = normalized.lastIndexOf(endMarker);
448
- if (endIndex < contentStart || normalized.slice(endIndex + endMarker.length).trim()) {
449
- throw new Error("direct reflect response missing terminal AKM_REFLECT_CONTENT_END marker");
450
- }
451
- const headerLines = normalized.slice(0, beginIndex).trim().split("\n").filter(Boolean);
319
+ const headerLines = raw.replaceAll("\r\n", "\n").trim().split("\n").filter(Boolean);
452
320
  const header = (prefix) => headerLines.find((line) => line.startsWith(prefix));
453
321
  const confidenceLine = header("AKM_REFLECT_CONFIDENCE:");
454
322
  const refLine = header("AKM_REFLECT_REF:");
@@ -464,9 +332,6 @@ function parseFramedReflectOutput(raw, targetRef) {
464
332
  const ref = targetRef ?? refLine?.slice("AKM_REFLECT_REF:".length).trim() ?? "";
465
333
  if (!ref)
466
334
  throw new Error("direct reflect response contained an empty AKM_REFLECT_REF value");
467
- const content = normalized.slice(contentStart, endIndex);
468
- if (!content.trim())
469
- throw new Error("direct reflect response contained empty framed content");
470
335
  let parsedPatch;
471
336
  try {
472
337
  parsedPatch = JSON.parse(patchLine.slice("AKM_REFLECT_FRONTMATTER_PATCH:".length).trim());
@@ -474,9 +339,11 @@ function parseFramedReflectOutput(raw, targetRef) {
474
339
  catch {
475
340
  throw new Error("direct reflect response contained invalid frontmatter patch JSON");
476
341
  }
477
- const frontmatter = parseReflectFrontmatterPatch(parsedPatch);
478
- const confidence = parseReflectConfidence(Number(confidenceText));
479
- return { ref, content, confidence, ...(frontmatter ? { frontmatter } : {}) };
342
+ return {
343
+ ref,
344
+ confidence: parseReflectConfidence(Number(confidenceText)),
345
+ patch: parseReflectFrontmatterPatch(parsedPatch),
346
+ };
480
347
  }
481
348
  /**
482
349
  * One reflect iteration on any engine, as an agent-shaped result (errors
@@ -494,7 +361,7 @@ export async function runReflectIteration(opts) {
494
361
  ? (opts.timeoutMs ?? null)
495
362
  : Object.hasOwn(opts.runner, "timeoutMs")
496
363
  ? (opts.runner.timeoutMs ?? null)
497
- : DEFAULT_MODEL_WORK_TIMEOUT_MS;
364
+ : DEFAULT_LLM_TIMEOUT_MS;
498
365
  const deadline = typeof configuredTimeout === "number" ? start + configuredTimeout : undefined;
499
366
  const messages = [{ role: "user", content: opts.prompt ?? "" }];
500
367
  if (opts.priorDraft !== undefined && opts.iteration > 0) {
@@ -516,16 +383,8 @@ export async function runReflectIteration(opts) {
516
383
  parsed: { outputMode: opts.outputMode, repairAttempts },
517
384
  };
518
385
  };
519
- // A reply that broke the contract. An agent or SDK engine's failure names the engine; an LLM
520
- // reply keeps its message, because improve feeds a failed reflect's `error` into the next
521
- // prompts as a pattern to avoid, and rewording it would change those requests.
522
- const invalidReply = (err, reply) => {
523
- if (runnerIsLlm(opts.runner))
524
- return failure(err, "parse_error", reply, 0);
525
- const attempts = repairAttempts + 1;
526
- const message = `Engine "${opts.runner.engine}" reply was not a valid reflect proposal after ${attempts} attempt${attempts === 1 ? "" : "s"}: ${errMessage(err)}`;
527
- return failure(new Error(message), "parse_error", reply, 0);
528
- };
386
+ // A reply that broke the contract; its message is the parser's, on every engine kind.
387
+ const invalidReply = (err, reply) => failure(err, "parse_error", reply, 0);
529
388
  // The result of the dispatch that failed, kept for an agent or SDK engine.
530
389
  let dispatched;
531
390
  // Reflect parses and repairs its own reply (the repair turn below), so one dispatch, unvalidated.
@@ -669,7 +528,7 @@ function unsupportedTypeFailure(ref, type, detail, emitFailed) {
669
528
  },
670
529
  };
671
530
  }
672
- /** The target's parsed ref and current content, or a refusal for a type reflect cannot rewrite. */
531
+ /** The target's parsed ref and current content, or a refusal for a type reflect cannot patch. */
673
532
  async function resolveReflectSource(options, stash, emitFailed) {
674
533
  if (!options.ref)
675
534
  return { assetContent: undefined, parsedRef: undefined };
@@ -693,7 +552,7 @@ async function resolveReflectSource(options, stash, emitFailed) {
693
552
  }
694
553
  }
695
554
  catch {
696
- // An index miss is not fatal: the agent can still propose a fresh asset.
555
+ // An index miss is not fatal: reflect then has no content to patch.
697
556
  }
698
557
  }
699
558
  if (!REFLECT_ALLOWED_TYPES.has(parsedRef.type) &&
@@ -756,7 +615,7 @@ function preflightReflectDispatch(runnerSpec, onNotices) {
756
615
  const prepared = resolveExecution({
757
616
  content: "Validate reflect operation transport before dispatch.",
758
617
  runner: runnerSpec,
759
- current: { tools: MODEL_WORK_TOOLS },
618
+ modelWork: true,
760
619
  });
761
620
  const lowered = buildExecution(prepared.request, prepared.runner);
762
621
  onNotices(lowered.notices);
@@ -765,7 +624,7 @@ function preflightReflectDispatch(runnerSpec, onNotices) {
765
624
  /**
766
625
  * The flat 12k content cap exists for CLI argv; the HTTP runner can spend half
767
626
  * its context window (after the rest of the prompt) on the asset, reserving
768
- * the other half for the rewrite. Never below the flat floor.
627
+ * the other half for the reply. Never below the flat floor.
769
628
  */
770
629
  function computeReflectContentBudgetChars(promptInput, runnerSpec) {
771
630
  if (!runnerIsLlm(runnerSpec) || !promptInput.assetContent?.trim())
@@ -775,13 +634,10 @@ function computeReflectContentBudgetChars(promptInput, runnerSpec) {
775
634
  return Math.max(REFLECT_CONTENT_CAP, Math.floor((window - overhead) / 2));
776
635
  }
777
636
  /** Every read-only prompt input, shared by dispatch and `--show-prompt`. */
778
- async function gatherReflectPromptSources(options, stash, parsedRef, assetContent) {
637
+ function gatherReflectPromptSources(options, stash, parsedRef, assetContent) {
779
638
  return {
780
639
  feedback: readRecentFeedback(options.ref ? (options.itemRef ?? options.ref) : undefined, options.eventsCtx),
781
640
  schemaHints: buildSchemaHints(parsedRef?.type ?? "", assetContent),
782
- relatedLessons: options.ref && parsedRef
783
- ? await readRelatedLessons(stash, options.ref, parsedRef, options.itemRef, options.eventsCtx)
784
- : [],
785
641
  rejectedProposals: rejectedProposalContext(stash, options.ref, options.ctx, options.eventsCtx),
786
642
  standardsContext: resolveStandardsContext(options.ref, stash),
787
643
  };
@@ -789,7 +645,7 @@ async function gatherReflectPromptSources(options, stash, parsedRef, assetConten
789
645
  /** The exact prompt reflect sends, shared by dispatch and `--show-prompt`. */
790
646
  function buildReflectPromptText(args) {
791
647
  const { options, parsedRef, assetContent, sources, runnerSpec, priorDraft } = args;
792
- const { feedback, schemaHints, relatedLessons, rejectedProposals, standardsContext } = sources;
648
+ const { feedback, schemaHints, rejectedProposals, standardsContext } = sources;
793
649
  // An LLM engine that rejects JSON Schema gets the framed contract; every other engine gets the JSON object.
794
650
  const outputMode = runnerIsLlm(runnerSpec) && !wantsJsonSchemaOutput(runnerSpec.connection) ? "framed_markdown" : "json_schema";
795
651
  const input = {
@@ -799,7 +655,6 @@ function buildReflectPromptText(args) {
799
655
  ...(assetContent !== undefined ? { assetContent } : {}),
800
656
  ...(feedback.length > 0 ? { feedback } : {}),
801
657
  ...(schemaHints.length > 0 ? { schemaHints } : {}),
802
- ...(relatedLessons.length > 0 ? { relatedLessons } : {}),
803
658
  ...(options.task ? { task: options.task } : {}),
804
659
  ...(standardsContext.trim() ? { standardsContext } : {}),
805
660
  ...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
@@ -881,60 +736,36 @@ async function runReflectRefineIterations(args) {
881
736
  }
882
737
  return result;
883
738
  }
884
- /** The proposal payload from a successful run: the JSON payload on stdout. */
885
- function resolveReflectPayload(run, result) {
886
- const { options } = run;
887
- try {
888
- return { payload: parseAgentProposalPayload(result.stdout ?? "") };
889
- }
890
- catch (err) {
891
- run.emitFailed("parse_error", "parse_error", options.ref, {
892
- ...exitCodeMeta(result),
893
- ...(reflectTelemetry(result) ?? {}),
894
- });
895
- return {
896
- failure: reflectFailure(run, result, "parse_error", err instanceof Error ? err.message : String(err), true),
897
- };
898
- }
899
- }
900
739
  const NOISE_SUBREASONS = {
901
740
  noop: "reflect_skipped_noop",
902
741
  cosmetic: "reflect_skipped_cosmetic",
903
742
  "low-value": "reflect_skipped_low_value",
904
743
  };
905
744
  /**
906
- * Sanitize, drop a no-op/cosmetic (and optionally low-value) change, judge the
907
- * exact content that would be persisted, then mint. A judge pass is staged only
908
- * when the body is unchanged: a body edit the judge passes, or one made with the
909
- * gate off, waits for review. Size-flagged or truncation-leaking content skips
910
- * the judge and waits for review.
745
+ * Apply the patch, drop a no-op/cosmetic (and optionally low-value) change,
746
+ * judge the exact content that would be persisted, then mint. A revision with a
747
+ * deterministic defect is refused before the judge runs, whether or not the
748
+ * gate is on.
911
749
  */
912
750
  async function finalizeReflectProposal(args) {
913
- const { run, assetContent, result, judge, feedback } = args;
751
+ const { run, payload, assetContent, result, judge, feedback } = args;
914
752
  const { options } = run;
915
753
  const telemetry = reflectTelemetry(result) ?? {};
916
- const sanitized = sanitizeReflectPayload({ content: args.payload.content, ...(args.payload.frontmatter ? { frontmatter: args.payload.frontmatter } : {}) }, assetContent, args.payload.ref);
917
- const payload = {
918
- ...args.payload,
919
- content: sanitized.content,
920
- ...(sanitized.frontmatter ? { frontmatter: sanitized.frontmatter } : {}),
921
- };
922
- if (assetContent !== undefined) {
923
- const changeKind = classifyReflectChange(assetContent, payload.content);
924
- if (changeKind === "noop" ||
925
- changeKind === "cosmetic" ||
926
- (changeKind === "low-value" && options.lowValueFilter === true)) {
927
- run.emitFailed("no_change", NOISE_SUBREASONS[changeKind], options.ref, { changeKind, ...telemetry });
928
- const what = changeKind === "noop"
929
- ? "identical to the current asset (empty diff)"
930
- : changeKind === "low-value"
931
- ? "a low-value prose micro-rewrite (few changed tokens, no structural changes)"
932
- : "a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes)";
933
- return reflectFailure(run, result, "no_change", `Reflect skipped: proposed content for ${payload.ref} is ${what}; no proposal created.`, false);
934
- }
754
+ const patched = applyReflectPatch(payload.patch, assetContent, payload.ref);
755
+ // A patch that changes nothing leaves the asset as it is: an empty diff.
756
+ const content = patched?.content ?? assetContent;
757
+ const changeKind = classifyReflectChange(assetContent, content);
758
+ if (changeKind === "noop" ||
759
+ changeKind === "cosmetic" ||
760
+ (changeKind === "low-value" && options.lowValueFilter === true)) {
761
+ run.emitFailed("no_change", NOISE_SUBREASONS[changeKind], options.ref, { changeKind, ...telemetry });
762
+ const what = changeKind === "noop"
763
+ ? "identical to the current asset (empty diff)"
764
+ : changeKind === "low-value"
765
+ ? "a low-value prose micro-rewrite (few changed tokens, no structural changes)"
766
+ : "a cosmetic-only reformat of the current asset (whitespace/fence/YAML-folding changes)";
767
+ return reflectFailure(run, result, "no_change", `Reflect skipped: proposed content for ${payload.ref} is ${what}; no proposal created.`, false);
935
768
  }
936
- const flagged = Boolean(sanitized.sizeGuardRatio || sanitized.truncationMarkerLeaked);
937
- const judged = judge.enabled && !flagged;
938
769
  /** A judge refused the revision: record it for the ledger's rejection window and stop. */
939
770
  const refuse = (detail, metadata, message) => {
940
771
  if (options.ref) {
@@ -953,11 +784,16 @@ async function finalizeReflectProposal(args) {
953
784
  }, options.eventsCtx);
954
785
  return reflectFailure(run, result, "quality_rejected", message, false);
955
786
  };
787
+ // A defect no judge needs to weigh is refused before any judge call, whether or not the gate is on.
788
+ const defect = findReflectDefect(assetContent, content, options.defectFilter);
789
+ if (defect)
790
+ return refuse(defect, { reflectDefect: defect }, `Reflect proposal refused before the judge: ${defect}`);
956
791
  let verdict;
957
792
  let judgeFailed = false;
958
- if (judged) {
959
- verdict = await runReflectQualityJudge(run.config, payload.content, assetContent ?? "", feedback, options.chat, {
793
+ if (judge.enabled) {
794
+ verdict = await runReflectQualityJudge(run.config, content, assetContent, feedback, options.chat, {
960
795
  runnerSelectionFrozen: true,
796
+ ref: payload.ref,
961
797
  ...(judge.runner ? { llmRunner: judge.runner } : {}),
962
798
  ...(Object.hasOwn(options, "timeoutMs") ? { timeoutMs: options.timeoutMs } : {}),
963
799
  ...(options.signal ? { signal: options.signal } : {}),
@@ -973,12 +809,12 @@ async function finalizeReflectProposal(args) {
973
809
  }, `Reflect proposal quality gate rejected: score=${verdict.score}, reason="${verdict.reason}"`);
974
810
  }
975
811
  }
976
- // #722: a rewrite of an existing asset must not grade lower on its own retrieval queries.
977
- if (verdict?.pass && judge.runner && assetContent !== undefined) {
812
+ // #722: a revision of an existing asset must not grade lower on its own retrieval queries.
813
+ if (verdict?.pass && judge.runner) {
978
814
  const retrieval = await runRetrievalRegressionGate({
979
815
  ref: payload.ref,
980
816
  before: assetContent,
981
- after: payload.content,
817
+ after: content,
982
818
  queries: loadRetrievalQueries({ proposalsCtx: options.ctx, eventsCtx: options.eventsCtx }, payload.ref),
983
819
  runner: judge.runner,
984
820
  ...(options.chat ? { chat: options.chat } : {}),
@@ -995,33 +831,17 @@ async function finalizeReflectProposal(args) {
995
831
  }, `Reflect proposal refused: ${retrieval.reason}`);
996
832
  }
997
833
  }
998
- // A lesson reflect wrote is marked so a later reflect on the same skill does
999
- // not read it back as independent evidence.
1000
- const frontmatter = {
1001
- ...(payload.frontmatter ?? {}),
1002
- ...(lenientRefType(payload.ref) === "lesson" ? { derived_from_reflect: true } : {}),
1003
- };
1004
834
  const reviewReasons = [
1005
835
  ...(judge.skippedNoJudge ? ["no-judge-configured"] : []),
1006
836
  ...(judgeFailed ? ["judge-error"] : []),
1007
- ...(sanitized.sizeGuardRatio ? ["reflect-size-ratio"] : []),
1008
- ...(sanitized.truncationMarkerLeaked ? ["reflect-truncation-leak"] : []),
1009
837
  ];
1010
- // A revision that changes the body is never auto-accepted: on labelled edits, the judge's
1011
- // passes on body edits were good 12 times in 37, and on frontmatter-only edits 13 in 13.
1012
- // One that nothing above holds for review (the judge passed it, or the gate is off) waits
1013
- // for a person, as does a revision with no source to compare.
1014
- const bodyOf = (content) => splitFrontmatter(content).body.replace(/\s+/g, " ").trim();
1015
- const bodyEdit = reviewReasons.length === 0 && (assetContent === undefined || bodyOf(assetContent) !== bodyOf(payload.content));
1016
- if (bodyEdit)
1017
- reviewReasons.push("body-edit");
1018
838
  const proposal = mintProposal(run.stash, options.ctx, {
1019
839
  ref: payload.ref,
1020
840
  ...(options.target ? { target: options.target } : {}),
1021
841
  source: "reflect",
1022
842
  sourceRun: `reflect-${Date.now()}`,
1023
- payload: { content: payload.content, ...(Object.keys(frontmatter).length > 0 ? { frontmatter } : {}) },
1024
- ...(typeof payload.confidence === "number" ? { confidence: payload.confidence } : {}),
843
+ payload: { content, ...(patched?.frontmatter ? { frontmatter: patched.frontmatter } : {}) },
844
+ confidence: payload.confidence,
1025
845
  ...(options.eligibilitySource ? { eligibilitySource: options.eligibilitySource } : {}),
1026
846
  ...(options.itemRef ? { itemRef: options.itemRef, attemptedRefs: [options.itemRef] } : {}),
1027
847
  }, reviewReasons.length > 0
@@ -1030,10 +850,6 @@ async function finalizeReflectProposal(args) {
1030
850
  reason: reviewReasons.join("+"),
1031
851
  // The quality gate's hand-off to a person, as distill's: the triage drain leaves it alone.
1032
852
  gate: judgeFailed ? "quality-gate" : "reflect",
1033
- ...(sanitized.sizeGuardRatio ? { measured: Math.round(sanitized.sizeGuardRatio.ratio * 100) } : {}),
1034
- // The reviewer sees why the judge passed it (with the gate off, nothing judged it).
1035
- ...(bodyEdit && verdict?.criteria ? { scores: verdict.criteria } : {}),
1036
- ...(bodyEdit && verdict ? { judgeReason: verdict.reason } : {}),
1037
853
  },
1038
854
  }
1039
855
  : { judged: verdict });
@@ -1046,10 +862,6 @@ async function finalizeReflectProposal(args) {
1046
862
  engine: run.engineName,
1047
863
  ...(judge.skippedNoJudge ? { qualityGateSkippedNoJudge: true } : {}),
1048
864
  ...(judgeFailed ? { qualityReason: verdict?.reason } : {}),
1049
- ...(sanitized.sizeGuardRatio
1050
- ? { sizeGuardRatio: sanitized.sizeGuardRatio.code, sizeGuardRatioValue: sanitized.sizeGuardRatio.ratio }
1051
- : {}),
1052
- ...(sanitized.truncationMarkerLeaked ? { truncationMarkerLeaked: true } : {}),
1053
865
  ...telemetry,
1054
866
  },
1055
867
  }, options.eventsCtx);
@@ -1079,7 +891,7 @@ export async function renderReflectPromptPreview(options) {
1079
891
  throw new UsageError((!failure.ok && failure.error) || `Reflect cannot preview ref "${ref}".`, "INVALID_FLAG_VALUE");
1080
892
  }
1081
893
  const { runnerSpec, engineName } = resolveReflectRunner(options);
1082
- const sources = await gatherReflectPromptSources(options, stash, source.parsedRef, source.assetContent);
894
+ const sources = gatherReflectPromptSources(options, stash, source.parsedRef, source.assetContent);
1083
895
  const { prompt } = buildReflectPromptText({
1084
896
  options,
1085
897
  parsedRef: source.parsedRef,
@@ -1126,7 +938,7 @@ export async function akmReflect(options = {}) {
1126
938
  preflightReflectDispatch(runnerSpec, notices.add);
1127
939
  if (judgeRunner && judgeRunner !== runnerSpec)
1128
940
  preflightReflectDispatch(judgeRunner, notices.add);
1129
- const sources = await gatherReflectPromptSources(options, stash, parsedRef, assetContent);
941
+ const sources = gatherReflectPromptSources(options, stash, parsedRef, assetContent);
1130
942
  const agentEnv = options.eventSource === "improve" ? { AKM_EVENT_SOURCE: "improve" } : {};
1131
943
  const sensitiveValues = collectDispatchSensitiveValues(runnerSpec, {
1132
944
  ...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
@@ -1163,25 +975,30 @@ export async function akmReflect(options = {}) {
1163
975
  });
1164
976
  return { ...envelope, ...notices.fields() };
1165
977
  }
1166
- const resolved = resolveReflectPayload(run, result);
1167
- if ("failure" in resolved)
1168
- return resolved.failure;
1169
- payload = resolved.payload;
978
+ // The iteration parsed the reply and put its payload on stdout.
979
+ payload = JSON.parse(result.stdout);
1170
980
  }
1171
981
  catch (error) {
1172
982
  if (!(error instanceof ConfigError))
1173
983
  emitInvoked();
1174
984
  throw error;
1175
985
  }
1176
- const unsafeContent = generatedContentRejection(payload.content, redactSensitiveText(payload.content, sensitiveValues));
986
+ const generated = Object.values(payload.patch).join("\n");
987
+ const unsafeContent = generatedContentRejection(generated, redactSensitiveText(generated, sensitiveValues));
1177
988
  if (unsafeContent) {
1178
989
  emitFailed("parse_error", "parse_error", options.ref, exitCodeMeta(result));
1179
990
  return reflectFailure(run, result, "parse_error", unsafeContent, false);
1180
991
  }
992
+ // An unscoped reply names its asset only now: read it as a targeted run read its own before dispatch.
993
+ const target = options.ref
994
+ ? { assetContent }
995
+ : await resolveReflectSource({ ...options, ref: payload.ref }, stash, emitFailed);
996
+ if ("failure" in target)
997
+ return target.failure;
1181
998
  return finalizeReflectProposal({
1182
999
  run,
1183
1000
  payload,
1184
- assetContent,
1001
+ assetContent: target.assetContent ?? "",
1185
1002
  result,
1186
1003
  judge: { enabled: judgeWanted && !skippedNoJudge, skippedNoJudge, runner: judgeRunner },
1187
1004
  feedback: sources.feedback,