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.
- package/CHANGELOG.md +243 -280
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -1
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -9
- package/dist/assets/prompts/reflect-llm-schema-contract.md +1 -3
- package/dist/assets/prompts/reflect-output-repair.md +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/improve/consolidate/pair-pass.js +1 -0
- package/dist/commands/improve/consolidate.js +7 -2
- package/dist/commands/improve/execution.js +2 -3
- package/dist/commands/improve/extract-cli.js +3 -2
- package/dist/commands/improve/extract.js +2 -1
- package/dist/commands/improve/improve-cli.js +33 -1
- package/dist/commands/improve/loop-stages.js +3 -0
- package/dist/commands/improve/reflect-noise.js +125 -0
- package/dist/commands/improve/reflect.js +150 -333
- package/dist/commands/improve/retrieval-gate.js +7 -2
- package/dist/commands/improve/session-asset.js +6 -0
- package/dist/commands/improve/stage.js +31 -39
- package/dist/commands/proposal/drain.js +4 -7
- package/dist/commands/proposal/propose.js +2 -11
- package/dist/commands/proposal/validators/proposal-quality-validators.js +11 -5
- package/dist/commands/proposal/validators/proposal-validators.js +4 -5
- package/dist/commands/read/search-cli.js +0 -38
- package/dist/core/asset/asset-serialize.js +1 -1
- package/dist/core/config/schema/engines.js +15 -33
- package/dist/core/config/schema/improve-processes.js +16 -0
- package/dist/core/content-safety.js +0 -24
- package/dist/core/redaction.js +4 -0
- package/dist/core/spawn-env.js +25 -0
- package/dist/core/structured.js +1 -1
- package/dist/execution/source.js +8 -12
- package/dist/integrations/agent/config.js +1 -3
- package/dist/integrations/agent/engine-resolution.js +0 -3
- package/dist/integrations/agent/execution.js +14 -13
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/model-map.js +15 -16
- package/dist/integrations/agent/profiles.js +2 -2
- package/dist/integrations/agent/prompts.js +51 -127
- package/dist/integrations/agent/request-lowering.js +9 -7
- package/dist/integrations/agent/runner-dispatch.js +25 -31
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -2
- package/dist/integrations/harnesses/amazonq/agent-builder.js +1 -2
- package/dist/integrations/harnesses/claude/agent-builder.js +4 -16
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -2
- package/dist/integrations/harnesses/codex/index.js +6 -11
- package/dist/integrations/harnesses/codex/session-log.js +211 -0
- package/dist/integrations/harnesses/ids.js +10 -16
- package/dist/integrations/harnesses/opencode/agent-builder.js +14 -24
- package/dist/integrations/harnesses/opencode/model-config.js +15 -62
- package/dist/integrations/harnesses/opencode/model-work-agent.js +71 -36
- package/dist/integrations/harnesses/opencode-sdk/harness.js +2 -6
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +32 -90
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -2
- package/dist/integrations/harnesses/pi/agent-builder.js +1 -2
- package/dist/integrations/harnesses/types.js +3 -3
- package/dist/llm/feature-gate.js +2 -5
- package/dist/llm/index-passes.js +2 -2
- package/dist/llm/structured-call.js +5 -5
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/scripts/akm-migrate-node.js +381 -239
- package/dist/scripts/akm-migrate.js +381 -239
- package/dist/workflows/exec/unit-dispatch.js +4 -13
- package/docs/reference/cli.md +29 -16
- package/docs/reference/configuration.md +79 -85
- package/docs/reference/data-and-telemetry.md +2 -3
- package/docs/reference/workflow-schema.md +6 -9
- package/package.json +1 -1
- 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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
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 {
|
|
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,
|
|
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 {
|
|
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
|
|
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,
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* required description
|
|
249
|
-
*
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 =
|
|
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
|
-
|
|
294
|
-
const derived = deriveDescriptionFromAsset(
|
|
295
|
-
if (derived)
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
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: ["
|
|
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", "
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
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
|
|
401
|
-
for (const field of
|
|
402
|
-
const fieldValue =
|
|
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
|
-
|
|
295
|
+
patch[field] = fieldValue.trim();
|
|
409
296
|
}
|
|
410
|
-
return
|
|
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
|
-
|
|
315
|
+
patch: parseReflectFrontmatterPatch(parsed.frontmatterPatch),
|
|
436
316
|
};
|
|
437
317
|
}
|
|
438
318
|
function parseFramedReflectOutput(raw, targetRef) {
|
|
439
|
-
const
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
:
|
|
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
|
|
520
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
*
|
|
907
|
-
* exact content that would be persisted, then mint. A
|
|
908
|
-
*
|
|
909
|
-
* gate
|
|
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
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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 (
|
|
959
|
-
verdict = await runReflectQualityJudge(run.config,
|
|
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
|
|
977
|
-
if (verdict?.pass && judge.runner
|
|
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:
|
|
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
|
|
1024
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1167
|
-
|
|
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
|
|
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,
|