vibe-coding-master 0.9.3 → 0.9.5
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/dist/backend/server.js +2 -1
- package/dist/backend/services/auto-memory-service.js +101 -16
- package/dist/backend/services/claude-hook-service.js +31 -12
- package/dist/backend/services/gate-review-service.js +24 -5
- package/dist/backend/services/harness-feedback-service.js +21 -3
- package/dist/backend/services/message-service.js +5 -0
- package/dist/backend/services/role-context-restart-service.js +28 -4
- package/dist/backend/services/route-message-evidence.js +83 -0
- package/dist/backend/services/runtime-coordinator-service.js +1 -1
- package/dist/backend/templates/harness/gate-review.js +13 -8
- package/package.json +1 -1
package/dist/backend/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
|
8
8
|
import { readVcmMemoryHostFrame, readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
9
9
|
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, durableDocAssignmentReportPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
10
10
|
import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
11
|
+
const MEMORY_REVIEW_STOP_BLOCK_LIMIT = 3;
|
|
11
12
|
const MEMORY_FILE_DEFINITIONS = [
|
|
12
13
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
13
14
|
{ path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
|
|
@@ -436,43 +437,75 @@ export function createAutoMemoryService(deps) {
|
|
|
436
437
|
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
|
|
437
438
|
if (state) {
|
|
438
439
|
await discardActiveReview(input.taskRepoRoot, state);
|
|
439
|
-
return true;
|
|
440
|
+
return { handled: true };
|
|
440
441
|
}
|
|
441
|
-
return false;
|
|
442
|
+
return { handled: false };
|
|
442
443
|
}
|
|
443
444
|
if (state?.status === "reviewing" && !state.reviewPromptDispatchedAt) {
|
|
444
|
-
return false;
|
|
445
|
+
return { handled: false };
|
|
445
446
|
}
|
|
446
447
|
if (!state || state.status !== "reviewing") {
|
|
447
|
-
return false;
|
|
448
|
+
return { handled: false };
|
|
448
449
|
}
|
|
449
450
|
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
450
|
-
return true;
|
|
451
|
+
return { handled: true };
|
|
451
452
|
}
|
|
452
453
|
if (input.eventName === "StopFailure") {
|
|
453
454
|
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective memory review turn failed.");
|
|
454
|
-
return true;
|
|
455
|
+
return { handled: true };
|
|
455
456
|
}
|
|
456
457
|
if (input.eventName === "Stop") {
|
|
457
458
|
if (!state.retrospectiveReportPath
|
|
458
459
|
|| !(await deps.fs.pathExists(state.retrospectiveReportPath))
|
|
459
460
|
|| !(await deps.fs.readText(state.retrospectiveReportPath)).trim()) {
|
|
460
|
-
|
|
461
|
-
return true;
|
|
461
|
+
return rejectHarnessEngineerStop(input, state, "Task Harness Retrospective did not write the required retrospective report.");
|
|
462
462
|
}
|
|
463
463
|
try {
|
|
464
464
|
await recordHarnessEngineerMemoryResult(input.taskRepoRoot, state);
|
|
465
|
-
if (state.assignments.length > 0) {
|
|
466
|
-
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
467
|
-
}
|
|
468
465
|
}
|
|
469
466
|
catch (error) {
|
|
470
|
-
|
|
471
|
-
|
|
467
|
+
const message = `Harness Engineer memory result could not be recorded: ${errorMessage(error)}`;
|
|
468
|
+
if (!isFixableMemoryResultError(error)) {
|
|
469
|
+
await failReview(input.taskRepoRoot, state, message);
|
|
470
|
+
return { handled: true };
|
|
471
|
+
}
|
|
472
|
+
const hint = error instanceof VcmError && error.hint ? ` ${error.hint}` : "";
|
|
473
|
+
return rejectHarnessEngineerStop(input, state, `${message}${hint}`);
|
|
472
474
|
}
|
|
473
|
-
|
|
475
|
+
if (state.assignments.length > 0) {
|
|
476
|
+
try {
|
|
477
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
await failReview(input.taskRepoRoot, state, `Durable-document assignment could not be dispatched: ${errorMessage(error)}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { handled: true };
|
|
474
484
|
}
|
|
475
|
-
return true;
|
|
485
|
+
return { handled: true };
|
|
486
|
+
}
|
|
487
|
+
// A correctable retrospective result keeps the Harness Engineer turn alive
|
|
488
|
+
// with the validation error, a bounded number of times, so the role can fix
|
|
489
|
+
// review-result.json or the memory commit instead of the review failing
|
|
490
|
+
// silently after the turn has ended.
|
|
491
|
+
async function rejectHarnessEngineerStop(input, state, message) {
|
|
492
|
+
const blocks = state.stopBlockCount ?? 0;
|
|
493
|
+
if (!input.allowStopBlock || blocks >= MEMORY_REVIEW_STOP_BLOCK_LIMIT) {
|
|
494
|
+
await failReview(input.taskRepoRoot, state, message);
|
|
495
|
+
return { handled: true };
|
|
496
|
+
}
|
|
497
|
+
state.stopBlockCount = blocks + 1;
|
|
498
|
+
state.error = message;
|
|
499
|
+
state.updatedAt = now();
|
|
500
|
+
await persistActiveState(input.taskRepoRoot, state);
|
|
501
|
+
return {
|
|
502
|
+
handled: true,
|
|
503
|
+
stopBlockReason: [
|
|
504
|
+
message,
|
|
505
|
+
`Fix the retrospective report, ${resolveRepoPath(input.taskRepoRoot, reviewResultRelativePath(state.runId))}, or the memory commit, then end the turn again.`,
|
|
506
|
+
`Attempt ${state.stopBlockCount} of ${MEMORY_REVIEW_STOP_BLOCK_LIMIT}; after that the memory review fails.`
|
|
507
|
+
].join(" ")
|
|
508
|
+
};
|
|
476
509
|
}
|
|
477
510
|
async function getFile(_baseRepoRoot, taskRepoRoot, filePath) {
|
|
478
511
|
await assertMemoryBlocksInstalled(deps.fs, taskRepoRoot);
|
|
@@ -549,9 +582,47 @@ export function createAutoMemoryService(deps) {
|
|
|
549
582
|
statusCode: 409
|
|
550
583
|
});
|
|
551
584
|
}
|
|
585
|
+
if (await harnessEngineerResultWritten(taskRepoRoot, state)) {
|
|
586
|
+
// The Harness Engineer finished its turn, so re-validate the current
|
|
587
|
+
// result file and memory commit before discarding anything.
|
|
588
|
+
try {
|
|
589
|
+
await recordHarnessEngineerMemoryResult(taskRepoRoot, state);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (await memoryChangedSinceReview(taskRepoRoot, state)) {
|
|
593
|
+
// Reviewed memory is already committed; restarting would lose the
|
|
594
|
+
// durable-document moves recorded only in this run's result file.
|
|
595
|
+
const message = `Harness Engineer memory result could not be recorded: ${errorMessage(error)}`;
|
|
596
|
+
await failReview(taskRepoRoot, state, message);
|
|
597
|
+
throw new VcmError({
|
|
598
|
+
code: "MEMORY_REVIEW_RESULT_STILL_INVALID",
|
|
599
|
+
message,
|
|
600
|
+
statusCode: 409,
|
|
601
|
+
hint: `Correct ${resolveRepoPath(taskRepoRoot, reviewResultRelativePath(state.runId))} or the memory commit, then retry.`
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
await clearActiveState(taskRepoRoot);
|
|
605
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
606
|
+
}
|
|
607
|
+
if (state.assignments.length > 0) {
|
|
608
|
+
await dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state);
|
|
609
|
+
}
|
|
610
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
611
|
+
}
|
|
552
612
|
await clearActiveState(taskRepoRoot);
|
|
553
613
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
554
614
|
}
|
|
615
|
+
async function harnessEngineerResultWritten(taskRepoRoot, state) {
|
|
616
|
+
if (!state.reviewBaseCommit || !state.reviewPromptDispatchedAt) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
return deps.fs.pathExists(resolveRepoPath(taskRepoRoot, reviewResultRelativePath(state.runId)));
|
|
620
|
+
}
|
|
621
|
+
async function memoryChangedSinceReview(taskRepoRoot, state) {
|
|
622
|
+
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
623
|
+
const after = await readMemorySet(taskRepoRoot);
|
|
624
|
+
return MEMORY_FILE_DEFINITIONS.some((definition) => before[definition.path] !== after[definition.path]);
|
|
625
|
+
}
|
|
555
626
|
async function retryDurableDocAssignment(baseRepoRoot, taskRepoRoot, assignmentId) {
|
|
556
627
|
const state = await loadActiveState(taskRepoRoot);
|
|
557
628
|
const assignment = state && (state.status === "documenting" || state.status === "failed")
|
|
@@ -996,7 +1067,7 @@ export function createAutoMemoryService(deps) {
|
|
|
996
1067
|
await persistActiveState(taskRepoRoot, state);
|
|
997
1068
|
}
|
|
998
1069
|
async function readAndValidateHarnessMemoryReviewResult(taskRepoRoot, state, before, after, currentHead, changedMemoryPaths) {
|
|
999
|
-
const resultPath = resolveRepoPath(taskRepoRoot,
|
|
1070
|
+
const resultPath = resolveRepoPath(taskRepoRoot, reviewResultRelativePath(state.runId));
|
|
1000
1071
|
if (!(await deps.fs.pathExists(resultPath))) {
|
|
1001
1072
|
throw new VcmError({
|
|
1002
1073
|
code: "MEMORY_REVIEW_RESULT_MISSING",
|
|
@@ -1791,6 +1862,20 @@ function ensureTrailingNewline(content) {
|
|
|
1791
1862
|
function sha256(content) {
|
|
1792
1863
|
return createHash("sha256").update(content).digest("hex");
|
|
1793
1864
|
}
|
|
1865
|
+
function reviewResultRelativePath(runId) {
|
|
1866
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/review-result.json`;
|
|
1867
|
+
}
|
|
1868
|
+
// Errors the Harness Engineer can correct in its own output: the result file,
|
|
1869
|
+
// the memory edit, or the memory commit. A malformed result file surfaces as a
|
|
1870
|
+
// JSON SyntaxError.
|
|
1871
|
+
function isFixableMemoryResultError(error) {
|
|
1872
|
+
if (error instanceof SyntaxError) {
|
|
1873
|
+
return true;
|
|
1874
|
+
}
|
|
1875
|
+
return error instanceof VcmError
|
|
1876
|
+
&& error.code.startsWith("MEMORY_")
|
|
1877
|
+
&& error.code !== "MEMORY_REVIEW_BASE_MISSING";
|
|
1878
|
+
}
|
|
1794
1879
|
function errorMessage(error) {
|
|
1795
1880
|
return error instanceof Error ? error.message : String(error);
|
|
1796
1881
|
}
|
|
@@ -114,29 +114,30 @@ export function createClaudeHookService(deps) {
|
|
|
114
114
|
dispatchedCount: 0
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
async function processHarnessEngineerHook(input) {
|
|
117
|
+
async function processHarnessEngineerHook(input, options) {
|
|
118
118
|
const eventName = parseBusinessHookEvent(input.event.hook_event_name);
|
|
119
119
|
const context = await getProjectToolHookContext("Harness Engineer");
|
|
120
120
|
const projectScoped = input.taskSlug === "__project_harness_engineer__";
|
|
121
|
-
const
|
|
122
|
-
?
|
|
123
|
-
eventName,
|
|
121
|
+
const recordSessionEvent = (recordedEventName) => projectScoped
|
|
122
|
+
? deps.sessionService.recordProjectHarnessEngineerHookEvent(context.project.repoRoot, {
|
|
123
|
+
eventName: recordedEventName,
|
|
124
124
|
sessionId: stringOrUndefined(input.event.session_id),
|
|
125
125
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
126
126
|
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
|
|
127
127
|
runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
|
|
128
128
|
runtimeSessionToken: input.runtimeSessionToken
|
|
129
129
|
})
|
|
130
|
-
:
|
|
130
|
+
: deps.sessionService.recordRoleHookEvent(context.project.repoRoot, {
|
|
131
131
|
taskSlug: input.taskSlug,
|
|
132
132
|
role: input.role,
|
|
133
|
-
eventName,
|
|
133
|
+
eventName: recordedEventName,
|
|
134
134
|
sessionId: stringOrUndefined(input.event.session_id),
|
|
135
135
|
transcriptPath: stringOrUndefined(input.event.transcript_path),
|
|
136
136
|
cwd: stringOrUndefined(input.event.cwd) ?? stringOrUndefined(input.event.new_cwd),
|
|
137
137
|
runtimeSessionId: stringOrUndefined(input.event.vcm_runtime_session_id),
|
|
138
138
|
runtimeSessionToken: input.runtimeSessionToken
|
|
139
139
|
});
|
|
140
|
+
const session = await recordSessionEvent(eventName);
|
|
140
141
|
if (!session) {
|
|
141
142
|
return completedHookResult(input, eventName);
|
|
142
143
|
}
|
|
@@ -144,14 +145,32 @@ export function createClaudeHookService(deps) {
|
|
|
144
145
|
? (await deps.taskService.listTasks(context.project.repoRoot))
|
|
145
146
|
.find((task) => task.cleanupStatus !== "cleaned" && (projectScoped || task.taskSlug === input.taskSlug))
|
|
146
147
|
: undefined;
|
|
147
|
-
const
|
|
148
|
+
const memoryResult = activeTask
|
|
148
149
|
? await deps.autoMemoryService?.handleHarnessEngineerHook({
|
|
149
150
|
baseRepoRoot: context.project.repoRoot,
|
|
150
151
|
taskRepoRoot: getTaskRuntimeRepoRoot(activeTask),
|
|
151
152
|
taskSlug: activeTask.taskSlug,
|
|
152
|
-
eventName
|
|
153
|
+
eventName,
|
|
154
|
+
allowStopBlock: options.allowBlock && eventName === "Stop"
|
|
153
155
|
})
|
|
154
|
-
:
|
|
156
|
+
: undefined;
|
|
157
|
+
if (memoryResult?.stopBlockReason) {
|
|
158
|
+
// The memory review result is still correctable: keep the Harness
|
|
159
|
+
// Engineer turn alive and skip retrospective completion until it passes.
|
|
160
|
+
// The turn continues, so the session must not stay recorded as idle and
|
|
161
|
+
// receive queued input meanwhile.
|
|
162
|
+
await recordSessionEvent("UserPromptSubmit");
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
eventName,
|
|
166
|
+
taskSlug: activeTask?.taskSlug ?? input.taskSlug,
|
|
167
|
+
role: input.role,
|
|
168
|
+
sessionUpdated: Boolean(session),
|
|
169
|
+
dispatchedCount: 0,
|
|
170
|
+
stopDecision: { behavior: "block", reason: memoryResult.stopBlockReason }
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const memoryHandled = memoryResult?.handled ?? false;
|
|
155
174
|
const memoryState = activeTask && deps.autoMemoryService
|
|
156
175
|
? await deps.autoMemoryService.getState(context.project.repoRoot, getTaskRuntimeRepoRoot(activeTask))
|
|
157
176
|
: undefined;
|
|
@@ -631,7 +650,7 @@ export function createClaudeHookService(deps) {
|
|
|
631
650
|
});
|
|
632
651
|
if (eventName === "Stop") {
|
|
633
652
|
const memoryState = await deps.autoMemoryService.getState(context.project.repoRoot, context.taskRepoRoot);
|
|
634
|
-
await deps.harnessFeedbackService?.completeWaitingTaskRetrospective(context.project.repoRoot, context.taskSlug, memoryState
|
|
653
|
+
await deps.harnessFeedbackService?.completeWaitingTaskRetrospective(context.project.repoRoot, context.taskSlug, memoryState);
|
|
635
654
|
}
|
|
636
655
|
return {
|
|
637
656
|
ok: true,
|
|
@@ -908,7 +927,7 @@ export function createClaudeHookService(deps) {
|
|
|
908
927
|
return processTranslatorHook(input);
|
|
909
928
|
}
|
|
910
929
|
if (isHarnessEngineerToolRoleName(input.role)) {
|
|
911
|
-
return processHarnessEngineerHook(input);
|
|
930
|
+
return processHarnessEngineerHook(input, { allowBlock: false });
|
|
912
931
|
}
|
|
913
932
|
if (eventName === "UserPromptSubmit") {
|
|
914
933
|
return handleUserPromptSubmitHook(input);
|
|
@@ -930,7 +949,7 @@ export function createClaudeHookService(deps) {
|
|
|
930
949
|
return processTranslatorHook(input);
|
|
931
950
|
}
|
|
932
951
|
if (isHarnessEngineerToolRoleName(input.role)) {
|
|
933
|
-
return processHarnessEngineerHook(input);
|
|
952
|
+
return processHarnessEngineerHook(input, { allowBlock: true });
|
|
934
953
|
}
|
|
935
954
|
return processStopHook(input, { allowBlock: true });
|
|
936
955
|
});
|
|
@@ -7,6 +7,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
|
7
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
8
8
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
9
9
|
import { parseGateReviewReportArtifact } from "./managed-artifact-validation.js";
|
|
10
|
+
import { collectFlowRouteMessages, renderRouteMessages } from "./route-message-evidence.js";
|
|
10
11
|
const REVIEWER_AGENT_PATH = ".claude/agents/reviewer.md";
|
|
11
12
|
const GATE_REVIEW_DIR = ".ai/vcm/gate-reviews";
|
|
12
13
|
const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
|
|
@@ -39,10 +40,7 @@ const CODE_DIFF_SOURCE_ARTIFACTS = {
|
|
|
39
40
|
".ai/vcm/handoffs/architecture-plan.md",
|
|
40
41
|
".ai/vcm/handoffs/coder-completion.md"
|
|
41
42
|
],
|
|
42
|
-
"architect-debug": [
|
|
43
|
-
".ai/vcm/handoffs/role-commands/architect.md",
|
|
44
|
-
".ai/vcm/handoffs/architect-debug.md"
|
|
45
|
-
],
|
|
43
|
+
"architect-debug": [".ai/vcm/handoffs/architect-debug.md"],
|
|
46
44
|
"architect-diagnosis": [
|
|
47
45
|
".ai/vcm/handoffs/architecture-diagnosis.md"
|
|
48
46
|
]
|
|
@@ -438,6 +436,9 @@ export function createGateReviewService(deps) {
|
|
|
438
436
|
const promptPath = path.posix.join(REQUESTS_DIR, `${requestId}.prompt.md`);
|
|
439
437
|
const requestReportPath = reportPathForRequest(requestId);
|
|
440
438
|
const inputSnapshots = await captureGateInputSnapshots(deps.fs, context.taskRepoRoot, requestId, getSourceArtifacts(gate, codeDiffSources));
|
|
439
|
+
if (gate === "code-diff" && codeDiffSources) {
|
|
440
|
+
inputSnapshots.push(await captureRouteMessageSnapshot(deps, context, requestId, codeDiffSources));
|
|
441
|
+
}
|
|
441
442
|
const nextRecord = {
|
|
442
443
|
...record,
|
|
443
444
|
status: "running",
|
|
@@ -1487,7 +1488,12 @@ function splitLines(value) {
|
|
|
1487
1488
|
function buildGatePrompt(context, gate, requestId, codeDiffInput, codeDiffSources, inputSnapshots = []) {
|
|
1488
1489
|
const reportPath = reportPathForRequest(requestId);
|
|
1489
1490
|
const absoluteReportPath = resolveRepoPath(context.taskRepoRoot, reportPath);
|
|
1490
|
-
const evidence =
|
|
1491
|
+
const evidence = [
|
|
1492
|
+
...getSourceArtifacts(gate, codeDiffSources),
|
|
1493
|
+
...(gate === "code-diff"
|
|
1494
|
+
? [`${path.posix.join(context.stateRoot, "messages", `${context.taskSlug}.jsonl`)} (PM route messages for the reviewed flow)`]
|
|
1495
|
+
: [])
|
|
1496
|
+
]
|
|
1491
1497
|
.map((relativePath) => `- ${relativePath}`)
|
|
1492
1498
|
.join("\n");
|
|
1493
1499
|
const capturedEvidence = inputSnapshots.length > 0
|
|
@@ -1561,6 +1567,19 @@ async function captureGateInputSnapshots(fs, taskRepoRoot, requestId, sourcePath
|
|
|
1561
1567
|
}
|
|
1562
1568
|
return snapshots;
|
|
1563
1569
|
}
|
|
1570
|
+
// The reviewer judges authorization and scope against the flow's PM route
|
|
1571
|
+
// messages, captured like any other immutable Gate input.
|
|
1572
|
+
async function captureRouteMessageSnapshot(deps, context, requestId, codeDiffSources) {
|
|
1573
|
+
const sourcePath = path.posix.join(context.stateRoot, "messages", `${context.taskSlug}.jsonl`);
|
|
1574
|
+
const roles = new Set(codeDiffSources.map((source) => source === "coder" ? "coder" : "architect"));
|
|
1575
|
+
const flow = await collectFlowRouteMessages(deps.fs, deps.workflowControlService, context, roles);
|
|
1576
|
+
if (flow.messages.length === 0) {
|
|
1577
|
+
return { sourcePath, status: "missing" };
|
|
1578
|
+
}
|
|
1579
|
+
const snapshotPath = inputSnapshotPathForRequest(requestId, ".ai/vcm/messages/route-messages.md");
|
|
1580
|
+
await deps.fs.writeText(resolveRepoPath(context.taskRepoRoot, snapshotPath), renderRouteMessages(context.taskSlug, [`Code sources: ${codeDiffSources.join(" -> ")}`], flow));
|
|
1581
|
+
return { sourcePath, snapshotPath, status: "captured" };
|
|
1582
|
+
}
|
|
1564
1583
|
function isTaskEvidencePath(sourcePath) {
|
|
1565
1584
|
return sourcePath.startsWith(".ai/vcm/handoffs/")
|
|
1566
1585
|
|| sourcePath.startsWith(".ai/vcm/gate-reviews/");
|
|
@@ -175,6 +175,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
175
175
|
...processedMarker,
|
|
176
176
|
status: "failed",
|
|
177
177
|
durableDocFailure: input.memoryReviewHasAssignments,
|
|
178
|
+
memoryReviewFailure: true,
|
|
178
179
|
failedAt: timestamp,
|
|
179
180
|
updatedAt: timestamp,
|
|
180
181
|
error: input.memoryReviewError
|
|
@@ -194,15 +195,28 @@ export function createHarnessFeedbackService(deps) {
|
|
|
194
195
|
await completeTaskRetrospective(repoRoot, processedMarker);
|
|
195
196
|
return true;
|
|
196
197
|
}
|
|
197
|
-
async function completeWaitingTaskRetrospective(repoRoot, taskSlug,
|
|
198
|
+
async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memory) {
|
|
198
199
|
const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
199
|
-
|
|
200
|
+
// A memory review that failed validation recovers only when a retry
|
|
201
|
+
// re-validated and applied that same run; a discarded run never does.
|
|
202
|
+
const memoryReviewRecovered = Boolean(marker?.memoryReviewFailure
|
|
203
|
+
&& marker.memoryRunId
|
|
204
|
+
&& memory.runs.some((run) => run.runId === marker.memoryRunId && run.status === "applied"));
|
|
205
|
+
if (!marker
|
|
206
|
+
|| (marker.status !== "waiting-docs"
|
|
207
|
+
&& !(marker.status === "failed" && (marker.durableDocFailure || memoryReviewRecovered)))) {
|
|
200
208
|
return false;
|
|
201
209
|
}
|
|
210
|
+
const memoryStatus = memory.status;
|
|
202
211
|
if (memoryStatus === "documenting") {
|
|
203
212
|
if (marker.status === "failed") {
|
|
204
213
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
205
|
-
...marker,
|
|
214
|
+
...marker,
|
|
215
|
+
status: "waiting-docs",
|
|
216
|
+
memoryReviewFailure: undefined,
|
|
217
|
+
updatedAt: now(),
|
|
218
|
+
error: undefined,
|
|
219
|
+
failedAt: undefined
|
|
206
220
|
});
|
|
207
221
|
}
|
|
208
222
|
return true;
|
|
@@ -235,6 +249,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
235
249
|
...processedMarker,
|
|
236
250
|
status: "completed",
|
|
237
251
|
durableDocFailure: undefined,
|
|
252
|
+
memoryReviewFailure: undefined,
|
|
238
253
|
error: undefined,
|
|
239
254
|
failedAt: undefined,
|
|
240
255
|
completedAt: timestamp,
|
|
@@ -408,8 +423,11 @@ export function createHarnessFeedbackService(deps) {
|
|
|
408
423
|
`Write the complete machine-readable review to: ${memoryReview.reviewResultPath}`,
|
|
409
424
|
"The JSON root must be: {\"version\":1,\"runId\":\"<assigned run id>\",\"memoryCommit\":\"<full commit or none>\",\"decisions\":[],\"durableDocAssignments\":[]}.",
|
|
410
425
|
"Each decision must contain itemId, source (existing|proposal), target, entry, decision, reason, impactIfAbsent, evidence (non-empty array), finalContent, and durableDocPath.",
|
|
426
|
+
"Every decision field is a non-empty string except evidence. Use \"none\" for finalContent when the decision keeps no memory text (remove, reject, move-to-durable-doc), and for durableDocPath unless the decision is move-to-durable-doc, where it must equal the assignment targetPath.",
|
|
427
|
+
"For retain, update, keep-in-memory, and keep-memory-reference, finalContent must be the exact memory text now present in the <VCM-memory> block; an update's finalContent must be one complete semantic entry as it now appears.",
|
|
411
428
|
"Existing decisions use retain|update|remove|move-to-durable-doc. Add or Update proposal decisions use keep-in-memory|keep-memory-reference|move-to-durable-doc|reject; Remove proposal decisions use remove|retain. Proposal itemId must equal the assigned candidate ID.",
|
|
412
429
|
"Each durableDocAssignment must contain sourceMemoryPath, sourceEntry, targetPath, content, reason, and evidence (non-empty array). Use a project-relative Markdown target outside .ai/vcm.",
|
|
430
|
+
"VCM validates review-result.json and the memory commit when you end the turn. If validation fails, the Stop is rejected with the error; correct the result file or the memory commit and end the turn again.",
|
|
413
431
|
"In the retrospective report, include only this memory summary:",
|
|
414
432
|
"",
|
|
415
433
|
"## Memory Review",
|
|
@@ -527,6 +527,11 @@ function arraysEqual(left, right) {
|
|
|
527
527
|
}
|
|
528
528
|
return left.every((value, index) => value === right[index]);
|
|
529
529
|
}
|
|
530
|
+
// Reads the task's message log outside the service's task lock, for consumers
|
|
531
|
+
// that only need a point-in-time view, such as Gate Review evidence capture.
|
|
532
|
+
export function readTaskMessages(fs, input) {
|
|
533
|
+
return readLatestMessages(fs, getMessagesPath(getStateRepoRoot(input), input.stateRoot, input.taskSlug));
|
|
534
|
+
}
|
|
530
535
|
async function readLatestMessages(fs, messagesPath) {
|
|
531
536
|
if (!(await fs.pathExists(messagesPath))) {
|
|
532
537
|
return [];
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { VCM_ROLE_NAMES } from "../../shared/constants.js";
|
|
3
3
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
4
4
|
import { toVcmError, VcmError } from "../errors.js";
|
|
5
|
+
import { collectFlowRouteMessages, renderRouteMessages } from "./route-message-evidence.js";
|
|
5
6
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
6
7
|
const STATE_DIR = "restart-with-context";
|
|
7
8
|
export function createRoleContextRestartService(deps) {
|
|
@@ -126,7 +127,11 @@ export function createRoleContextRestartService(deps) {
|
|
|
126
127
|
]);
|
|
127
128
|
const handoffDir = task.handoffDir;
|
|
128
129
|
const stateRoot = config.stateRoot;
|
|
129
|
-
const
|
|
130
|
+
const routeMessagesPath = await writeRouteMessages(repoRoot, task, stateRoot, role);
|
|
131
|
+
const files = [
|
|
132
|
+
...(routeMessagesPath ? [routeMessagesPath] : []),
|
|
133
|
+
...roleContextFiles(role, handoffDir, stateRoot)
|
|
134
|
+
];
|
|
130
135
|
return {
|
|
131
136
|
system: [
|
|
132
137
|
`This fresh ${role} session continues the current VCM task after Restart With Context.`,
|
|
@@ -140,6 +145,28 @@ export function createRoleContextRestartService(deps) {
|
|
|
140
145
|
user: `[VCM RESTART WITH CONTEXT]\nRestore the current ${role} work from the listed task artifacts and continue the accepted assignment.`
|
|
141
146
|
};
|
|
142
147
|
}
|
|
148
|
+
// A delivery role's assignment lives only in the PM route messages, so render
|
|
149
|
+
// the current flow run's messages to that role for the fresh session.
|
|
150
|
+
async function writeRouteMessages(repoRoot, task, stateRoot, role) {
|
|
151
|
+
if (role !== "architect" && role !== "coder" && role !== "tester") {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const taskRepoRoot = getTaskRuntimeRepoRoot(task);
|
|
155
|
+
const context = {
|
|
156
|
+
repoRoot,
|
|
157
|
+
taskRepoRoot,
|
|
158
|
+
stateRoot,
|
|
159
|
+
handoffDir: task.handoffDir,
|
|
160
|
+
taskSlug: task.taskSlug
|
|
161
|
+
};
|
|
162
|
+
const flow = await collectFlowRouteMessages(deps.fs, deps.workflowControlService, context, new Set([role]));
|
|
163
|
+
if (flow.messages.length === 0) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const relativePath = path.posix.join(stateRoot, STATE_DIR, `${role}-route-messages.md`);
|
|
167
|
+
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, relativePath), renderRouteMessages(task.taskSlug, [`Role: ${role}`], flow));
|
|
168
|
+
return relativePath;
|
|
169
|
+
}
|
|
143
170
|
async function persist(repoRoot, state) {
|
|
144
171
|
await deps.fs.writeJsonAtomic(await statePath(repoRoot, state.taskSlug, state.role), state);
|
|
145
172
|
pending.set(restartKey(repoRoot, state.taskSlug, state.role), state);
|
|
@@ -205,7 +232,6 @@ function roleContextFiles(role, handoffDir, stateRoot) {
|
|
|
205
232
|
];
|
|
206
233
|
case "architect":
|
|
207
234
|
return [
|
|
208
|
-
`${handoffDir}/role-commands/architect.md`,
|
|
209
235
|
`${handoffDir}/architecture-brief.md`,
|
|
210
236
|
`${handoffDir}/architecture-evidence.md`,
|
|
211
237
|
`${handoffDir}/planning-progress.md`,
|
|
@@ -216,14 +242,12 @@ function roleContextFiles(role, handoffDir, stateRoot) {
|
|
|
216
242
|
];
|
|
217
243
|
case "coder":
|
|
218
244
|
return [
|
|
219
|
-
`${handoffDir}/role-commands/coder.md`,
|
|
220
245
|
`${handoffDir}/architecture-plan.md`,
|
|
221
246
|
`${handoffDir}/coder-completion.md`,
|
|
222
247
|
".ai/vcm/coder-workers/"
|
|
223
248
|
];
|
|
224
249
|
case "tester":
|
|
225
250
|
return [
|
|
226
|
-
`${handoffDir}/role-commands/tester.md`,
|
|
227
251
|
`${handoffDir}/architecture-plan.md`,
|
|
228
252
|
`${handoffDir}/coder-completion.md`,
|
|
229
253
|
`${handoffDir}/test-report.md`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readTaskMessages } from "./message-service.js";
|
|
2
|
+
// PM route messages are the operative instructions for a role: the dispatch
|
|
3
|
+
// that started the current flow run and every later relay of a user decision.
|
|
4
|
+
// They exist only in the task message log, so callers render this slice of it.
|
|
5
|
+
export async function collectFlowRouteMessages(fs, workflowControlService, context, roles) {
|
|
6
|
+
const messages = await readTaskMessages(fs, {
|
|
7
|
+
repoRoot: context.repoRoot,
|
|
8
|
+
stateRepoRoot: context.taskRepoRoot,
|
|
9
|
+
stateRoot: context.stateRoot,
|
|
10
|
+
taskSlug: context.taskSlug
|
|
11
|
+
});
|
|
12
|
+
const flowRunStart = await findFlowRunStart(workflowControlService, context);
|
|
13
|
+
const acceptedPmMessages = messages
|
|
14
|
+
.filter((message) => message.fromRole === "project-manager" && message.acceptedAt)
|
|
15
|
+
.sort((left, right) => left.acceptedAt.localeCompare(right.acceptedAt));
|
|
16
|
+
// A PM message is accepted immediately before Workflow Progress confirms its
|
|
17
|
+
// dispatch, so the flow's starting message is the last one accepted at or
|
|
18
|
+
// before the first confirmation of the flow run.
|
|
19
|
+
const windowStart = flowRunStart
|
|
20
|
+
? acceptedPmMessages.filter((message) => message.acceptedAt <= flowRunStart).at(-1)?.acceptedAt ?? flowRunStart
|
|
21
|
+
: undefined;
|
|
22
|
+
return {
|
|
23
|
+
windowStart,
|
|
24
|
+
messages: messages
|
|
25
|
+
.filter((message) => message.fromRole === "user"
|
|
26
|
+
? !windowStart || message.createdAt >= windowStart
|
|
27
|
+
: message.fromRole === "project-manager"
|
|
28
|
+
&& roles.has(message.toRole)
|
|
29
|
+
&& Boolean(message.acceptedAt)
|
|
30
|
+
&& (!windowStart || message.acceptedAt >= windowStart))
|
|
31
|
+
.sort((left, right) => messageTime(left).localeCompare(messageTime(right)))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function renderRouteMessages(taskSlug, header, flow) {
|
|
35
|
+
return [
|
|
36
|
+
"# PM Route Messages",
|
|
37
|
+
"",
|
|
38
|
+
`Task: ${taskSlug}`,
|
|
39
|
+
...header,
|
|
40
|
+
flow.windowStart
|
|
41
|
+
? `Scope: messages since the current flow run started at ${flow.windowStart}`
|
|
42
|
+
: "Scope: the whole task message log; the current flow run start was unavailable",
|
|
43
|
+
"",
|
|
44
|
+
"PM route messages to the listed roles and user messages to PM, oldest first.",
|
|
45
|
+
"A later message overrides an earlier one where they conflict.",
|
|
46
|
+
...flow.messages.flatMap((message, index) => [
|
|
47
|
+
"",
|
|
48
|
+
`## ${index + 1}. ${message.fromRole} -> ${message.toRole} (${message.type})`,
|
|
49
|
+
"",
|
|
50
|
+
`Message: ${message.id}`,
|
|
51
|
+
`Time: ${messageTime(message)}`,
|
|
52
|
+
...(message.artifactRefs.length > 0 ? [`Artifacts: ${message.artifactRefs.join(", ")}`] : []),
|
|
53
|
+
"",
|
|
54
|
+
message.body.trimEnd()
|
|
55
|
+
]),
|
|
56
|
+
""
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
async function findFlowRunStart(workflowControlService, context) {
|
|
60
|
+
if (!workflowControlService) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const workflowContext = {
|
|
65
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
66
|
+
stateRoot: context.stateRoot,
|
|
67
|
+
handoffDir: context.handoffDir,
|
|
68
|
+
taskSlug: context.taskSlug
|
|
69
|
+
};
|
|
70
|
+
const [state, progress] = await Promise.all([
|
|
71
|
+
workflowControlService.getState(workflowContext),
|
|
72
|
+
workflowControlService.getProgress(workflowContext)
|
|
73
|
+
]);
|
|
74
|
+
const startedAtSequence = state.flowRun?.startedAtSequence;
|
|
75
|
+
return progress.history.find((entry) => entry.sequence === startedAtSequence)?.confirmedAt;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function messageTime(message) {
|
|
82
|
+
return message.acceptedAt ?? message.createdAt;
|
|
83
|
+
}
|
|
@@ -59,7 +59,7 @@ export function createRuntimeCoordinatorService(deps) {
|
|
|
59
59
|
await deps.translationService.stopTask(taskRepoRoot, activeTask.taskSlug).catch(() => undefined);
|
|
60
60
|
}
|
|
61
61
|
const memoryState = await reconcileAutoMemory(repoRoot, activeTask, preferences.autoTaskHarnessReviewEnabled ? "auto" : undefined);
|
|
62
|
-
await deps.harnessFeedbackService.completeWaitingTaskRetrospective(repoRoot, activeTask.taskSlug, memoryState
|
|
62
|
+
await deps.harnessFeedbackService.completeWaitingTaskRetrospective(repoRoot, activeTask.taskSlug, memoryState);
|
|
63
63
|
const memoryReadiness = await getTaskRetrospectiveMemoryReadiness(repoRoot, activeTask);
|
|
64
64
|
if ((preferences.autoTaskHarnessReviewEnabled || memoryReadiness.trigger) && memoryReadiness.ready) {
|
|
65
65
|
await maybeStartTaskHarnessRetrospective(repoRoot, activeTask, memoryReadiness.trigger ?? "auto");
|
|
@@ -334,6 +334,13 @@ Treat every production and test hunk introduced by \`architect-debug\` or
|
|
|
334
334
|
review to confirming that the reported symptom disappeared or that the recorded
|
|
335
335
|
root cause was addressed; apply the complete code-diff checks to every repair.
|
|
336
336
|
|
|
337
|
+
When the VCM prompt captures PM route messages, they are the operative
|
|
338
|
+
instructions for the reviewed flow: the dispatch that started it and every
|
|
339
|
+
later PM relay of a user decision, oldest first. A later message, such as a user grant, a narrowed scope, or a
|
|
340
|
+
changed acceptance target, overrides the plan or an earlier message where they
|
|
341
|
+
conflict. Judge authorization and scope against them, and record under Code
|
|
342
|
+
Diff Analysis any change they neither request nor permit.
|
|
343
|
+
|
|
337
344
|
Before deciding:
|
|
338
345
|
|
|
339
346
|
- Inspect every changed file and diff hunk. Read the complete implementation of
|
|
@@ -351,7 +358,7 @@ Before deciding:
|
|
|
351
358
|
Do not satisfy review by repeating a generic checklist.
|
|
352
359
|
|
|
353
360
|
For \`coder\`, compare the commits with the approved architecture plan,
|
|
354
|
-
scaffold,
|
|
361
|
+
scaffold, coder completion evidence, and the captured PM route messages. Verify that the complete planned
|
|
355
362
|
behavior is implemented without changing architect-owned boundaries or
|
|
356
363
|
contracts.
|
|
357
364
|
|
|
@@ -362,8 +369,8 @@ the Tester role, the repair or coverage evidence, and
|
|
|
362
369
|
because it is not a Coder scaffold item. Verify that Tester changes remain
|
|
363
370
|
test-only, preserve real behavior paths, and are committed and validated.
|
|
364
371
|
|
|
365
|
-
For \`architect-debug\`, compare the commits with the
|
|
366
|
-
|
|
372
|
+
For \`architect-debug\`, compare the commits with the captured PM route messages
|
|
373
|
+
to Architect and \`.ai/vcm/handoffs/architect-debug.md\`. Verify that the confirmed
|
|
367
374
|
root cause is supported by the code, the implementation fixes that cause rather
|
|
368
375
|
than only its surface symptom, temporary diagnostics are removed, and affected
|
|
369
376
|
callers, contracts, and tests are updated. Verify that the Debug evidence records
|
|
@@ -372,7 +379,8 @@ when an applicable check was not run, did not pass, or does not exercise that
|
|
|
372
379
|
failure path.
|
|
373
380
|
|
|
374
381
|
For \`architect-diagnosis\`, compare the commits with
|
|
375
|
-
\`.ai/vcm/handoffs/architecture-diagnosis.md
|
|
382
|
+
\`.ai/vcm/handoffs/architecture-diagnosis.md\` and the captured PM route messages
|
|
383
|
+
to Architect, and verify that the commits implement the diagnosed
|
|
376
384
|
ownership, data flow, lifecycle, boundaries, invariants, and failure model.
|
|
377
385
|
Request changes when the architecture problem remains, the required direction
|
|
378
386
|
is contradicted, or the implementation is only a local workaround for the surface failure.
|
|
@@ -599,10 +607,7 @@ const CODE_DIFF_SOURCE_ARTIFACTS = {
|
|
|
599
607
|
".ai/vcm/handoffs/architecture-plan.md",
|
|
600
608
|
".ai/vcm/handoffs/coder-completion.md"
|
|
601
609
|
],
|
|
602
|
-
"architect-debug": [
|
|
603
|
-
".ai/vcm/handoffs/role-commands/architect.md",
|
|
604
|
-
".ai/vcm/handoffs/architect-debug.md"
|
|
605
|
-
],
|
|
610
|
+
"architect-debug": [".ai/vcm/handoffs/architect-debug.md"],
|
|
606
611
|
"architect-diagnosis": [".ai/vcm/handoffs/architecture-diagnosis.md"]
|
|
607
612
|
};
|
|
608
613
|
const CORE_INPUT_ARTIFACTS = {
|