vibe-coding-master 0.7.28 → 0.7.30
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 +26 -8
- package/dist/backend/api/harness-routes.js +0 -1
- package/dist/backend/api/task-routes.js +4 -2
- package/dist/backend/server.js +11 -7
- package/dist/backend/services/architect-restart-service.js +107 -20
- package/dist/backend/services/auto-memory-service.js +118 -61
- package/dist/backend/services/claude-hook-service.js +12 -2
- package/dist/backend/services/gate-review-service.js +62 -2
- package/dist/backend/services/harness-feedback-service.js +138 -7
- package/dist/backend/services/memory-proposal-validation.js +80 -0
- package/dist/backend/services/memory-review-paths.js +7 -0
- package/dist/backend/services/memory-review-validation.js +94 -0
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/claude-root.js +3 -3
- package/dist/backend/templates/harness/gate-review.js +79 -9
- package/dist/backend/templates/harness/harness-engineer-agent.js +53 -12
- package/dist/backend/templates/harness/project-manager-agent.js +15 -13
- package/dist/backend/templates/harness/restart-architect-skill.js +16 -2
- package/dist/backend/templates/harness/role-memory.js +2 -2
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +55 -7
- package/dist/shared/types/architect-restart.js +1 -0
- package/dist-frontend/assets/index-CguKU8Q5.css +32 -0
- package/dist-frontend/assets/{index-Bq1kZK1u.js → index-Cmr9W7-n.js} +22 -22
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist-frontend/assets/index-Ci7z8tW3.css +0 -32
|
@@ -68,7 +68,10 @@ const SOURCE_ARTIFACTS = {
|
|
|
68
68
|
".ai/vcm/handoffs/test-report.md",
|
|
69
69
|
"docs/TESTING.md"
|
|
70
70
|
],
|
|
71
|
-
"code-diff": [
|
|
71
|
+
"code-diff": [
|
|
72
|
+
".ai/vcm/handoffs/test-report.md",
|
|
73
|
+
".ai/vcm/gate-reviews/validation-adequacy-review.md"
|
|
74
|
+
]
|
|
72
75
|
};
|
|
73
76
|
const CODE_DIFF_SOURCE_ARTIFACTS = {
|
|
74
77
|
coder: [
|
|
@@ -309,6 +312,39 @@ export function createGateReviewService(deps) {
|
|
|
309
312
|
};
|
|
310
313
|
}
|
|
311
314
|
}
|
|
315
|
+
if (gate === "code-diff") {
|
|
316
|
+
const prerequisiteError = await readCodeDiffPrerequisiteError(deps, context, index);
|
|
317
|
+
if (prerequisiteError) {
|
|
318
|
+
index = applyGateState(index, gate, {
|
|
319
|
+
status: "failed",
|
|
320
|
+
decision: undefined,
|
|
321
|
+
error: prerequisiteError,
|
|
322
|
+
exceptionReason: undefined,
|
|
323
|
+
requestId: undefined,
|
|
324
|
+
requestPath: undefined,
|
|
325
|
+
inputHash: undefined,
|
|
326
|
+
baseCommit: undefined,
|
|
327
|
+
headCommit: undefined,
|
|
328
|
+
commits: undefined,
|
|
329
|
+
changedFiles: undefined,
|
|
330
|
+
diffStat: undefined,
|
|
331
|
+
codeDiffSource,
|
|
332
|
+
codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
|
|
333
|
+
requestedAt: undefined,
|
|
334
|
+
startedAt: undefined,
|
|
335
|
+
completedAt: now(),
|
|
336
|
+
callbackStatus: "not_sent",
|
|
337
|
+
callbackError: undefined
|
|
338
|
+
}, now(), true);
|
|
339
|
+
await saveIndex(deps.fs, context.taskRepoRoot, index);
|
|
340
|
+
return {
|
|
341
|
+
status: "failed_to_start",
|
|
342
|
+
gate,
|
|
343
|
+
record: index.gates[gate],
|
|
344
|
+
message: prerequisiteError
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
312
348
|
const codeDiffInput = gate === "code-diff"
|
|
313
349
|
? await resolveCodeDiffInput(deps, context, record)
|
|
314
350
|
: undefined;
|
|
@@ -1037,6 +1073,27 @@ async function readValidationReportError(fs, taskRepoRoot) {
|
|
|
1037
1073
|
return `${relativePath} is incomplete and cannot start validation-adequacy review. `
|
|
1038
1074
|
+ formatValidationArtifactFailure(check, content);
|
|
1039
1075
|
}
|
|
1076
|
+
async function readCodeDiffPrerequisiteError(deps, context, index) {
|
|
1077
|
+
const reportError = await readValidationReportError(deps.fs, context.taskRepoRoot);
|
|
1078
|
+
if (reportError) {
|
|
1079
|
+
return "code-diff requires completed Tester validation. " + reportError;
|
|
1080
|
+
}
|
|
1081
|
+
const validationGate = index.gates["validation-adequacy"];
|
|
1082
|
+
if (!validationGate.required) {
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
if (validationGate.status === "skipped" || validationGate.status === "overridden") {
|
|
1086
|
+
return undefined;
|
|
1087
|
+
}
|
|
1088
|
+
if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
|
|
1089
|
+
return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence.";
|
|
1090
|
+
}
|
|
1091
|
+
const currentValidationHash = await computeInputHash(deps, context.taskRepoRoot, "validation-adequacy");
|
|
1092
|
+
if (!validationGate.inputHash || validationGate.inputHash !== currentValidationHash) {
|
|
1093
|
+
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
|
|
1094
|
+
}
|
|
1095
|
+
return undefined;
|
|
1096
|
+
}
|
|
1040
1097
|
async function readArchitectureEvidenceError(fs, taskRepoRoot) {
|
|
1041
1098
|
const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
|
|
1042
1099
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
@@ -1420,7 +1477,10 @@ function getSourceArtifacts(gate, codeDiffSources) {
|
|
|
1420
1477
|
if (gate !== "code-diff") {
|
|
1421
1478
|
return SOURCE_ARTIFACTS[gate];
|
|
1422
1479
|
}
|
|
1423
|
-
return [...new Set(
|
|
1480
|
+
return [...new Set([
|
|
1481
|
+
...SOURCE_ARTIFACTS["code-diff"],
|
|
1482
|
+
...(codeDiffSources ?? []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source])
|
|
1483
|
+
])];
|
|
1424
1484
|
}
|
|
1425
1485
|
function resolveCodeDiffSources(record, codeDiffInput, currentSource) {
|
|
1426
1486
|
const continuingRecordedRange = record.baseCommit === codeDiffInput.baseCommit
|
|
@@ -49,7 +49,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
52
|
-
if (existingMarker) {
|
|
52
|
+
if (existingMarker && existingMarker.status !== "failed") {
|
|
53
53
|
throw new VcmError({
|
|
54
54
|
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
55
55
|
message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
|
|
@@ -77,19 +77,82 @@ export function createHarnessFeedbackService(deps) {
|
|
|
77
77
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
78
78
|
const timestamp = now();
|
|
79
79
|
const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
80
|
-
|
|
80
|
+
const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
|
|
81
|
+
const memoryReview = await deps.autoMemoryService?.prepareTaskRetrospectiveReview(input.taskRepoRoot, analysisAbsolutePath);
|
|
82
|
+
const marker = {
|
|
81
83
|
version: 1,
|
|
82
84
|
taskSlug,
|
|
83
85
|
trigger: input.trigger,
|
|
84
|
-
status: "
|
|
86
|
+
status: "running",
|
|
85
87
|
analysisPath,
|
|
86
88
|
finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
|
|
89
|
+
...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
|
|
87
90
|
createdAt: timestamp,
|
|
88
91
|
updatedAt: timestamp
|
|
89
|
-
}
|
|
90
|
-
|
|
92
|
+
};
|
|
93
|
+
const pendingFeedback = await listPendingFeedback(repoRoot);
|
|
94
|
+
try {
|
|
95
|
+
await persistTaskRetrospectiveMarker(repoRoot, marker);
|
|
96
|
+
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (memoryReview) {
|
|
100
|
+
await deps.autoMemoryService?.cancelTaskRetrospectiveReview(input.taskRepoRoot, memoryReview.runId);
|
|
101
|
+
}
|
|
102
|
+
const failedAt = now();
|
|
103
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
104
|
+
...marker,
|
|
105
|
+
status: "failed",
|
|
106
|
+
failedAt,
|
|
107
|
+
updatedAt: failedAt,
|
|
108
|
+
error: errorMessage(error)
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
91
112
|
return getState(repoRoot);
|
|
92
113
|
}
|
|
114
|
+
async function handleTaskRetrospectiveHook(repoRoot, input) {
|
|
115
|
+
const marker = await loadTaskRetrospectiveMarker(repoRoot, input.taskSlug);
|
|
116
|
+
if (!marker || (marker.status !== "running" && marker.status !== "triggered")) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
const timestamp = now();
|
|
123
|
+
if (input.eventName === "StopFailure") {
|
|
124
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
125
|
+
...marker,
|
|
126
|
+
status: "failed",
|
|
127
|
+
failedAt: timestamp,
|
|
128
|
+
updatedAt: timestamp,
|
|
129
|
+
error: "Harness Engineer Task Harness Retrospective turn failed."
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
const reportPath = resolveRepoPath(repoRoot, marker.analysisPath);
|
|
134
|
+
const reportReady = await deps.fs.pathExists(reportPath)
|
|
135
|
+
&& Boolean((await deps.fs.readText(reportPath)).trim());
|
|
136
|
+
if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
|
|
137
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
138
|
+
...marker,
|
|
139
|
+
status: "failed",
|
|
140
|
+
failedAt: timestamp,
|
|
141
|
+
updatedAt: timestamp,
|
|
142
|
+
error: !reportReady
|
|
143
|
+
? "Harness Engineer did not write the required Task Harness Retrospective report."
|
|
144
|
+
: "Task Harness Retrospective memory review failed."
|
|
145
|
+
});
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
149
|
+
...marker,
|
|
150
|
+
status: "completed",
|
|
151
|
+
completedAt: timestamp,
|
|
152
|
+
updatedAt: timestamp
|
|
153
|
+
});
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
93
156
|
async function assertHarnessEngineerAvailable(_repoRoot) {
|
|
94
157
|
return undefined;
|
|
95
158
|
}
|
|
@@ -162,12 +225,76 @@ export function createHarnessFeedbackService(deps) {
|
|
|
162
225
|
summary: metadata.summary
|
|
163
226
|
};
|
|
164
227
|
}
|
|
165
|
-
function buildTaskRetrospectivePrompt(repoRoot, analysisPath) {
|
|
228
|
+
function buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedbackPaths, memoryReview) {
|
|
229
|
+
const pendingFeedback = pendingFeedbackPaths.length > 0
|
|
230
|
+
? pendingFeedbackPaths.map((feedbackPath) => `- ${resolveRepoPath(repoRoot, feedbackPath)}`)
|
|
231
|
+
: ["none"];
|
|
166
232
|
return [
|
|
167
233
|
"[VCM Task Harness Retrospective]",
|
|
168
234
|
"",
|
|
169
235
|
"Review the completed task from the current active task worktree.",
|
|
170
236
|
"",
|
|
237
|
+
`Pending Feedback Directory: ${resolveRepoPath(repoRoot, PENDING_DIR)}`,
|
|
238
|
+
"",
|
|
239
|
+
"Pending Feedback:",
|
|
240
|
+
...pendingFeedback,
|
|
241
|
+
...(pendingFeedbackPaths.length > 0
|
|
242
|
+
? [
|
|
243
|
+
"",
|
|
244
|
+
"Process every listed feedback inside this retrospective. Record every disposition in the retrospective report, then delete the processed feedback files before ending the turn."
|
|
245
|
+
]
|
|
246
|
+
: []),
|
|
247
|
+
...(memoryReview
|
|
248
|
+
? [
|
|
249
|
+
"",
|
|
250
|
+
"Auto Memory Review:",
|
|
251
|
+
`Role drafts: ${memoryReview.roleDraftsPath}`,
|
|
252
|
+
`Current memory snapshot: ${memoryReview.currentMemoryPath}`,
|
|
253
|
+
...(memoryReview.planningCandidatePath
|
|
254
|
+
? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
|
|
255
|
+
: []),
|
|
256
|
+
`Write the complete reviewed memory set to: ${memoryReview.reviewedMemoryPath}`,
|
|
257
|
+
"",
|
|
258
|
+
"Review every memory candidate against final task evidence while performing this retrospective.",
|
|
259
|
+
"Each snapshot file contains only the matching <VCM-memory> block content. Edit every existing reviewed-memory file in place.",
|
|
260
|
+
"Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
|
|
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
|
+
"Complete this full existing-memory review even when every proposal says no-change.",
|
|
263
|
+
"Then evaluate every proposal, including its stated need, absence impact, and durable-document disposition.",
|
|
264
|
+
"Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
|
|
265
|
+
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
266
|
+
"Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
|
|
267
|
+
"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
|
+
"Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
|
|
269
|
+
"Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
|
|
270
|
+
"Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
|
|
271
|
+
"",
|
|
272
|
+
"## Memory Review",
|
|
273
|
+
"Existing memory reviewed: complete",
|
|
274
|
+
"",
|
|
275
|
+
"### Proposal Dispositions",
|
|
276
|
+
...memoryReview.proposalRoles.map((role) => `- ${role}: accepted|rejected|no-change`),
|
|
277
|
+
"",
|
|
278
|
+
"### Existing Memory Decisions",
|
|
279
|
+
"#### Item 1",
|
|
280
|
+
"Target: shared",
|
|
281
|
+
"Existing: <exact existing memory entry>",
|
|
282
|
+
"Decision: retain",
|
|
283
|
+
"Reason: <why this decision is correct>",
|
|
284
|
+
"Impact if removed: <specific future role or task failure>",
|
|
285
|
+
"Durable doc disposition: memory",
|
|
286
|
+
"Durable doc path: none",
|
|
287
|
+
"Evidence: <current code, durable documentation, or final task evidence>",
|
|
288
|
+
"",
|
|
289
|
+
"### Existing Memory Changes",
|
|
290
|
+
"- retained: <summary or none>",
|
|
291
|
+
"- updated: <summary or none>",
|
|
292
|
+
"- removed: <summary or none>",
|
|
293
|
+
"",
|
|
294
|
+
"Reviewed memory set: complete"
|
|
295
|
+
]
|
|
296
|
+
: []),
|
|
297
|
+
"",
|
|
171
298
|
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
|
|
172
299
|
"End your turn after writing the result."
|
|
173
300
|
].join("\n");
|
|
@@ -190,7 +317,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
190
317
|
return deps.fs.readJson(markerPath);
|
|
191
318
|
}
|
|
192
319
|
async function persistTaskRetrospectiveMarker(repoRoot, marker) {
|
|
193
|
-
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(
|
|
320
|
+
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(marker.taskSlug));
|
|
194
321
|
await deps.fs.ensureDir(path.dirname(markerPath));
|
|
195
322
|
await deps.fs.writeJsonAtomic(markerPath, marker);
|
|
196
323
|
}
|
|
@@ -214,6 +341,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
214
341
|
getState,
|
|
215
342
|
sendPendingFeedback,
|
|
216
343
|
startTaskRetrospective,
|
|
344
|
+
handleTaskRetrospectiveHook,
|
|
217
345
|
assertHarnessEngineerAvailable
|
|
218
346
|
};
|
|
219
347
|
}
|
|
@@ -245,3 +373,6 @@ function sanitizeFeedbackId(value) {
|
|
|
245
373
|
function sha256(content) {
|
|
246
374
|
return createHash("sha256").update(content).digest("hex");
|
|
247
375
|
}
|
|
376
|
+
function errorMessage(error) {
|
|
377
|
+
return error instanceof Error ? error.message : String(error);
|
|
378
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const OPERATIONS = ["Add", "Update", "Remove"];
|
|
2
|
+
export function validateMemoryProposal(content) {
|
|
3
|
+
if (!/^# Memory Proposal\s*$/m.test(content)) {
|
|
4
|
+
return "is missing the # Memory Proposal heading";
|
|
5
|
+
}
|
|
6
|
+
const decisionMatches = [...content.matchAll(/^Decision:[ \t]*(update|no-change)[ \t]*$/gm)];
|
|
7
|
+
if (decisionMatches.length !== 1) {
|
|
8
|
+
return "must contain exactly one Decision: update or Decision: no-change field";
|
|
9
|
+
}
|
|
10
|
+
const levelTwoHeadings = [...content.matchAll(/^## ([^\r\n]+?)[ \t]*$/gm)];
|
|
11
|
+
const sections = [...content.matchAll(/^## (Add|Update|Remove)[ \t]*$/gm)];
|
|
12
|
+
if (levelTwoHeadings.length !== OPERATIONS.length
|
|
13
|
+
|| sections.length !== OPERATIONS.length
|
|
14
|
+
|| 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";
|
|
16
|
+
}
|
|
17
|
+
let itemCount = 0;
|
|
18
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
19
|
+
const section = sections[index];
|
|
20
|
+
const operation = section[1];
|
|
21
|
+
const bodyStart = (section.index ?? 0) + section[0].length;
|
|
22
|
+
const bodyEnd = index + 1 < sections.length
|
|
23
|
+
? sections[index + 1].index ?? content.length
|
|
24
|
+
: content.length;
|
|
25
|
+
const result = validateOperationBody(operation, content.slice(bodyStart, bodyEnd));
|
|
26
|
+
if (typeof result === "string") {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
itemCount += result;
|
|
30
|
+
}
|
|
31
|
+
const decision = decisionMatches[0][1];
|
|
32
|
+
if (decision === "no-change" && itemCount !== 0) {
|
|
33
|
+
return "uses Decision: no-change but contains a memory item";
|
|
34
|
+
}
|
|
35
|
+
if (decision === "update" && itemCount === 0) {
|
|
36
|
+
return "uses Decision: update without a structured memory item";
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function validateOperationBody(operation, rawBody) {
|
|
41
|
+
const body = rawBody.trim();
|
|
42
|
+
if (body === "none") {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
const itemHeadings = [...body.matchAll(/^### Item \d+\s*$/gm)];
|
|
46
|
+
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
47
|
+
return `${operation} must contain none or one or more ### Item N blocks`;
|
|
48
|
+
}
|
|
49
|
+
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
50
|
+
const heading = itemHeadings[index];
|
|
51
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
52
|
+
const itemEnd = index + 1 < itemHeadings.length
|
|
53
|
+
? itemHeadings[index + 1].index ?? body.length
|
|
54
|
+
: body.length;
|
|
55
|
+
const item = body.slice(itemStart, itemEnd).trim();
|
|
56
|
+
const expectedPattern = operation === "Add"
|
|
57
|
+
? /^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
|
+
: operation === "Update"
|
|
59
|
+
? /^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
|
+
: /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/;
|
|
61
|
+
const fieldMatch = expectedPattern.exec(item);
|
|
62
|
+
if (!fieldMatch) {
|
|
63
|
+
const fields = operation === "Add"
|
|
64
|
+
? "Target, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
65
|
+
: operation === "Update"
|
|
66
|
+
? "Target, Existing, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
|
|
67
|
+
: "Target, Existing, and Evidence";
|
|
68
|
+
return `${operation} ${heading[0].trim()} must contain one-line ${fields} fields in that order`;
|
|
69
|
+
}
|
|
70
|
+
if (operation === "Add" || operation === "Update") {
|
|
71
|
+
const disposition = fieldMatch[operation === "Add" ? 5 : 6];
|
|
72
|
+
const durableDocPath = fieldMatch[operation === "Add" ? 6 : 7];
|
|
73
|
+
if ((disposition === "memory" && durableDocPath !== "none")
|
|
74
|
+
|| (disposition !== "memory" && durableDocPath === "none")) {
|
|
75
|
+
return `${operation} ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return itemHeadings.length;
|
|
80
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
|
|
2
|
+
export const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
|
|
3
|
+
export const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
|
|
4
|
+
export const ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH = `${MEMORY_REVIEW_ROOT}/candidates/architect/planning.md`;
|
|
5
|
+
export function architectPlanningCandidateSnapshotPath(runId) {
|
|
6
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/sources/architect-planning.md`;
|
|
7
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export function validateMemoryReviewReport(content, proposalRoles, hasExistingMemory = false) {
|
|
2
|
+
const memoryReview = /^## Memory Review\s*$/m.exec(content);
|
|
3
|
+
if (!memoryReview || memoryReview.index === undefined) {
|
|
4
|
+
return "is missing the ## Memory Review section";
|
|
5
|
+
}
|
|
6
|
+
const sectionStart = memoryReview.index + memoryReview[0].length;
|
|
7
|
+
const nextSection = /^## (?!#)/m.exec(content.slice(sectionStart));
|
|
8
|
+
const section = content.slice(sectionStart, nextSection?.index === undefined ? content.length : sectionStart + nextSection.index);
|
|
9
|
+
if (!/^Existing memory reviewed:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
10
|
+
return "must declare Existing memory reviewed: complete";
|
|
11
|
+
}
|
|
12
|
+
if (!/^Reviewed memory set:[ \t]*complete[ \t]*$/m.test(section)) {
|
|
13
|
+
return "must declare Reviewed memory set: complete";
|
|
14
|
+
}
|
|
15
|
+
const dispositions = extractReportSubsection(section, "Proposal Dispositions", "Existing Memory Decisions");
|
|
16
|
+
if (dispositions === undefined) {
|
|
17
|
+
return "is missing the Proposal Dispositions subsection";
|
|
18
|
+
}
|
|
19
|
+
for (const role of proposalRoles) {
|
|
20
|
+
const matches = dispositions.match(new RegExp(`^- ${escapeRegExp(role)}:[ \\t]*(accepted|rejected|no-change)[ \\t]*$`, "gm"));
|
|
21
|
+
if (matches?.length !== 1) {
|
|
22
|
+
return `must record exactly one accepted, rejected, or no-change disposition for ${role}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const existingDecisions = extractReportSubsection(section, "Existing Memory Decisions", "Existing Memory Changes");
|
|
26
|
+
if (existingDecisions === undefined) {
|
|
27
|
+
return "is missing the Existing Memory Decisions subsection";
|
|
28
|
+
}
|
|
29
|
+
const existingDecisionError = validateExistingMemoryDecisions(existingDecisions, hasExistingMemory);
|
|
30
|
+
if (existingDecisionError) {
|
|
31
|
+
return existingDecisionError;
|
|
32
|
+
}
|
|
33
|
+
const existingChanges = extractReportSubsection(section, "Existing Memory Changes");
|
|
34
|
+
if (existingChanges === undefined) {
|
|
35
|
+
return "is missing the Existing Memory Changes subsection";
|
|
36
|
+
}
|
|
37
|
+
for (const field of ["retained", "updated", "removed"]) {
|
|
38
|
+
const matches = existingChanges.match(new RegExp(`^- ${field}:[ \\t]*\\S.*$`, "gm"));
|
|
39
|
+
if (matches?.length !== 1) {
|
|
40
|
+
return `must record exactly one non-empty ${field} summary`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function validateExistingMemoryDecisions(content, hasExistingMemory) {
|
|
46
|
+
const body = content.trim();
|
|
47
|
+
if (body === "none") {
|
|
48
|
+
return hasExistingMemory
|
|
49
|
+
? "Existing Memory Decisions cannot be none while substantive existing memory is present"
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
const itemHeadings = [...body.matchAll(/^#### Item \d+[ \t]*$/gm)];
|
|
53
|
+
if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
|
|
54
|
+
return "Existing Memory Decisions must contain none or one or more #### Item N blocks";
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < itemHeadings.length; index += 1) {
|
|
57
|
+
const heading = itemHeadings[index];
|
|
58
|
+
const itemStart = (heading.index ?? 0) + heading[0].length;
|
|
59
|
+
const itemEnd = index + 1 < itemHeadings.length
|
|
60
|
+
? itemHeadings[index + 1].index ?? body.length
|
|
61
|
+
: body.length;
|
|
62
|
+
const item = body.slice(itemStart, itemEnd).trim();
|
|
63
|
+
const match = /^Target:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(retain|update|remove|move-to-durable-doc)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if removed:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/.exec(item);
|
|
64
|
+
if (!match) {
|
|
65
|
+
return `Existing Memory Decisions ${heading[0].trim()} must contain one-line Target, Existing, Decision, Reason, Impact if removed, Durable doc disposition, Durable doc path, and Evidence fields in that order`;
|
|
66
|
+
}
|
|
67
|
+
const disposition = match[6];
|
|
68
|
+
const durableDocPath = match[7];
|
|
69
|
+
if ((disposition === "memory" && durableDocPath !== "none")
|
|
70
|
+
|| (disposition !== "memory" && durableDocPath === "none")) {
|
|
71
|
+
return `Existing Memory Decisions ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function extractReportSubsection(content, heading, nextHeading) {
|
|
77
|
+
const start = new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
|
|
78
|
+
if (!start || start.index === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const bodyStart = start.index + start[0].length;
|
|
82
|
+
if (nextHeading) {
|
|
83
|
+
const end = new RegExp(`^### ${escapeRegExp(nextHeading)}\\s*$`, "m")
|
|
84
|
+
.exec(content.slice(bodyStart));
|
|
85
|
+
return end?.index === undefined
|
|
86
|
+
? undefined
|
|
87
|
+
: content.slice(bodyStart, bodyStart + end.index);
|
|
88
|
+
}
|
|
89
|
+
const reviewedSet = /^Reviewed memory set:/m.exec(content.slice(bodyStart));
|
|
90
|
+
return content.slice(bodyStart, reviewedSet?.index === undefined ? content.length : bodyStart + reviewedSet.index);
|
|
91
|
+
}
|
|
92
|
+
function escapeRegExp(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
@@ -115,8 +115,9 @@ ${renderRoleMemoryRules("architect")}
|
|
|
115
115
|
|
|
116
116
|
#### Planning Completion
|
|
117
117
|
|
|
118
|
-
-
|
|
119
|
-
-
|
|
118
|
+
- Before the first completed Architect-to-PM planning route, use the \`restart-architect\` skill after the complete plan, scaffold, reconciliation, L0 evidence, and commits are ready.
|
|
119
|
+
- If VCM returns a \`memoryCandidatePath\`, ensure one planning-session memory candidate exists at that path before routing. Use \`vcm-propose-memory\` only when the candidate is absent. Include only verified, durable, reusable project knowledge from planning; do not record task narrative, temporary state, unverified conclusions, or Harness rules.
|
|
120
|
+
- Gate revision rounds reuse the same pending restart and task-level candidate; do not recreate either one. Write the latest route message with both architecture artifacts and the plan, then end the turn. VCM restarts the session only after the Gate is accepted. Do not wait for or inspect the replacement session.
|
|
120
121
|
|
|
121
122
|
### Complete Task Planning
|
|
122
123
|
|
|
@@ -7,7 +7,7 @@ export function renderRootClaudeHarnessRules() {
|
|
|
7
7
|
- Project-manager uses \`vcm-task-state\` to declare the current workflow checkpoint. This state is recoverable context only; flow rules and task artifacts remain authoritative.
|
|
8
8
|
- Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
|
|
9
9
|
- Use \`vcm-report-harness-issue\` when you notice a reusable VCM harness problem. Record feedback; do not contact Harness Engineer directly.
|
|
10
|
-
- The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns
|
|
10
|
+
- The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns an exact memory proposal or candidate path.
|
|
11
11
|
- Only the user may approve scope reduction, skipped required validation, Gate Review skip or override, skipped required docs sync, accepted unresolved task-scope risk, or weakening of baseline Harness rules. PM may record and route the user's approval but cannot grant it.
|
|
12
12
|
- Project-manager runs \`vcm-gate-review\` unconditionally at every Gate Review trigger point and on VCM Gate Review callbacks; the tool reports the authoritative enable state.
|
|
13
13
|
|
|
@@ -51,8 +51,8 @@ If a reusable harness problem is suspected, it is enough to record a concise fee
|
|
|
51
51
|
- All standard workflow routes among project-manager, architect, coder, and tester are PM-hub routes. Project-manager starts and advances every flow; architect, coder, and tester report blockers, failures, conflicts, incomplete work, and findings back to project-manager.
|
|
52
52
|
- Code changes use: \`project-manager -> architect interview -> architect planning -> coder -> tester -> architect docs sync -> project-manager final acceptance\`.
|
|
53
53
|
- Architect Debug Mode runs inside either Architect Debug Flow or Architect Debug Branch. Architecture Diagnosis Mode runs inside either Architecture Diagnosis Flow or Architecture Diagnosis Branch.
|
|
54
|
-
- Architect Debug Flow and an Architecture Diagnosis Flow that produces code changes
|
|
55
|
-
- Architect Debug Branch and Architecture Diagnosis Branch preserve the active parent flow and resume point, then return there after
|
|
54
|
+
- Code-Change Flow, Architect Debug Flow, and an Architecture Diagnosis Flow that produces code changes run tester validation, validation-adequacy Gate Review, and then code-diff Gate Review before architect docs sync and project-manager final acceptance. An analysis-only Architecture Diagnosis Flow completes from the diagnosis result.
|
|
55
|
+
- Architect Debug Branch and Architecture Diagnosis Branch preserve the active parent flow and resume point, then return there after tester validation, validation-adequacy Gate Review, and code-diff Gate Review complete. They do not run their own final acceptance.
|
|
56
56
|
- Docs-Only Flow uses: \`project-manager -> architect -> project-manager completion\`.
|
|
57
57
|
- Validation-Only Flow uses: \`project-manager -> tester -> validation-adequacy Gate Review -> project-manager completion\`.
|
|
58
58
|
- Communication-Only Flow uses: \`project-manager response or relay -> completion\`.
|
|
@@ -207,10 +207,12 @@ convert the result to \`pass\` or independently accept the risk.
|
|
|
207
207
|
|
|
208
208
|
## Code Diff Gate
|
|
209
209
|
|
|
210
|
-
Read \`.claude/agents/coder.md
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
210
|
+
Read \`.claude/agents/coder.md\`, \`.claude/agents/tester.md\`,
|
|
211
|
+
\`.ai/vcm/handoffs/test-report.md\`, the current validation-adequacy Gate report,
|
|
212
|
+
and \`docs/CODING_STANDARDS.md\`; use the architect definition to understand
|
|
213
|
+
implementation responsibility boundaries. Code-diff runs only after Tester
|
|
214
|
+
validation and the current validation-adequacy disposition. Review every commit
|
|
215
|
+
in the range named by VCM and nothing outside that range.
|
|
214
216
|
|
|
215
217
|
Use every code source and evidence artifact named in the VCM prompt. A source
|
|
216
218
|
chain means the range contains the original implementation and later corrective
|
|
@@ -262,6 +264,11 @@ and changes outside its governing evidence. Verify callable and public-surface
|
|
|
262
264
|
changes against their callers, exports, compatibility obligations, generated
|
|
263
265
|
context, and durable documentation.
|
|
264
266
|
|
|
267
|
+
Use the completed test report and validation-adequacy disposition as execution
|
|
268
|
+
evidence while independently deciding whether the implementation handles its
|
|
269
|
+
required behavior and boundary cases. Do not repeat the validation-adequacy
|
|
270
|
+
decision.
|
|
271
|
+
|
|
265
272
|
Inspect changed baseline tests for the changed callable units and applicable
|
|
266
273
|
branches. Request changes for weakened, deleted, skipped, fabricated, or
|
|
267
274
|
implementation-shaped tests, and for obvious missing baseline coverage required
|
|
@@ -464,8 +471,8 @@ Use this skill at every project-manager Gate Review trigger point and whenever V
|
|
|
464
471
|
## Trigger Points
|
|
465
472
|
|
|
466
473
|
- \`architecture-plan\`: after the user confirms \`.ai/vcm/handoffs/architecture-brief.md\` and architect writes \`.ai/vcm/handoffs/architecture-plan.md\`, before coder dispatch.
|
|
467
|
-
- \`validation-adequacy\`: after tester writes a terminal \`Test Result: pass|fail\` that the active flow permits to reach the gate
|
|
468
|
-
- \`code-diff\`: after
|
|
474
|
+
- \`validation-adequacy\`: after tester writes a terminal \`Test Result: pass|fail\` that the active flow permits to reach the gate. Never request this gate for \`Test Result: incomplete\`.
|
|
475
|
+
- \`code-diff\`: after Tester completes and the current validation-adequacy Gate finishes successfully for a Coder implementation, Architect Debug fix, or Architecture Diagnosis fix. Identify the production-code source with \`--source coder\`, \`--source architect-debug\`, or \`--source architect-diagnosis\`. Validation-Only Flow does not request code-diff.
|
|
469
476
|
|
|
470
477
|
## Request
|
|
471
478
|
|
|
@@ -500,6 +507,7 @@ import argparse
|
|
|
500
507
|
import hashlib
|
|
501
508
|
import json
|
|
502
509
|
import os
|
|
510
|
+
import re
|
|
503
511
|
import subprocess
|
|
504
512
|
import sys
|
|
505
513
|
import urllib.error
|
|
@@ -528,7 +536,10 @@ SOURCE_ARTIFACTS = {
|
|
|
528
536
|
".ai/vcm/handoffs/test-report.md",
|
|
529
537
|
"docs/TESTING.md",
|
|
530
538
|
],
|
|
531
|
-
"code-diff": [
|
|
539
|
+
"code-diff": [
|
|
540
|
+
".ai/vcm/handoffs/test-report.md",
|
|
541
|
+
".ai/vcm/gate-reviews/validation-adequacy-review.md",
|
|
542
|
+
],
|
|
532
543
|
}
|
|
533
544
|
CODE_DIFF_SOURCE_ARTIFACTS = {
|
|
534
545
|
"coder": [
|
|
@@ -703,11 +714,14 @@ def code_diff_sources(gate_record: dict, source: str | None, code_diff: dict) ->
|
|
|
703
714
|
def source_artifacts(gate: str, sources: list[str] | None) -> list[str]:
|
|
704
715
|
if gate != "code-diff":
|
|
705
716
|
return SOURCE_ARTIFACTS[gate]
|
|
706
|
-
return list(dict.fromkeys(
|
|
717
|
+
return list(dict.fromkeys([
|
|
718
|
+
*SOURCE_ARTIFACTS["code-diff"],
|
|
719
|
+
*(
|
|
707
720
|
artifact
|
|
708
721
|
for source in (sources or [])
|
|
709
722
|
for artifact in CODE_DIFF_SOURCE_ARTIFACTS.get(source, [])
|
|
710
|
-
|
|
723
|
+
),
|
|
724
|
+
]))
|
|
711
725
|
|
|
712
726
|
|
|
713
727
|
def input_hash(root: Path, gate: str, sources: list[str] | None = None, gate_record=None) -> str:
|
|
@@ -787,6 +801,31 @@ def core_input_status(root: Path, gate: str) -> tuple[str, str] | None:
|
|
|
787
801
|
return (core_artifact, "ready")
|
|
788
802
|
|
|
789
803
|
|
|
804
|
+
def code_diff_prerequisite_error(root: Path, index: dict) -> str | None:
|
|
805
|
+
report_path = root / ".ai/vcm/handoffs/test-report.md"
|
|
806
|
+
try:
|
|
807
|
+
report = report_path.read_text()
|
|
808
|
+
except OSError:
|
|
809
|
+
return "code-diff requires completed Tester validation. .ai/vcm/handoffs/test-report.md is missing."
|
|
810
|
+
result = re.search(r"^\\s*Test Result\\s*:\\s*(pass|fail|incomplete)\\s*$", report, re.IGNORECASE | re.MULTILINE)
|
|
811
|
+
if result is None:
|
|
812
|
+
return "code-diff requires completed Tester validation. Test Result must be exactly pass or fail."
|
|
813
|
+
if result.group(1).lower() == "incomplete":
|
|
814
|
+
return "code-diff requires completed Tester validation. Test Result is incomplete."
|
|
815
|
+
|
|
816
|
+
validation = index.get("gates", {}).get("validation-adequacy", {})
|
|
817
|
+
if not isinstance(validation, dict) or not validation.get("required", False):
|
|
818
|
+
return None
|
|
819
|
+
if validation.get("status") in ("skipped", "overridden"):
|
|
820
|
+
return None
|
|
821
|
+
if validation.get("status") != "completed" or validation.get("decision") != "approve":
|
|
822
|
+
return "code-diff requires the validation-adequacy Gate to complete successfully for the current Tester evidence."
|
|
823
|
+
current_hash = input_hash(root, "validation-adequacy")
|
|
824
|
+
if not validation.get("inputHash") or validation.get("inputHash") != current_hash:
|
|
825
|
+
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval."
|
|
826
|
+
return None
|
|
827
|
+
|
|
828
|
+
|
|
790
829
|
def request_id(gate: str) -> str:
|
|
791
830
|
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
792
831
|
return f"{stamp}-{gate}-{uuid.uuid4().hex[:8]}"
|
|
@@ -883,6 +922,37 @@ def local_request(gate: str, source: str | None) -> int:
|
|
|
883
922
|
print_result("not_required", gate=gate, message=f"{core_status[0]} is {core_status[1]}.")
|
|
884
923
|
return 0
|
|
885
924
|
|
|
925
|
+
if gate == "code-diff":
|
|
926
|
+
prerequisite_error = code_diff_prerequisite_error(root, index)
|
|
927
|
+
if prerequisite_error:
|
|
928
|
+
gate_record = index["gates"].setdefault(gate, {})
|
|
929
|
+
gate_record.update({
|
|
930
|
+
"required": True,
|
|
931
|
+
"status": "failed",
|
|
932
|
+
"decision": None,
|
|
933
|
+
"error": prerequisite_error,
|
|
934
|
+
"exceptionReason": None,
|
|
935
|
+
"requestId": None,
|
|
936
|
+
"requestPath": None,
|
|
937
|
+
"inputHash": None,
|
|
938
|
+
"baseCommit": None,
|
|
939
|
+
"headCommit": None,
|
|
940
|
+
"commits": None,
|
|
941
|
+
"changedFiles": None,
|
|
942
|
+
"diffStat": None,
|
|
943
|
+
"requestedAt": None,
|
|
944
|
+
"startedAt": None,
|
|
945
|
+
"completedAt": now_iso(),
|
|
946
|
+
"callbackStatus": "not_sent",
|
|
947
|
+
"callbackError": None,
|
|
948
|
+
"updatedAt": now_iso(),
|
|
949
|
+
})
|
|
950
|
+
if index.get("activeGate") == gate:
|
|
951
|
+
index["activeGate"] = None
|
|
952
|
+
write_json(index_path, index)
|
|
953
|
+
print_result("failed_to_start", gate=gate, reason=prerequisite_error)
|
|
954
|
+
return 2
|
|
955
|
+
|
|
886
956
|
gate_record = index["gates"].get(gate, {})
|
|
887
957
|
code_diff = {}
|
|
888
958
|
if gate == "code-diff":
|