vibe-coding-master 0.7.43 → 0.7.45
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 +64 -39
- package/dist/backend/adapters/claude-adapter.js +2 -2
- package/dist/backend/adapters/codex-bridge-adapter.js +213 -0
- package/dist/backend/api/app-settings-routes.js +6 -6
- package/dist/backend/api/artifact-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +16 -0
- package/dist/backend/api/task-routes.js +1 -0
- package/dist/backend/api/workflow-control-routes.js +12 -0
- package/dist/backend/cli/install-vcm-harness.js +21 -0
- package/dist/backend/server.js +11 -9
- package/dist/backend/services/app-settings-service.js +11 -11
- package/dist/backend/services/artifact-service.js +7 -30
- package/dist/backend/services/auto-memory-service.js +640 -12
- package/dist/backend/services/claude-hook-service.js +80 -2
- package/dist/backend/services/{ccr-integration-service.js → codex-bridge-integration-service.js} +71 -71
- package/dist/backend/services/gate-review-service.js +173 -74
- package/dist/backend/services/harness-feedback-service.js +76 -78
- package/dist/backend/services/harness-service.js +27 -3
- package/dist/backend/services/runtime-coordinator-service.js +2 -1
- package/dist/backend/services/session-service.js +16 -16
- package/dist/backend/services/status-service.js +1 -0
- package/dist/backend/services/translation-worker-service.js +19 -4
- package/dist/backend/services/usage-analytics-service.js +3 -3
- package/dist/backend/services/workflow-control-service.js +96 -12
- package/dist/backend/templates/handoff.js +44 -2
- package/dist/backend/templates/harness/architect-agent.js +13 -7
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +1 -1
- package/dist/backend/templates/harness/check-scaffold-ledger.js +234 -10
- package/dist/backend/templates/harness/claude-root.js +3 -2
- package/dist/backend/templates/harness/coder-agent.js +8 -0
- package/dist/backend/templates/harness/gate-review.js +144 -49
- package/dist/backend/templates/harness/harness-engineer-agent.js +25 -10
- package/dist/backend/templates/harness/project-manager-agent.js +16 -10
- package/dist/backend/templates/harness/resolve-durable-doc-assignment.js +60 -0
- package/dist/backend/templates/harness/tester-agent.js +13 -0
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +82 -0
- package/dist/backend/templates/harness/vcm-code-navigation-skill.js +7 -5
- package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +1 -1
- package/dist/shared/types/session.js +28 -22
- package/dist/shared/types/workflow.js +1 -0
- package/dist/shared/validation/artifact-check.js +3 -3
- package/dist/shared/validation/artifact-contract.js +1 -1
- package/dist/shared/validation/artifact-registry.js +16 -0
- package/dist-frontend/assets/index-DDmygnV6.css +32 -0
- package/dist-frontend/assets/{index-VW9tYPP5.js → index-nAV6toi8.js} +47 -47
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/claude-plugins/vcm-lsp-bridge/.claude-plugin/plugin.json +21 -6
- package/scripts/{ccr-api-key-helper.mjs → codex-bridge-api-key-helper.mjs} +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -2
- package/scripts/harness-tools/vcm-bash-guard +1 -1
- package/dist/backend/adapters/ccr-gateway-adapter.js +0 -172
- package/dist-frontend/assets/index-B0d4Z6ny.css +0 -32
|
@@ -10,6 +10,7 @@ const REVIEWER_AGENT_PATH = ".claude/agents/reviewer.md";
|
|
|
10
10
|
const GATE_REVIEW_DIR = ".ai/vcm/gate-reviews";
|
|
11
11
|
const REQUESTS_DIR = ".ai/vcm/gate-reviews/requests";
|
|
12
12
|
const GATE_REVIEW_VERSION = 1;
|
|
13
|
+
const HARNESS_COMMIT_PREFIX = "[VCM Harness] ";
|
|
13
14
|
const REVIEWER_ROLE = "reviewer";
|
|
14
15
|
const DEFAULT_REPORT_POLL_INTERVAL_MS = 1000;
|
|
15
16
|
const activeRuns = new Map();
|
|
@@ -107,6 +108,7 @@ export function createGateReviewService(deps) {
|
|
|
107
108
|
taskSlug,
|
|
108
109
|
taskRepoRoot,
|
|
109
110
|
stateRoot: projectConfig.stateRoot,
|
|
111
|
+
handoffDir: task.handoffDir,
|
|
110
112
|
config: loadRuntimeConfig(reviewSettings)
|
|
111
113
|
};
|
|
112
114
|
}
|
|
@@ -345,42 +347,21 @@ export function createGateReviewService(deps) {
|
|
|
345
347
|
}
|
|
346
348
|
}
|
|
347
349
|
if (gate === "code-diff") {
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
index =
|
|
351
|
-
status: "failed",
|
|
352
|
-
decision: undefined,
|
|
353
|
-
error: prerequisiteError,
|
|
354
|
-
exceptionReason: undefined,
|
|
355
|
-
requestId: undefined,
|
|
356
|
-
requestPath: undefined,
|
|
357
|
-
inputHash: undefined,
|
|
358
|
-
baseCommit: undefined,
|
|
359
|
-
headCommit: undefined,
|
|
360
|
-
commits: undefined,
|
|
361
|
-
changedFiles: undefined,
|
|
362
|
-
diffStat: undefined,
|
|
363
|
-
codeDiffSource,
|
|
364
|
-
codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
|
|
365
|
-
requestedAt: undefined,
|
|
366
|
-
startedAt: undefined,
|
|
367
|
-
completedAt: now(),
|
|
368
|
-
callbackStatus: "not_sent",
|
|
369
|
-
callbackError: undefined
|
|
370
|
-
}, now(), true);
|
|
371
|
-
await saveIndex(deps.fs, context.taskRepoRoot, index);
|
|
350
|
+
const evidenceError = await readCodeDiffEvidenceError(deps.fs, context.taskRepoRoot, codeDiffSource);
|
|
351
|
+
if (evidenceError) {
|
|
352
|
+
index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, evidenceError);
|
|
372
353
|
return {
|
|
373
354
|
status: "failed_to_start",
|
|
374
355
|
gate,
|
|
375
356
|
record: index.gates[gate],
|
|
376
|
-
message:
|
|
357
|
+
message: evidenceError
|
|
377
358
|
};
|
|
378
359
|
}
|
|
379
360
|
}
|
|
380
361
|
const codeDiffInput = gate === "code-diff"
|
|
381
362
|
? await resolveCodeDiffInput(deps, context, record)
|
|
382
363
|
: undefined;
|
|
383
|
-
if (gate === "code-diff" && !codeDiffInput) {
|
|
364
|
+
if (gate === "code-diff" && (!codeDiffInput || codeDiffInput.commits.length === 0)) {
|
|
384
365
|
index = applyGateState(index, gate, {
|
|
385
366
|
status: "not_required",
|
|
386
367
|
decision: undefined,
|
|
@@ -389,16 +370,16 @@ export function createGateReviewService(deps) {
|
|
|
389
370
|
requestId: undefined,
|
|
390
371
|
requestPath: undefined,
|
|
391
372
|
inputHash: undefined,
|
|
392
|
-
baseCommit:
|
|
393
|
-
headCommit:
|
|
394
|
-
commits:
|
|
395
|
-
changedFiles:
|
|
396
|
-
diffStat:
|
|
373
|
+
baseCommit: codeDiffInput?.baseCommit,
|
|
374
|
+
headCommit: codeDiffInput?.headCommit,
|
|
375
|
+
commits: codeDiffInput?.commits,
|
|
376
|
+
changedFiles: codeDiffInput?.changedFiles,
|
|
377
|
+
diffStat: codeDiffInput?.diffStat,
|
|
397
378
|
codeDiffSource,
|
|
398
379
|
codeDiffSources: codeDiffSource ? [codeDiffSource] : undefined,
|
|
399
380
|
requestedAt: undefined,
|
|
400
381
|
startedAt: undefined,
|
|
401
|
-
completedAt: undefined,
|
|
382
|
+
completedAt: codeDiffInput ? now() : undefined,
|
|
402
383
|
callbackStatus: "not_sent",
|
|
403
384
|
callbackError: undefined
|
|
404
385
|
}, now(), true);
|
|
@@ -407,11 +388,51 @@ export function createGateReviewService(deps) {
|
|
|
407
388
|
status: "not_required",
|
|
408
389
|
gate,
|
|
409
390
|
record: index.gates[gate],
|
|
410
|
-
message: "No new commits to review."
|
|
391
|
+
message: codeDiffInput ? "No non-Harness commits to review." : "No new commits to review."
|
|
411
392
|
};
|
|
412
393
|
}
|
|
413
|
-
|
|
414
|
-
|
|
394
|
+
if (gate === "code-diff") {
|
|
395
|
+
const prerequisiteError = await readCodeDiffValidationGateError(deps, context, index);
|
|
396
|
+
if (prerequisiteError) {
|
|
397
|
+
index = await recordCodeDiffStartFailure(index, context, gate, codeDiffSource, prerequisiteError, codeDiffInput);
|
|
398
|
+
return {
|
|
399
|
+
status: "failed_to_start",
|
|
400
|
+
gate,
|
|
401
|
+
record: index.gates[gate],
|
|
402
|
+
message: prerequisiteError
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function recordCodeDiffStartFailure(currentIndex, currentContext, currentGate, currentSource, message, currentCodeDiffInput) {
|
|
407
|
+
const failedIndex = applyGateState(currentIndex, currentGate, {
|
|
408
|
+
status: "failed",
|
|
409
|
+
decision: undefined,
|
|
410
|
+
error: message,
|
|
411
|
+
exceptionReason: undefined,
|
|
412
|
+
requestId: undefined,
|
|
413
|
+
requestPath: undefined,
|
|
414
|
+
inputHash: undefined,
|
|
415
|
+
baseCommit: currentCodeDiffInput?.baseCommit,
|
|
416
|
+
headCommit: currentCodeDiffInput?.headCommit,
|
|
417
|
+
commits: currentCodeDiffInput?.commits,
|
|
418
|
+
changedFiles: currentCodeDiffInput?.changedFiles,
|
|
419
|
+
diffStat: currentCodeDiffInput?.diffStat,
|
|
420
|
+
codeDiffSource: currentSource,
|
|
421
|
+
codeDiffSources: currentSource ? [currentSource] : undefined,
|
|
422
|
+
requestedAt: undefined,
|
|
423
|
+
startedAt: undefined,
|
|
424
|
+
completedAt: now(),
|
|
425
|
+
callbackStatus: "not_sent",
|
|
426
|
+
callbackError: undefined
|
|
427
|
+
}, now(), true);
|
|
428
|
+
await saveIndex(deps.fs, currentContext.taskRepoRoot, failedIndex);
|
|
429
|
+
return failedIndex;
|
|
430
|
+
}
|
|
431
|
+
const currentCodeDiffSources = gate === "code-diff" && codeDiffSource
|
|
432
|
+
? await resolveWorkflowCodeDiffSources(deps, context, codeDiffSource)
|
|
433
|
+
: undefined;
|
|
434
|
+
const codeDiffSources = gate === "code-diff" && codeDiffInput && currentCodeDiffSources
|
|
435
|
+
? resolveCodeDiffSources(record, codeDiffInput, currentCodeDiffSources)
|
|
415
436
|
: undefined;
|
|
416
437
|
const inputHash = await computeInputHash(deps, context.taskRepoRoot, gate, codeDiffInput, codeDiffSources);
|
|
417
438
|
if (!options.force
|
|
@@ -1059,44 +1080,78 @@ async function resolveCodeDiffInput(deps, context, record) {
|
|
|
1059
1080
|
return undefined;
|
|
1060
1081
|
}
|
|
1061
1082
|
const range = `${baseCommit}..${headCommit}`;
|
|
1062
|
-
const
|
|
1083
|
+
const allCommits = splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
|
|
1063
1084
|
"log",
|
|
1064
1085
|
"--oneline",
|
|
1065
1086
|
"--reverse",
|
|
1066
1087
|
range
|
|
1067
1088
|
]));
|
|
1068
|
-
if (
|
|
1089
|
+
if (allCommits.length === 0) {
|
|
1069
1090
|
return undefined;
|
|
1070
1091
|
}
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1092
|
+
const commits = allCommits.filter((line) => !isHarnessCommitLine(line));
|
|
1093
|
+
const commitShas = commits.map(commitShaFromOneline);
|
|
1094
|
+
const changedFiles = new Set();
|
|
1095
|
+
const diffStats = [];
|
|
1096
|
+
const diffs = [];
|
|
1097
|
+
for (const commitSha of commitShas) {
|
|
1098
|
+
for (const changedFile of splitLines(await commandStdout(deps.runner, context.taskRepoRoot, [
|
|
1099
|
+
"show",
|
|
1100
|
+
"--format=",
|
|
1101
|
+
"--name-only",
|
|
1102
|
+
"--find-renames",
|
|
1103
|
+
commitSha
|
|
1104
|
+
]))) {
|
|
1105
|
+
changedFiles.add(changedFile);
|
|
1106
|
+
}
|
|
1107
|
+
const stat = await commandStdout(deps.runner, context.taskRepoRoot, [
|
|
1108
|
+
"show",
|
|
1109
|
+
"--format=",
|
|
1110
|
+
"--stat",
|
|
1111
|
+
"--find-renames",
|
|
1112
|
+
commitSha
|
|
1113
|
+
]);
|
|
1114
|
+
if (stat.trim()) {
|
|
1115
|
+
diffStats.push(stat.trim());
|
|
1116
|
+
}
|
|
1117
|
+
const patch = await commandStdout(deps.runner, context.taskRepoRoot, [
|
|
1118
|
+
"show",
|
|
1119
|
+
"--format=",
|
|
1120
|
+
"--binary",
|
|
1121
|
+
"--find-renames",
|
|
1122
|
+
commitSha
|
|
1123
|
+
]);
|
|
1124
|
+
if (patch) {
|
|
1125
|
+
diffs.push(patch);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
const diffStat = diffStats.join("\n");
|
|
1129
|
+
const diff = diffs.join("\n");
|
|
1089
1130
|
const diffHash = createHash("sha256").update(diff).digest("hex");
|
|
1090
1131
|
return {
|
|
1091
1132
|
baseCommit,
|
|
1092
1133
|
headCommit,
|
|
1093
1134
|
commits,
|
|
1094
|
-
|
|
1135
|
+
commitShas,
|
|
1136
|
+
changedFiles: [...changedFiles],
|
|
1095
1137
|
diffStat,
|
|
1096
1138
|
diffHash
|
|
1097
1139
|
};
|
|
1098
1140
|
}
|
|
1141
|
+
function commitShaFromOneline(line) {
|
|
1142
|
+
return line.split(/\s+/, 1)[0] ?? line;
|
|
1143
|
+
}
|
|
1144
|
+
function isHarnessCommitLine(line) {
|
|
1145
|
+
const separator = line.indexOf(" ");
|
|
1146
|
+
return separator >= 0 && line.slice(separator + 1).startsWith(HARNESS_COMMIT_PREFIX);
|
|
1147
|
+
}
|
|
1099
1148
|
async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
1149
|
+
if (record.gate === "code-diff"
|
|
1150
|
+
&& record.status === "failed"
|
|
1151
|
+
&& record.baseCommit
|
|
1152
|
+
&& await isAncestor(deps.runner, context.taskRepoRoot, record.baseCommit, headCommit)) {
|
|
1153
|
+
return record.baseCommit;
|
|
1154
|
+
}
|
|
1100
1155
|
if (record.gate === "code-diff"
|
|
1101
1156
|
&& record.status === "completed"
|
|
1102
1157
|
&& record.decision === "request_changes"
|
|
@@ -1111,6 +1166,12 @@ async function resolveCodeDiffBaseCommit(deps, context, record, headCommit) {
|
|
|
1111
1166
|
&& await isAncestor(deps.runner, context.taskRepoRoot, record.headCommit, headCommit)) {
|
|
1112
1167
|
return record.headCommit;
|
|
1113
1168
|
}
|
|
1169
|
+
if (record.gate === "code-diff"
|
|
1170
|
+
&& record.status === "not_required"
|
|
1171
|
+
&& record.headCommit
|
|
1172
|
+
&& await isAncestor(deps.runner, context.taskRepoRoot, record.headCommit, headCommit)) {
|
|
1173
|
+
return record.headCommit;
|
|
1174
|
+
}
|
|
1114
1175
|
const rootHead = (await commandStdout(deps.runner, context.repoRoot, ["rev-parse", "HEAD"])).trim();
|
|
1115
1176
|
if (rootHead
|
|
1116
1177
|
&& rootHead !== headCommit
|
|
@@ -1165,7 +1226,7 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
|
|
|
1165
1226
|
digest.update(coreArtifact);
|
|
1166
1227
|
digest.update(await deps.fs.readText(resolveRepoPath(taskRepoRoot, coreArtifact)));
|
|
1167
1228
|
}
|
|
1168
|
-
const common = [
|
|
1229
|
+
const common = gate === "code-diff" ? [] : [
|
|
1169
1230
|
"CLAUDE.md",
|
|
1170
1231
|
".claude/agents/architect.md",
|
|
1171
1232
|
".claude/agents/coder.md",
|
|
@@ -1189,10 +1250,6 @@ async function computeInputHash(deps, taskRepoRoot, gate, codeDiffInput, codeDif
|
|
|
1189
1250
|
if (gate === "code-diff" && codeDiffInput) {
|
|
1190
1251
|
digest.update("codeDiffSources");
|
|
1191
1252
|
digest.update(codeDiffSources?.join("\n") ?? "<missing>");
|
|
1192
|
-
digest.update("baseCommit");
|
|
1193
|
-
digest.update(codeDiffInput.baseCommit);
|
|
1194
|
-
digest.update("headCommit");
|
|
1195
|
-
digest.update(codeDiffInput.headCommit);
|
|
1196
1253
|
digest.update("commits");
|
|
1197
1254
|
digest.update(codeDiffInput.commits.join("\n"));
|
|
1198
1255
|
digest.update("changedFiles");
|
|
@@ -1295,11 +1352,14 @@ async function readValidationReportError(fs, taskRepoRoot) {
|
|
|
1295
1352
|
}
|
|
1296
1353
|
return undefined;
|
|
1297
1354
|
}
|
|
1298
|
-
async function
|
|
1299
|
-
const reportError = await readValidationReportError(
|
|
1355
|
+
async function readCodeDiffEvidenceError(fs, taskRepoRoot, source) {
|
|
1356
|
+
const reportError = await readValidationReportError(fs, taskRepoRoot);
|
|
1300
1357
|
if (reportError) {
|
|
1301
1358
|
return "code-diff requires completed Tester validation. " + reportError;
|
|
1302
1359
|
}
|
|
1360
|
+
return readCodeDiffSourceArtifactError(fs, taskRepoRoot, source);
|
|
1361
|
+
}
|
|
1362
|
+
async function readCodeDiffValidationGateError(deps, context, index) {
|
|
1303
1363
|
const validationGate = index.gates["validation-adequacy"];
|
|
1304
1364
|
if (validationGate.required && validationGate.status !== "skipped" && validationGate.status !== "overridden") {
|
|
1305
1365
|
if (validationGate.status !== "completed" || validationGate.decision !== "approve") {
|
|
@@ -1310,7 +1370,7 @@ async function readCodeDiffPrerequisiteError(deps, context, index, source) {
|
|
|
1310
1370
|
return "code-diff requires a current validation-adequacy approval; code or test evidence changed after the recorded approval.";
|
|
1311
1371
|
}
|
|
1312
1372
|
}
|
|
1313
|
-
return
|
|
1373
|
+
return undefined;
|
|
1314
1374
|
}
|
|
1315
1375
|
async function readArchitectureEvidenceError(fs, taskRepoRoot) {
|
|
1316
1376
|
const relativePath = ".ai/vcm/handoffs/architecture-evidence.md";
|
|
@@ -1399,17 +1459,16 @@ Use each captured snapshot as the immutable handoff or prior-Gate input for this
|
|
|
1399
1459
|
Code Diff Input:
|
|
1400
1460
|
This code-diff gate reviews the new commits from one PM route flow, not the whole task and not one terminal turn.
|
|
1401
1461
|
Code sources: ${codeDiffSources?.join(" -> ") ?? "<missing>"}
|
|
1402
|
-
|
|
1403
|
-
Head commit: ${codeDiffInput.headCommit}
|
|
1404
|
-
Commits:
|
|
1462
|
+
Reviewable commits:
|
|
1405
1463
|
${codeDiffInput.commits.map((line) => `- ${line}`).join("\n")}
|
|
1406
1464
|
Changed files:
|
|
1407
1465
|
${codeDiffInput.changedFiles.length > 0 ? codeDiffInput.changedFiles.map((line) => `- ${line}`).join("\n") : "- <none>"}
|
|
1408
1466
|
Diff commands:
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1467
|
+
${codeDiffInput.commitShas.flatMap((commitSha) => [
|
|
1468
|
+
`- git show --stat --oneline ${commitSha}`,
|
|
1469
|
+
`- git show --find-renames ${commitSha}`
|
|
1470
|
+
]).join("\n")}
|
|
1471
|
+
Review only these commits.`
|
|
1413
1472
|
: "";
|
|
1414
1473
|
return `[VCM GATE REVIEW]
|
|
1415
1474
|
Task: ${context.taskSlug}
|
|
@@ -1763,18 +1822,58 @@ function getSourceArtifacts(gate, codeDiffSources) {
|
|
|
1763
1822
|
...(codeDiffSources ?? []).flatMap((source) => CODE_DIFF_SOURCE_ARTIFACTS[source])
|
|
1764
1823
|
])];
|
|
1765
1824
|
}
|
|
1766
|
-
function resolveCodeDiffSources(record, codeDiffInput,
|
|
1825
|
+
function resolveCodeDiffSources(record, codeDiffInput, currentSources) {
|
|
1767
1826
|
const continuingRecordedRange = record.baseCommit === codeDiffInput.baseCommit
|
|
1768
1827
|
&& ((record.status === "completed" && record.decision === "request_changes")
|
|
1769
1828
|
|| record.status === "failed");
|
|
1770
1829
|
if (!continuingRecordedRange) {
|
|
1771
|
-
return
|
|
1830
|
+
return currentSources;
|
|
1772
1831
|
}
|
|
1773
1832
|
return [...new Set([
|
|
1774
1833
|
...(normalizeCodeDiffSources(record.codeDiffSources, record.codeDiffSource) ?? []),
|
|
1775
|
-
|
|
1834
|
+
...currentSources
|
|
1776
1835
|
])];
|
|
1777
1836
|
}
|
|
1837
|
+
async function resolveWorkflowCodeDiffSources(deps, context, currentSource) {
|
|
1838
|
+
if (!deps.workflowControlService) {
|
|
1839
|
+
return [currentSource];
|
|
1840
|
+
}
|
|
1841
|
+
try {
|
|
1842
|
+
const workflowContext = {
|
|
1843
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
1844
|
+
stateRoot: context.stateRoot,
|
|
1845
|
+
handoffDir: context.handoffDir,
|
|
1846
|
+
taskSlug: context.taskSlug
|
|
1847
|
+
};
|
|
1848
|
+
const [state, progress] = await Promise.all([
|
|
1849
|
+
deps.workflowControlService.getState(workflowContext),
|
|
1850
|
+
deps.workflowControlService.getProgress(workflowContext)
|
|
1851
|
+
]);
|
|
1852
|
+
const startedAtSequence = state.flowRun?.startedAtSequence;
|
|
1853
|
+
if (state.warnings.length > 0 || startedAtSequence === undefined) {
|
|
1854
|
+
return [currentSource];
|
|
1855
|
+
}
|
|
1856
|
+
const sources = [];
|
|
1857
|
+
for (const entry of progress.history) {
|
|
1858
|
+
if (entry.sequence < startedAtSequence)
|
|
1859
|
+
continue;
|
|
1860
|
+
if (entry.flow === "code-change" && entry.targetRole === "coder") {
|
|
1861
|
+
sources.push("coder");
|
|
1862
|
+
}
|
|
1863
|
+
else if (entry.flow === "architect-debug" && entry.targetRole === "architect") {
|
|
1864
|
+
sources.push("architect-debug");
|
|
1865
|
+
}
|
|
1866
|
+
else if (entry.flow === "architecture-diagnosis" && entry.targetRole === "architect") {
|
|
1867
|
+
sources.push("architect-diagnosis");
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
sources.push(currentSource);
|
|
1871
|
+
return [...new Set(sources)];
|
|
1872
|
+
}
|
|
1873
|
+
catch {
|
|
1874
|
+
return [currentSource];
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1778
1877
|
function normalizeCodeDiffSources(sources, source) {
|
|
1779
1878
|
const normalized = Array.isArray(sources) ? sources.filter(isCodeDiffSource) : [];
|
|
1780
1879
|
if (normalized.length === 0 && isCodeDiffSource(source)) {
|
|
@@ -4,7 +4,6 @@ import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/va
|
|
|
4
4
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
|
-
import { getRetrospectiveReportErrors } from "./artifact-service.js";
|
|
8
7
|
const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
|
|
9
8
|
const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
|
|
10
9
|
const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
|
|
@@ -143,7 +142,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
143
142
|
]
|
|
144
143
|
: ["Report is empty."];
|
|
145
144
|
const reportReady = reportErrors.length === 0;
|
|
146
|
-
if (!reportReady || (marker.memoryRunId &&
|
|
145
|
+
if (!reportReady || (marker.memoryRunId && input.memoryReviewStatus === "failed")) {
|
|
147
146
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
148
147
|
...marker,
|
|
149
148
|
status: "failed",
|
|
@@ -155,6 +154,41 @@ export function createHarnessFeedbackService(deps) {
|
|
|
155
154
|
});
|
|
156
155
|
return true;
|
|
157
156
|
}
|
|
157
|
+
if (marker.memoryRunId && input.memoryReviewStatus === "documenting") {
|
|
158
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
159
|
+
...marker,
|
|
160
|
+
status: "waiting-docs",
|
|
161
|
+
updatedAt: timestamp
|
|
162
|
+
});
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
await completeTaskRetrospective(repoRoot, marker);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
|
|
169
|
+
const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
170
|
+
if (!marker || marker.status !== "waiting-docs") {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (memoryStatus === "documenting") {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (memoryStatus === "failed") {
|
|
177
|
+
const timestamp = now();
|
|
178
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
179
|
+
...marker,
|
|
180
|
+
status: "failed",
|
|
181
|
+
failedAt: timestamp,
|
|
182
|
+
updatedAt: timestamp,
|
|
183
|
+
error: "Task Harness Retrospective durable-document assignment failed."
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
await completeTaskRetrospective(repoRoot, marker);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
async function completeTaskRetrospective(repoRoot, marker) {
|
|
191
|
+
const timestamp = now();
|
|
158
192
|
try {
|
|
159
193
|
await removeProcessedFeedback(repoRoot, marker.pendingFeedbackPaths ?? []);
|
|
160
194
|
}
|
|
@@ -166,7 +200,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
166
200
|
updatedAt: timestamp,
|
|
167
201
|
error: `VCM could not remove processed Harness Feedback: ${errorMessage(error)}`
|
|
168
202
|
});
|
|
169
|
-
return
|
|
203
|
+
return;
|
|
170
204
|
}
|
|
171
205
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
172
206
|
...marker,
|
|
@@ -174,7 +208,6 @@ export function createHarnessFeedbackService(deps) {
|
|
|
174
208
|
completedAt: timestamp,
|
|
175
209
|
updatedAt: timestamp
|
|
176
210
|
});
|
|
177
|
-
return true;
|
|
178
211
|
}
|
|
179
212
|
async function assertHarnessEngineerAvailable(_repoRoot) {
|
|
180
213
|
return undefined;
|
|
@@ -285,6 +318,16 @@ export function createHarnessFeedbackService(deps) {
|
|
|
285
318
|
`Current memory snapshot: ${memoryReview.currentMemoryPath}`,
|
|
286
319
|
"Active memory files:",
|
|
287
320
|
...memoryReview.activeMemoryPaths.map((memoryPath) => `- ${memoryPath}`),
|
|
321
|
+
"Proposal candidates:",
|
|
322
|
+
...(memoryReview.proposalCandidates.length > 0
|
|
323
|
+
? memoryReview.proposalCandidates.map((candidate) => [
|
|
324
|
+
candidate.id,
|
|
325
|
+
`source=${candidate.source}`,
|
|
326
|
+
`operation=${candidate.operation}`,
|
|
327
|
+
`target=${candidate.target}`,
|
|
328
|
+
`entry=${candidate.content ?? candidate.existing ?? "none"}`
|
|
329
|
+
].join(" | "))
|
|
330
|
+
: ["none"]),
|
|
288
331
|
...(memoryReview.planningCandidatePath
|
|
289
332
|
? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
|
|
290
333
|
: []),
|
|
@@ -292,88 +335,30 @@ export function createHarnessFeedbackService(deps) {
|
|
|
292
335
|
"Review every memory candidate against final task evidence while performing this retrospective.",
|
|
293
336
|
"The snapshot files contain only the matching pre-review <VCM-memory> block content.",
|
|
294
337
|
"Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
|
|
301
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
|
|
307
|
-
"Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
|
|
308
|
-
"Apply the reviewed result directly to the <VCM-memory> blocks in the listed active memory files. Do not change any content outside those blocks.",
|
|
309
|
-
"If memory changes, commit only the changed active memory files before ending the turn. Use commit message: chore: update VCM memory. If memory is unchanged, do not create a commit.",
|
|
310
|
-
"Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
|
|
338
|
+
"Evaluate each proposal independently. Keep only verified, durable, reusable project knowledge; do not keep task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
|
|
339
|
+
"For every existing entry and proposal, record why the decision is necessary, the impact if the knowledge is absent, the evidence checked, and whether a durable document is the correct source.",
|
|
340
|
+
"When the decision is move-to-durable-doc, remove the entry from memory now and add one durableDocAssignment. Do not wait for the durable document update before removing memory.",
|
|
341
|
+
"Apply the reviewed result directly to the listed <VCM-memory> blocks. Do not change content outside those blocks.",
|
|
342
|
+
"If memory changes, commit only the changed active memory files with message [VCM Harness] Update VCM memory. If memory is unchanged, do not create a commit.",
|
|
343
|
+
`Write the complete machine-readable review to: ${memoryReview.reviewResultPath}`,
|
|
344
|
+
"The JSON root must be: {\"version\":1,\"runId\":\"<assigned run id>\",\"memoryCommit\":\"<full commit or none>\",\"decisions\":[],\"durableDocAssignments\":[]}.",
|
|
345
|
+
"Each decision must contain itemId, source (existing|proposal), target, entry, decision, reason, impactIfAbsent, evidence (non-empty array), finalContent, and durableDocPath.",
|
|
346
|
+
"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.",
|
|
347
|
+
"Each durableDocAssignment must contain sourceMemoryPath, sourceEntry, targetPath, content, reason, and evidence (non-empty array). Use a project-relative Markdown target outside .ai/vcm.",
|
|
348
|
+
"In the retrospective report, include only this memory summary:",
|
|
311
349
|
"",
|
|
312
350
|
"## Memory Review",
|
|
313
|
-
"
|
|
314
|
-
|
|
315
|
-
"
|
|
316
|
-
...(memoryReview.proposalCandidates.length > 0
|
|
317
|
-
? memoryReview.proposalCandidates.flatMap(renderMemoryProposalDecisionTemplate)
|
|
318
|
-
: ["none"]),
|
|
319
|
-
"",
|
|
320
|
-
"### Existing Memory Decisions",
|
|
321
|
-
"#### Item 1",
|
|
322
|
-
"Target: shared",
|
|
323
|
-
"Existing: <exact existing memory entry>",
|
|
324
|
-
"Decision: retain",
|
|
325
|
-
"Reason: <why this decision is correct>",
|
|
326
|
-
"Impact if removed: <specific future role or task failure>",
|
|
327
|
-
"Durable doc disposition: memory",
|
|
328
|
-
"Durable doc path: none",
|
|
329
|
-
"Evidence: <current code, durable documentation, or final task evidence>",
|
|
330
|
-
"",
|
|
331
|
-
"### Existing Memory Changes",
|
|
332
|
-
"- retained: <summary or none>",
|
|
333
|
-
"- updated: <summary or none>",
|
|
334
|
-
"- removed: <summary or none>",
|
|
335
|
-
"",
|
|
336
|
-
"Reviewed memory set: complete"
|
|
351
|
+
"Memory commit: <full commit or none>",
|
|
352
|
+
`Review result: ${memoryReview.reviewResultPath}`,
|
|
353
|
+
"Durable document assignments: <count>"
|
|
337
354
|
]
|
|
338
355
|
: []),
|
|
339
356
|
"",
|
|
340
357
|
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
|
|
341
|
-
|
|
342
|
-
"End your turn after
|
|
358
|
+
"Write the report directly to that path. Do not use vcm-artifact.",
|
|
359
|
+
"End your turn after the report is complete."
|
|
343
360
|
].join("\n");
|
|
344
361
|
}
|
|
345
|
-
function renderMemoryProposalDecisionTemplate(candidate) {
|
|
346
|
-
if (candidate.operation === "remove") {
|
|
347
|
-
return [
|
|
348
|
-
`#### Candidate ${candidate.id}`,
|
|
349
|
-
`Source: ${candidate.source}`,
|
|
350
|
-
"Operation: remove",
|
|
351
|
-
`Target: ${candidate.target}`,
|
|
352
|
-
`Existing: ${candidate.existing}`,
|
|
353
|
-
"Decision: remove|retain",
|
|
354
|
-
"Reason: <why the proposed removal should be applied or rejected>",
|
|
355
|
-
"Evidence checked: <current code, durable documentation, or final task evidence>",
|
|
356
|
-
""
|
|
357
|
-
];
|
|
358
|
-
}
|
|
359
|
-
return [
|
|
360
|
-
`#### Candidate ${candidate.id}`,
|
|
361
|
-
`Source: ${candidate.source}`,
|
|
362
|
-
`Operation: ${candidate.operation}`,
|
|
363
|
-
`Target: ${candidate.target}`,
|
|
364
|
-
`Candidate: ${candidate.content}`,
|
|
365
|
-
"Decision: keep-in-memory|keep-memory-reference|move-to-durable-doc|reject",
|
|
366
|
-
"Final target: shared|project-manager|architect|coder|tester|reviewer|harness-engineer|none",
|
|
367
|
-
"Why memory is necessary: <independent reason, or why it is not necessary>",
|
|
368
|
-
"Impact if absent: <specific impact, or why no durable impact exists>",
|
|
369
|
-
"Durable doc disposition: memory|durable-doc|memory-reference",
|
|
370
|
-
"Durable doc analysis: <why this destination is correct>",
|
|
371
|
-
"Durable doc path: <none or a project-relative durable doc path>",
|
|
372
|
-
"Evidence checked: <current code, durable documentation, or final task evidence>",
|
|
373
|
-
"Final content: <exact one-line reviewed-memory content or none>",
|
|
374
|
-
""
|
|
375
|
-
];
|
|
376
|
-
}
|
|
377
362
|
function buildPendingFeedbackPrompt(repoRoot, feedbackPath) {
|
|
378
363
|
return [
|
|
379
364
|
"[VCM Harness Feedback]",
|
|
@@ -431,6 +416,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
431
416
|
sendPendingFeedback,
|
|
432
417
|
startTaskRetrospective,
|
|
433
418
|
handleTaskRetrospectiveHook,
|
|
419
|
+
completeWaitingTaskRetrospective,
|
|
434
420
|
assertHarnessEngineerAvailable
|
|
435
421
|
};
|
|
436
422
|
}
|
|
@@ -495,6 +481,18 @@ function assertPendingFeedbackPath(feedbackPath) {
|
|
|
495
481
|
throw new Error(`Invalid assigned Harness Feedback path: ${feedbackPath}.`);
|
|
496
482
|
}
|
|
497
483
|
}
|
|
484
|
+
function getRetrospectiveReportErrors(content) {
|
|
485
|
+
const errors = [];
|
|
486
|
+
if (!/^# Task Harness Retrospective(?::\s*.+)?\s*$/m.test(content)) {
|
|
487
|
+
errors.push("Retrospective report requires '# Task Harness Retrospective: <task>'.");
|
|
488
|
+
}
|
|
489
|
+
for (const heading of ["Findings", "Feedback Dispositions", "Recommended Harness Changes", "VCM Issue Drafts"]) {
|
|
490
|
+
if (!new RegExp(`^## ${heading}\\s*$`, "m").test(content)) {
|
|
491
|
+
errors.push(`Missing required section: ${heading}.`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return errors;
|
|
495
|
+
}
|
|
498
496
|
function parseSimpleMetadata(content) {
|
|
499
497
|
const result = {};
|
|
500
498
|
for (const line of content.split(/\r?\n/).slice(0, 80)) {
|