vibe-coding-master 0.7.31 → 0.7.32
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/README.md +8 -5
- package/dist/backend/services/auto-memory-service.js +88 -8
- package/dist/backend/services/harness-feedback-service.js +41 -3
- package/dist/backend/services/memory-proposal-validation.js +76 -31
- package/dist/backend/services/memory-review-validation.js +207 -15
- package/dist/backend/templates/harness/harness-engineer-agent.js +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -503,11 +503,14 @@ shared or current-role target and its supporting evidence. Adds and updates
|
|
|
503
503
|
also state why the memory is necessary, the impact of omitting it, and whether
|
|
504
504
|
the knowledge belongs in memory or a durable document. Harness Engineer first
|
|
505
505
|
reviews every existing memory entry, recording its retention reason, removal
|
|
506
|
-
impact, and durable-document disposition, then evaluates
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
and
|
|
510
|
-
|
|
506
|
+
impact, and durable-document disposition, then evaluates every proposal item
|
|
507
|
+
independently. Each Add or Update decision records why the memory is necessary,
|
|
508
|
+
what happens if it is absent, whether it belongs in memory or a durable
|
|
509
|
+
document, and the exact final memory content when retained. VCM validates that
|
|
510
|
+
every candidate has one matching decision and that the reviewed memory files
|
|
511
|
+
implement those decisions before applying them. Harness Engineer review and
|
|
512
|
+
retrospective work remain tool role activity and do not participate in Round
|
|
513
|
+
completion.
|
|
511
514
|
|
|
512
515
|
When Auto Memory is disabled, Review Task Harness does not collect proposals or
|
|
513
516
|
ask Harness Engineer to update memory. When enabled, both automatic and manual
|
|
@@ -6,8 +6,8 @@ import { VcmError } from "../errors.js";
|
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
7
|
import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
8
|
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
|
-
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
|
-
import {
|
|
9
|
+
import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
|
+
import { parseMemoryReviewReport, validateMemoryReviewOutput } from "./memory-review-validation.js";
|
|
11
11
|
const MEMORY_FILE_DEFINITIONS = [
|
|
12
12
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
13
13
|
{ path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
|
|
@@ -251,6 +251,7 @@ export function createAutoMemoryService(deps) {
|
|
|
251
251
|
statusCode: 409
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
|
+
const proposalCandidates = await readReviewCandidates(taskRepoRoot, state);
|
|
254
255
|
const timestamp = now();
|
|
255
256
|
state.reviewPromptDispatchedAt = timestamp;
|
|
256
257
|
state.retrospectiveReportPath = retrospectiveReportPath;
|
|
@@ -263,7 +264,7 @@ export function createAutoMemoryService(deps) {
|
|
|
263
264
|
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
264
265
|
currentMemoryPath: path.join(runRoot, "before"),
|
|
265
266
|
reviewedMemoryPath: path.join(runRoot, "after"),
|
|
266
|
-
|
|
267
|
+
proposalCandidates,
|
|
267
268
|
...(planningCandidatePath
|
|
268
269
|
? { planningCandidatePath: resolveRepoPath(taskRepoRoot, planningCandidatePath) }
|
|
269
270
|
: {})
|
|
@@ -360,11 +361,24 @@ export function createAutoMemoryService(deps) {
|
|
|
360
361
|
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective did not write the required retrospective report.");
|
|
361
362
|
return true;
|
|
362
363
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
await
|
|
364
|
+
try {
|
|
365
|
+
const report = await deps.fs.readText(state.retrospectiveReportPath);
|
|
366
|
+
const currentMemorySnapshot = await readRunMemorySet(input.taskRepoRoot, state.runId, "before");
|
|
367
|
+
const reviewedMemorySnapshot = await readRunMemorySet(input.taskRepoRoot, state.runId, "after");
|
|
368
|
+
const candidates = await readReviewCandidates(input.taskRepoRoot, state);
|
|
369
|
+
const reportResult = parseMemoryReviewReport(report, candidates, hasSubstantiveMemory(currentMemorySnapshot));
|
|
370
|
+
if (reportResult.error) {
|
|
371
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective memory review report ${reportResult.error}.`);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
const outputError = await validateReviewedMemoryOutput(input.taskRepoRoot, currentMemorySnapshot, reviewedMemorySnapshot, reportResult.decisions ?? []);
|
|
375
|
+
if (outputError) {
|
|
376
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective reviewed memory ${outputError}.`);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective memory review validation failed: ${errorMessage(error)}`);
|
|
368
382
|
return true;
|
|
369
383
|
}
|
|
370
384
|
await applyReviewedMemory(input.taskRepoRoot, state);
|
|
@@ -525,6 +539,41 @@ export function createAutoMemoryService(deps) {
|
|
|
525
539
|
? relativePath
|
|
526
540
|
: undefined;
|
|
527
541
|
}
|
|
542
|
+
async function readReviewCandidates(taskRepoRoot, state) {
|
|
543
|
+
const candidates = [];
|
|
544
|
+
for (const draft of state.drafts) {
|
|
545
|
+
const draftPath = resolveRepoPath(taskRepoRoot, draft.path);
|
|
546
|
+
const parsed = parseMemoryProposal(await deps.fs.readText(draftPath));
|
|
547
|
+
if (!parsed.proposal) {
|
|
548
|
+
throw new Error(`${draft.role} memory draft ${parsed.error ?? "could not be parsed"}`);
|
|
549
|
+
}
|
|
550
|
+
candidates.push(...toReviewCandidates(draft.role, draft.role, parsed.proposal.items));
|
|
551
|
+
}
|
|
552
|
+
const planningCandidatePath = await findPlanningCandidateSnapshot(taskRepoRoot, state.runId);
|
|
553
|
+
if (planningCandidatePath) {
|
|
554
|
+
const parsed = parseMemoryProposal(await deps.fs.readText(resolveRepoPath(taskRepoRoot, planningCandidatePath)));
|
|
555
|
+
if (!parsed.proposal) {
|
|
556
|
+
throw new Error(`Architect planning-session memory candidate ${parsed.error ?? "could not be parsed"}`);
|
|
557
|
+
}
|
|
558
|
+
candidates.push(...toReviewCandidates("architect-planning", "architect", parsed.proposal.items));
|
|
559
|
+
}
|
|
560
|
+
return candidates;
|
|
561
|
+
}
|
|
562
|
+
async function validateReviewedMemoryOutput(taskRepoRoot, before, after, decisions) {
|
|
563
|
+
return validateMemoryReviewOutput({
|
|
564
|
+
before: memorySetByTarget(before),
|
|
565
|
+
after: memorySetByTarget(after),
|
|
566
|
+
decisions,
|
|
567
|
+
durableDocExists: async (durableDocPath) => {
|
|
568
|
+
try {
|
|
569
|
+
return await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, durableDocPath));
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
528
577
|
async function applyReviewedMemory(taskRepoRoot, state) {
|
|
529
578
|
try {
|
|
530
579
|
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
@@ -756,6 +805,37 @@ export async function assertMemoryBlocksInstalled(fs, taskRepoRoot) {
|
|
|
756
805
|
function currentDraft(state) {
|
|
757
806
|
return state.drafts.find((draft) => draft.status !== "completed");
|
|
758
807
|
}
|
|
808
|
+
function toReviewCandidates(source, currentRole, items) {
|
|
809
|
+
return items.map((item) => ({
|
|
810
|
+
id: `${source}:${item.operation}:${item.ordinal}`,
|
|
811
|
+
source,
|
|
812
|
+
operation: item.operation,
|
|
813
|
+
target: item.target === "shared" ? "shared" : currentRole,
|
|
814
|
+
...(item.content ? { content: item.content } : {}),
|
|
815
|
+
...(item.existing ? { existing: item.existing } : {})
|
|
816
|
+
}));
|
|
817
|
+
}
|
|
818
|
+
function memoryPathForTarget(target) {
|
|
819
|
+
if (target === "shared") {
|
|
820
|
+
return "CLAUDE.md";
|
|
821
|
+
}
|
|
822
|
+
const definition = MEMORY_FILE_DEFINITIONS.find((candidate) => "role" in candidate && candidate.role === target);
|
|
823
|
+
if (!definition) {
|
|
824
|
+
throw new Error(`Missing memory file definition for review target: ${target}`);
|
|
825
|
+
}
|
|
826
|
+
return definition.path;
|
|
827
|
+
}
|
|
828
|
+
function memorySetByTarget(memory) {
|
|
829
|
+
return {
|
|
830
|
+
shared: memory["CLAUDE.md"],
|
|
831
|
+
"project-manager": memory[memoryPathForTarget("project-manager")],
|
|
832
|
+
architect: memory[memoryPathForTarget("architect")],
|
|
833
|
+
coder: memory[memoryPathForTarget("coder")],
|
|
834
|
+
tester: memory[memoryPathForTarget("tester")],
|
|
835
|
+
reviewer: memory[memoryPathForTarget("reviewer")],
|
|
836
|
+
"harness-engineer": memory[memoryPathForTarget("harness-engineer")]
|
|
837
|
+
};
|
|
838
|
+
}
|
|
759
839
|
function toActiveReview(state) {
|
|
760
840
|
return {
|
|
761
841
|
runId: state.runId,
|
|
@@ -260,9 +260,13 @@ export function createHarnessFeedbackService(deps) {
|
|
|
260
260
|
"Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
|
|
261
261
|
"For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
|
|
262
262
|
"Complete this full existing-memory review even when every proposal says no-change.",
|
|
263
|
-
"Then evaluate every proposal
|
|
263
|
+
"Then evaluate every proposal item independently. Do not accept or reject a whole role draft as one decision.",
|
|
264
|
+
"For every Add or Update candidate, independently explain why the memory is necessary, what fails if it is absent, and why memory or a durable document is the correct destination.",
|
|
265
|
+
"Do not copy the proposer rationale as the review. Verify it against current code, durable documentation, and final task evidence.",
|
|
264
266
|
"Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
|
|
265
267
|
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
268
|
+
"Final content must be one exact line written to the selected reviewed-memory file. Use none when the decision does not keep memory.",
|
|
269
|
+
"For keep-in-memory use Durable doc disposition: memory. For keep-memory-reference use memory-reference. For move-to-durable-doc use durable-doc.",
|
|
266
270
|
"Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
|
|
267
271
|
"Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
|
|
268
272
|
"Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
|
|
@@ -272,8 +276,10 @@ export function createHarnessFeedbackService(deps) {
|
|
|
272
276
|
"## Memory Review",
|
|
273
277
|
"Existing memory reviewed: complete",
|
|
274
278
|
"",
|
|
275
|
-
"### Proposal
|
|
276
|
-
...memoryReview.
|
|
279
|
+
"### Proposal Decisions",
|
|
280
|
+
...(memoryReview.proposalCandidates.length > 0
|
|
281
|
+
? memoryReview.proposalCandidates.flatMap(renderMemoryProposalDecisionTemplate)
|
|
282
|
+
: ["none"]),
|
|
277
283
|
"",
|
|
278
284
|
"### Existing Memory Decisions",
|
|
279
285
|
"#### Item 1",
|
|
@@ -299,6 +305,38 @@ export function createHarnessFeedbackService(deps) {
|
|
|
299
305
|
"End your turn after writing the result."
|
|
300
306
|
].join("\n");
|
|
301
307
|
}
|
|
308
|
+
function renderMemoryProposalDecisionTemplate(candidate) {
|
|
309
|
+
if (candidate.operation === "remove") {
|
|
310
|
+
return [
|
|
311
|
+
`#### Candidate ${candidate.id}`,
|
|
312
|
+
`Source: ${candidate.source}`,
|
|
313
|
+
"Operation: remove",
|
|
314
|
+
`Target: ${candidate.target}`,
|
|
315
|
+
`Existing: ${candidate.existing}`,
|
|
316
|
+
"Decision: remove|retain",
|
|
317
|
+
"Reason: <why the proposed removal should be applied or rejected>",
|
|
318
|
+
"Evidence checked: <current code, durable documentation, or final task evidence>",
|
|
319
|
+
""
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
return [
|
|
323
|
+
`#### Candidate ${candidate.id}`,
|
|
324
|
+
`Source: ${candidate.source}`,
|
|
325
|
+
`Operation: ${candidate.operation}`,
|
|
326
|
+
`Target: ${candidate.target}`,
|
|
327
|
+
`Candidate: ${candidate.content}`,
|
|
328
|
+
"Decision: keep-in-memory|keep-memory-reference|move-to-durable-doc|reject",
|
|
329
|
+
"Final target: shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none",
|
|
330
|
+
"Why memory is necessary: <independent reason, or why it is not necessary>",
|
|
331
|
+
"Impact if absent: <specific impact, or why no durable impact exists>",
|
|
332
|
+
"Durable doc disposition: memory|durable-doc|memory-reference",
|
|
333
|
+
"Durable doc analysis: <why this destination is correct>",
|
|
334
|
+
"Durable doc path: <none or a project-relative durable doc path>",
|
|
335
|
+
"Evidence checked: <current code, durable documentation, or final task evidence>",
|
|
336
|
+
"Final content: <exact one-line reviewed-memory content or none>",
|
|
337
|
+
""
|
|
338
|
+
];
|
|
339
|
+
}
|
|
302
340
|
function buildPendingFeedbackPrompt(repoRoot, feedbackPath) {
|
|
303
341
|
return [
|
|
304
342
|
"[VCM Harness Feedback]",
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
const OPERATIONS = ["Add", "Update", "Remove"];
|
|
2
2
|
export function validateMemoryProposal(content) {
|
|
3
|
+
return parseMemoryProposal(content).error;
|
|
4
|
+
}
|
|
5
|
+
export function parseMemoryProposal(content) {
|
|
3
6
|
if (!/^# Memory Proposal\s*$/m.test(content)) {
|
|
4
|
-
return "is missing the # Memory Proposal heading";
|
|
7
|
+
return { error: "is missing the # Memory Proposal heading" };
|
|
5
8
|
}
|
|
6
9
|
const decisionMatches = [...content.matchAll(/^Decision:[ \t]*(update|no-change)[ \t]*$/gm)];
|
|
7
10
|
if (decisionMatches.length !== 1) {
|
|
8
|
-
return "must contain exactly one Decision: update or Decision: no-change field";
|
|
11
|
+
return { error: "must contain exactly one Decision: update or Decision: no-change field" };
|
|
9
12
|
}
|
|
10
13
|
const levelTwoHeadings = [...content.matchAll(/^## ([^\r\n]+?)[ \t]*$/gm)];
|
|
11
14
|
const sections = [...content.matchAll(/^## (Add|Update|Remove)[ \t]*$/gm)];
|
|
12
15
|
if (levelTwoHeadings.length !== OPERATIONS.length
|
|
13
16
|
|| sections.length !== OPERATIONS.length
|
|
14
17
|
|| sections.some((section, index) => section[1] !== OPERATIONS[index])) {
|
|
15
|
-
return "must contain only the Add, Update, and Remove sections, exactly once and in that order";
|
|
18
|
+
return { error: "must contain only the Add, Update, and Remove sections, exactly once and in that order" };
|
|
16
19
|
}
|
|
17
|
-
|
|
20
|
+
const items = [];
|
|
18
21
|
for (let index = 0; index < sections.length; index += 1) {
|
|
19
22
|
const section = sections[index];
|
|
20
23
|
const operation = section[1];
|
|
@@ -22,30 +25,31 @@ export function validateMemoryProposal(content) {
|
|
|
22
25
|
const bodyEnd = index + 1 < sections.length
|
|
23
26
|
? sections[index + 1].index ?? content.length
|
|
24
27
|
: content.length;
|
|
25
|
-
const result =
|
|
26
|
-
if (
|
|
27
|
-
return result;
|
|
28
|
+
const result = parseOperationBody(operation, content.slice(bodyStart, bodyEnd));
|
|
29
|
+
if (result.error) {
|
|
30
|
+
return { error: result.error };
|
|
28
31
|
}
|
|
29
|
-
|
|
32
|
+
items.push(...result.items);
|
|
30
33
|
}
|
|
31
34
|
const decision = decisionMatches[0][1];
|
|
32
|
-
if (decision === "no-change" &&
|
|
33
|
-
return "uses Decision: no-change but contains a memory item";
|
|
35
|
+
if (decision === "no-change" && items.length !== 0) {
|
|
36
|
+
return { error: "uses Decision: no-change but contains a memory item" };
|
|
34
37
|
}
|
|
35
|
-
if (decision === "update" &&
|
|
36
|
-
return "uses Decision: update without a structured memory item";
|
|
38
|
+
if (decision === "update" && items.length === 0) {
|
|
39
|
+
return { error: "uses Decision: update without a structured memory item" };
|
|
37
40
|
}
|
|
38
|
-
return
|
|
41
|
+
return { proposal: { decision, items } };
|
|
39
42
|
}
|
|
40
|
-
function
|
|
43
|
+
function parseOperationBody(section, rawBody) {
|
|
41
44
|
const body = rawBody.trim();
|
|
42
45
|
if (body === "none") {
|
|
43
|
-
return
|
|
46
|
+
return { items: [] };
|
|
44
47
|
}
|
|
45
48
|
const itemHeadings = [...body.matchAll(/^### Item \d+\s*$/gm)];
|
|
46
49
|
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
47
|
-
return `${
|
|
50
|
+
return { items: [], error: `${section} must contain none or one or more ### Item N blocks` };
|
|
48
51
|
}
|
|
52
|
+
const items = [];
|
|
49
53
|
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
50
54
|
const heading = itemHeadings[index];
|
|
51
55
|
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
@@ -53,28 +57,69 @@ function validateOperationBody(operation, rawBody) {
|
|
|
53
57
|
? itemHeadings[index + 1].index ?? body.length
|
|
54
58
|
: body.length;
|
|
55
59
|
const item = body.slice(itemStart, itemEnd).trim();
|
|
56
|
-
const expectedPattern =
|
|
60
|
+
const expectedPattern = section === "Add"
|
|
57
61
|
? /^Target:[ \t]*(shared|current-role)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
58
|
-
:
|
|
62
|
+
: section === "Update"
|
|
59
63
|
? /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
|
|
60
64
|
: /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/;
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
const fields =
|
|
65
|
+
const match = expectedPattern.exec(item);
|
|
66
|
+
if (!match) {
|
|
67
|
+
const fields = section === "Add"
|
|
64
68
|
? "Target, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
65
|
-
:
|
|
69
|
+
: section === "Update"
|
|
66
70
|
? "Target, Existing, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
67
71
|
: "Target, Existing, and Evidence";
|
|
68
|
-
return
|
|
72
|
+
return {
|
|
73
|
+
items: [],
|
|
74
|
+
error: `${section} ${heading[0].trim()} must contain one-line ${fields} fields in that order`
|
|
75
|
+
};
|
|
69
76
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
const parsed = parseProposalItem(section, index + 1, match);
|
|
78
|
+
if (parsed.durableDocDisposition
|
|
79
|
+
&& ((parsed.durableDocDisposition === "memory" && parsed.durableDocPath !== "none")
|
|
80
|
+
|| (parsed.durableDocDisposition !== "memory" && parsed.durableDocPath === "none"))) {
|
|
81
|
+
return {
|
|
82
|
+
items: [],
|
|
83
|
+
error: `${section} ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`
|
|
84
|
+
};
|
|
77
85
|
}
|
|
86
|
+
items.push(parsed);
|
|
87
|
+
}
|
|
88
|
+
return { items };
|
|
89
|
+
}
|
|
90
|
+
function parseProposalItem(section, ordinal, match) {
|
|
91
|
+
if (section === "Add") {
|
|
92
|
+
return {
|
|
93
|
+
operation: "add",
|
|
94
|
+
ordinal,
|
|
95
|
+
target: match[1],
|
|
96
|
+
content: match[2],
|
|
97
|
+
reason: match[3],
|
|
98
|
+
impactIfAbsent: match[4],
|
|
99
|
+
durableDocDisposition: match[5],
|
|
100
|
+
durableDocPath: match[6],
|
|
101
|
+
evidence: match[7]
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (section === "Update") {
|
|
105
|
+
return {
|
|
106
|
+
operation: "update",
|
|
107
|
+
ordinal,
|
|
108
|
+
target: match[1],
|
|
109
|
+
existing: match[2],
|
|
110
|
+
content: match[3],
|
|
111
|
+
reason: match[4],
|
|
112
|
+
impactIfAbsent: match[5],
|
|
113
|
+
durableDocDisposition: match[6],
|
|
114
|
+
durableDocPath: match[7],
|
|
115
|
+
evidence: match[8]
|
|
116
|
+
};
|
|
78
117
|
}
|
|
79
|
-
return
|
|
118
|
+
return {
|
|
119
|
+
operation: "remove",
|
|
120
|
+
ordinal,
|
|
121
|
+
target: match[1],
|
|
122
|
+
existing: match[2],
|
|
123
|
+
evidence: match[3]
|
|
124
|
+
};
|
|
80
125
|
}
|
|
@@ -1,47 +1,222 @@
|
|
|
1
|
-
export function validateMemoryReviewReport(content,
|
|
1
|
+
export function validateMemoryReviewReport(content, candidates, hasExistingMemory = false) {
|
|
2
|
+
return parseMemoryReviewReport(content, candidates, hasExistingMemory).error;
|
|
3
|
+
}
|
|
4
|
+
export function parseMemoryReviewReport(content, candidates, hasExistingMemory = false) {
|
|
2
5
|
const memoryReview = /^## Memory Review\s*$/m.exec(content);
|
|
3
6
|
if (!memoryReview || memoryReview.index === undefined) {
|
|
4
|
-
return "is missing the ## Memory Review section";
|
|
7
|
+
return { error: "is missing the ## Memory Review section" };
|
|
5
8
|
}
|
|
6
9
|
const sectionStart = memoryReview.index + memoryReview[0].length;
|
|
7
10
|
const nextSection = /^## (?!#)/m.exec(content.slice(sectionStart));
|
|
8
11
|
const section = content.slice(sectionStart, nextSection?.index === undefined ? content.length : sectionStart + nextSection.index);
|
|
9
12
|
if (!/^Existing memory reviewed:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
10
|
-
return "must declare Existing memory reviewed: complete";
|
|
13
|
+
return { error: "must declare Existing memory reviewed: complete" };
|
|
11
14
|
}
|
|
12
15
|
if (!/^Reviewed memory set:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
13
|
-
return "must declare Reviewed memory set: complete";
|
|
16
|
+
return { error: "must declare Reviewed memory set: complete" };
|
|
14
17
|
}
|
|
15
|
-
const dispositions = extractReportSubsection(section, "Proposal
|
|
18
|
+
const dispositions = extractReportSubsection(section, "Proposal Decisions", "Existing Memory Decisions");
|
|
16
19
|
if (dispositions === undefined) {
|
|
17
|
-
return "is missing the Proposal
|
|
20
|
+
return { error: "is missing the Proposal Decisions subsection" };
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return `must record exactly one accepted, rejected, or no-change disposition for ${role}`;
|
|
23
|
-
}
|
|
22
|
+
const decisionsResult = parseProposalDecisions(dispositions, candidates);
|
|
23
|
+
if (decisionsResult.error) {
|
|
24
|
+
return decisionsResult;
|
|
24
25
|
}
|
|
25
26
|
const existingDecisions = extractReportSubsection(section, "Existing Memory Decisions", "Existing Memory Changes");
|
|
26
27
|
if (existingDecisions === undefined) {
|
|
27
|
-
return "is missing the Existing Memory Decisions subsection";
|
|
28
|
+
return { error: "is missing the Existing Memory Decisions subsection" };
|
|
28
29
|
}
|
|
29
30
|
const existingDecisionError = validateExistingMemoryDecisions(existingDecisions, hasExistingMemory);
|
|
30
31
|
if (existingDecisionError) {
|
|
31
|
-
return existingDecisionError;
|
|
32
|
+
return { error: existingDecisionError };
|
|
32
33
|
}
|
|
33
34
|
const existingChanges = extractReportSubsection(section, "Existing Memory Changes");
|
|
34
35
|
if (existingChanges === undefined) {
|
|
35
|
-
return "is missing the Existing Memory Changes subsection";
|
|
36
|
+
return { error: "is missing the Existing Memory Changes subsection" };
|
|
36
37
|
}
|
|
37
38
|
for (const field of ["retained", "updated", "removed"]) {
|
|
38
39
|
const matches = existingChanges.match(new RegExp(`^- ${field}:[ \\t]*\\S.*$`, "gm"));
|
|
39
40
|
if (matches?.length !== 1) {
|
|
40
|
-
return `must record exactly one non-empty ${field} summary
|
|
41
|
+
return { error: `must record exactly one non-empty ${field} summary` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { decisions: decisionsResult.decisions };
|
|
45
|
+
}
|
|
46
|
+
export async function validateMemoryReviewOutput(input) {
|
|
47
|
+
const retainedProposalContent = new Set(input.decisions
|
|
48
|
+
.filter((decision) => (decision.decision === "keep-in-memory"
|
|
49
|
+
|| decision.decision === "keep-memory-reference"))
|
|
50
|
+
.map((decision) => decision.finalContent)
|
|
51
|
+
.filter((content) => Boolean(content && content !== "none")));
|
|
52
|
+
for (const decision of input.decisions) {
|
|
53
|
+
const originalBefore = memoryLines(input.before[decision.target]);
|
|
54
|
+
const originalAfter = memoryLines(input.after[decision.target]);
|
|
55
|
+
if (decision.operation === "remove") {
|
|
56
|
+
if (decision.decision === "remove" && originalAfter.has(decision.existing ?? "")) {
|
|
57
|
+
return `still contains ${decision.candidateId}, which the report decided to remove`;
|
|
58
|
+
}
|
|
59
|
+
if (decision.decision === "retain" && !originalAfter.has(decision.existing ?? "")) {
|
|
60
|
+
return `does not retain ${decision.candidateId} as required by the report`;
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const keepsMemory = decision.decision === "keep-in-memory"
|
|
65
|
+
|| decision.decision === "keep-memory-reference";
|
|
66
|
+
if (keepsMemory) {
|
|
67
|
+
const finalTarget = decision.finalTarget;
|
|
68
|
+
if (!memoryLines(input.after[finalTarget]).has(decision.finalContent ?? "")) {
|
|
69
|
+
return `does not contain the exact Final content for ${decision.candidateId} in ${finalTarget}`;
|
|
70
|
+
}
|
|
71
|
+
if (decision.operation === "update"
|
|
72
|
+
&& decision.existing
|
|
73
|
+
&& decision.existing !== decision.finalContent
|
|
74
|
+
&& originalAfter.has(decision.existing)) {
|
|
75
|
+
return `still contains the superseded Existing value for ${decision.candidateId}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (decision.candidate
|
|
79
|
+
&& !memorySetContains(input.before, decision.candidate)
|
|
80
|
+
&& memorySetContains(input.after, decision.candidate)
|
|
81
|
+
&& !retainedProposalContent.has(decision.candidate)) {
|
|
82
|
+
return `contains the rejected or durable-doc-only candidate ${decision.candidateId}`;
|
|
83
|
+
}
|
|
84
|
+
if (decision.durableDocDisposition === "memory-reference"
|
|
85
|
+
&& decision.durableDocPath
|
|
86
|
+
&& !(await input.durableDocExists(decision.durableDocPath))) {
|
|
87
|
+
return `references a missing durable document for ${decision.candidateId}: ${decision.durableDocPath}`;
|
|
41
88
|
}
|
|
42
89
|
}
|
|
43
90
|
return undefined;
|
|
44
91
|
}
|
|
92
|
+
function parseProposalDecisions(content, candidates) {
|
|
93
|
+
const body = content.trim();
|
|
94
|
+
if (candidates.length === 0) {
|
|
95
|
+
return body === "none"
|
|
96
|
+
? { decisions: [] }
|
|
97
|
+
: { error: "Proposal Decisions must be none when no memory candidate exists" };
|
|
98
|
+
}
|
|
99
|
+
if (body === "none") {
|
|
100
|
+
return { error: "Proposal Decisions cannot be none while memory candidates exist" };
|
|
101
|
+
}
|
|
102
|
+
const headings = [...body.matchAll(/^#### Candidate ([A-Za-z0-9._:-]+)[ \t]*$/gm)];
|
|
103
|
+
if (headings.length === 0 || body.slice(0, headings[0].index).trim()) {
|
|
104
|
+
return { error: "Proposal Decisions must contain one #### Candidate <id> block per memory candidate" };
|
|
105
|
+
}
|
|
106
|
+
const expected = new Map(candidates.map((candidate) => [candidate.id, candidate]));
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
const decisions = [];
|
|
109
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
110
|
+
const heading = headings[index];
|
|
111
|
+
const candidateId = heading[1];
|
|
112
|
+
if (seen.has(candidateId)) {
|
|
113
|
+
return { error: `contains duplicate proposal decision for ${candidateId}` };
|
|
114
|
+
}
|
|
115
|
+
const candidate = expected.get(candidateId);
|
|
116
|
+
if (!candidate) {
|
|
117
|
+
return { error: `contains an unexpected proposal decision for ${candidateId}` };
|
|
118
|
+
}
|
|
119
|
+
seen.add(candidateId);
|
|
120
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
121
|
+
const itemEnd = index + 1 < headings.length
|
|
122
|
+
? headings[index + 1].index ?? body.length
|
|
123
|
+
: body.length;
|
|
124
|
+
const result = parseProposalDecision(candidate, body.slice(itemStart, itemEnd).trim());
|
|
125
|
+
if (result.error) {
|
|
126
|
+
return { error: result.error };
|
|
127
|
+
}
|
|
128
|
+
decisions.push(result.decision);
|
|
129
|
+
}
|
|
130
|
+
const missing = candidates.find((candidate) => !seen.has(candidate.id));
|
|
131
|
+
if (missing) {
|
|
132
|
+
return { error: `is missing the proposal decision for ${missing.id}` };
|
|
133
|
+
}
|
|
134
|
+
return { decisions };
|
|
135
|
+
}
|
|
136
|
+
function parseProposalDecision(candidate, item) {
|
|
137
|
+
if (candidate.operation === "remove") {
|
|
138
|
+
const match = /^Source:[ \t]*(\S.*)[ \t]*\nOperation:[ \t]*remove[ \t]*\nTarget:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(remove|retain)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nEvidence checked:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
139
|
+
if (!match) {
|
|
140
|
+
return {
|
|
141
|
+
error: `Proposal Decision ${candidate.id} must contain one-line Source, Operation, Target, Existing, Decision, Reason, and Evidence checked fields in that order`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (match[1] !== candidate.source
|
|
145
|
+
|| match[2] !== candidate.target
|
|
146
|
+
|| match[3] !== candidate.existing) {
|
|
147
|
+
return { error: `Proposal Decision ${candidate.id} does not match its source proposal` };
|
|
148
|
+
}
|
|
149
|
+
if (isUnresolvedReviewText(match[5]) || isUnresolvedReviewText(match[6])) {
|
|
150
|
+
return { error: `Proposal Decision ${candidate.id} must replace every review placeholder with verified reasoning and evidence` };
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
decision: {
|
|
154
|
+
candidateId: candidate.id,
|
|
155
|
+
source: match[1],
|
|
156
|
+
operation: "remove",
|
|
157
|
+
target: match[2],
|
|
158
|
+
existing: match[3],
|
|
159
|
+
decision: match[4],
|
|
160
|
+
reason: match[5],
|
|
161
|
+
evidenceChecked: match[6]
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const match = /^Source:[ \t]*(\S.*)[ \t]*\nOperation:[ \t]*(add|update)[ \t]*\nTarget:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nCandidate:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(keep-in-memory|keep-memory-reference|move-to-durable-doc|reject)[ \t]*\nFinal target:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none)[ \t]*\nWhy memory is necessary:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc analysis:[ \t]*(\S.*)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence checked:[ \t]*(\S.*)[ \t]*\nFinal content:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
166
|
+
if (!match) {
|
|
167
|
+
return {
|
|
168
|
+
error: `Proposal Decision ${candidate.id} must contain one-line Source, Operation, Target, Candidate, Decision, Final target, Why memory is necessary, Impact if absent, Durable doc disposition, Durable doc analysis, Durable doc path, Evidence checked, and Final content fields in that order`
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (match[1] !== candidate.source
|
|
172
|
+
|| match[2] !== candidate.operation
|
|
173
|
+
|| match[3] !== candidate.target
|
|
174
|
+
|| match[4] !== candidate.content) {
|
|
175
|
+
return { error: `Proposal Decision ${candidate.id} does not match its source proposal` };
|
|
176
|
+
}
|
|
177
|
+
if ([match[7], match[8], match[10], match[12]].some(isUnresolvedReviewText)) {
|
|
178
|
+
return { error: `Proposal Decision ${candidate.id} must replace every review placeholder with independent analysis and verified evidence` };
|
|
179
|
+
}
|
|
180
|
+
const decision = match[5];
|
|
181
|
+
const finalTarget = match[6];
|
|
182
|
+
const durableDocDisposition = match[9];
|
|
183
|
+
const durableDocPath = match[11];
|
|
184
|
+
const finalContent = match[13];
|
|
185
|
+
const keepsMemory = decision === "keep-in-memory" || decision === "keep-memory-reference";
|
|
186
|
+
if (keepsMemory && (finalTarget === "none" || finalContent === "none")) {
|
|
187
|
+
return { error: `Proposal Decision ${candidate.id} must provide Final target and Final content when keeping memory` };
|
|
188
|
+
}
|
|
189
|
+
if (!keepsMemory && (finalTarget !== "none" || finalContent !== "none")) {
|
|
190
|
+
return { error: `Proposal Decision ${candidate.id} must use Final target: none and Final content: none when not keeping memory` };
|
|
191
|
+
}
|
|
192
|
+
if ((decision === "keep-in-memory" && durableDocDisposition !== "memory")
|
|
193
|
+
|| (decision === "keep-memory-reference" && durableDocDisposition !== "memory-reference")
|
|
194
|
+
|| (decision === "move-to-durable-doc" && durableDocDisposition !== "durable-doc")) {
|
|
195
|
+
return { error: `Proposal Decision ${candidate.id} has inconsistent Decision and Durable doc disposition values` };
|
|
196
|
+
}
|
|
197
|
+
if ((durableDocDisposition === "memory" && durableDocPath !== "none")
|
|
198
|
+
|| (durableDocDisposition !== "memory" && durableDocPath === "none")) {
|
|
199
|
+
return { error: `Proposal Decision ${candidate.id} must use Durable doc path: none only with Durable doc disposition: memory` };
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
decision: {
|
|
203
|
+
candidateId: candidate.id,
|
|
204
|
+
source: match[1],
|
|
205
|
+
operation: match[2],
|
|
206
|
+
target: match[3],
|
|
207
|
+
candidate: match[4],
|
|
208
|
+
decision,
|
|
209
|
+
finalTarget,
|
|
210
|
+
whyMemoryIsNecessary: match[7],
|
|
211
|
+
impactIfAbsent: match[8],
|
|
212
|
+
durableDocDisposition,
|
|
213
|
+
durableDocAnalysis: match[10],
|
|
214
|
+
durableDocPath,
|
|
215
|
+
evidenceChecked: match[12],
|
|
216
|
+
finalContent
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
45
220
|
function validateExistingMemoryDecisions(content, hasExistingMemory) {
|
|
46
221
|
const body = content.trim();
|
|
47
222
|
if (body === "none") {
|
|
@@ -92,3 +267,20 @@ function extractReportSubsection(content, heading, nextHeading) {
|
|
|
92
267
|
function escapeRegExp(value) {
|
|
93
268
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
269
|
}
|
|
270
|
+
function memorySetContains(memory, value) {
|
|
271
|
+
return Object.values(memory).some((content) => memoryLines(content).has(value));
|
|
272
|
+
}
|
|
273
|
+
function memoryLines(content) {
|
|
274
|
+
return new Set(content
|
|
275
|
+
.split(/\r?\n/)
|
|
276
|
+
.map((line) => line.trim())
|
|
277
|
+
.filter(Boolean));
|
|
278
|
+
}
|
|
279
|
+
function isUnresolvedReviewText(value) {
|
|
280
|
+
const normalized = value.trim().toLowerCase();
|
|
281
|
+
return (/[<>]/.test(value)
|
|
282
|
+
|| normalized === "none"
|
|
283
|
+
|| normalized === "tbd"
|
|
284
|
+
|| normalized === "unknown"
|
|
285
|
+
|| normalized === "n/a");
|
|
286
|
+
}
|
|
@@ -77,14 +77,17 @@ You are not part of the task workflow round state.
|
|
|
77
77
|
even when every proposal says \`no-change\`.
|
|
78
78
|
- After reviewing existing memory, verify every role proposal against task
|
|
79
79
|
evidence, including any Architect planning-session candidate assigned by VCM.
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
Review every proposal item separately; never accept or reject an entire role
|
|
81
|
+
draft as one decision. For every Add or Update candidate, independently state
|
|
82
|
+
why the memory is necessary, what fails if it is absent, and why memory or a
|
|
83
|
+
durable document is the correct destination. Do not copy the proposer rationale
|
|
84
|
+
as the review. Treat every candidate as a proposal rather than authority, merge
|
|
82
85
|
duplicates, remove stale entries, and keep role-specific knowledge in the
|
|
83
86
|
matching role memory output.
|
|
84
87
|
- Do not keep the full content in memory when a durable document is the correct
|
|
85
88
|
source. Use a short memory reference only when the role needs that document
|
|
86
89
|
pointer across tasks.
|
|
87
|
-
- Record every proposal
|
|
90
|
+
- Record every proposal decision and the retained, updated, and removed
|
|
88
91
|
existing-memory decisions and summary in the exact Memory Review report block
|
|
89
92
|
assigned by VCM.
|
|
90
93
|
- Do not record task narrative, temporary state, unverified conclusions, or
|