vibe-coding-master 0.7.42 → 0.7.44
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 +49 -17
- 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 +32 -2
- package/dist/backend/api/workflow-control-routes.js +7 -26
- package/dist/backend/cli/install-vcm-harness.js +61 -6
- package/dist/backend/role-tool-policy.js +1 -1
- package/dist/backend/server.js +14 -5
- package/dist/backend/services/artifact-service.js +10 -32
- package/dist/backend/services/auto-memory-service.js +642 -14
- package/dist/backend/services/claude-hook-service.js +183 -6
- package/dist/backend/services/gate-review-service.js +173 -74
- package/dist/backend/services/harness-feedback-service.js +180 -80
- package/dist/backend/services/harness-service.js +65 -9
- package/dist/backend/services/memory-review-paths.js +13 -0
- package/dist/backend/services/role-stall-detector-service.js +322 -0
- package/dist/backend/services/round-service.js +25 -0
- package/dist/backend/services/runtime-coordinator-service.js +12 -1
- package/dist/backend/services/session-service.js +70 -3
- package/dist/backend/services/status-service.js +1 -0
- package/dist/backend/services/translation-worker-service.js +19 -4
- package/dist/backend/services/workflow-control-service.js +524 -204
- package/dist/backend/templates/handoff.js +46 -5
- package/dist/backend/templates/harness/architect-agent.js +12 -6
- 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 +12 -5
- package/dist/backend/templates/harness/gate-review.js +150 -57
- package/dist/backend/templates/harness/harness-engineer-agent.js +38 -13
- package/dist/backend/templates/harness/project-manager-agent.js +18 -12
- 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 -6
- package/dist/backend/templates/harness/vcm-task-state-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +7 -9
- package/dist/shared/types/role-stall.js +1 -0
- package/dist/shared/types/workflow.js +15 -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 +17 -1
- package/dist-frontend/assets/{index-C_XHGNBD.css → index-B0d4Z6ny.css} +1 -1
- package/dist-frontend/assets/index-BvCmrFlN.js +97 -0
- 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/harness-tools/vcm-artifact +1 -2
- package/scripts/harness-tools/vcm-bash-guard +204 -14
- package/dist-frontend/assets/index-Bocc2DWF.js +0 -97
|
@@ -5,7 +5,7 @@ import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
7
|
import { readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
|
-
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
8
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
9
|
import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
10
|
const MEMORY_FILE_DEFINITIONS = [
|
|
11
11
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
@@ -122,7 +122,7 @@ export function createAutoMemoryService(deps) {
|
|
|
122
122
|
listRuns(taskRepoRoot),
|
|
123
123
|
listMemoryFiles(taskRepoRoot)
|
|
124
124
|
]);
|
|
125
|
-
if (active && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
125
|
+
if (active && !preserveDisabledReview(active) && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
126
126
|
await discardActiveReview(taskRepoRoot, active);
|
|
127
127
|
active = undefined;
|
|
128
128
|
runs = await listRuns(taskRepoRoot);
|
|
@@ -140,7 +140,13 @@ export function createAutoMemoryService(deps) {
|
|
|
140
140
|
const active = await loadActiveState(input.taskRepoRoot);
|
|
141
141
|
const preferences = await deps.appSettings.getPreferences();
|
|
142
142
|
if (!preferences.autoMemoryEnabled) {
|
|
143
|
-
if (active) {
|
|
143
|
+
if (active?.status === "documenting") {
|
|
144
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
145
|
+
}
|
|
146
|
+
else if (active?.status === "reviewing" && active.reviewPromptDispatchedAt) {
|
|
147
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
148
|
+
}
|
|
149
|
+
else if (active) {
|
|
144
150
|
await discardActiveReview(input.taskRepoRoot, active);
|
|
145
151
|
}
|
|
146
152
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
@@ -153,6 +159,10 @@ export function createAutoMemoryService(deps) {
|
|
|
153
159
|
if (active?.status === "reviewing") {
|
|
154
160
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
155
161
|
}
|
|
162
|
+
if (active?.status === "documenting") {
|
|
163
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
164
|
+
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
165
|
+
}
|
|
156
166
|
if (active || !input.roundReady || !input.requestTrigger) {
|
|
157
167
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
158
168
|
}
|
|
@@ -175,7 +185,7 @@ export function createAutoMemoryService(deps) {
|
|
|
175
185
|
const runId = createRunId(timestamp, "auto");
|
|
176
186
|
const drafts = roles.map((role) => ({
|
|
177
187
|
role,
|
|
178
|
-
path:
|
|
188
|
+
path: memoryReviewRoleDraftPath(runId, role),
|
|
179
189
|
status: "pending"
|
|
180
190
|
}));
|
|
181
191
|
const state = {
|
|
@@ -187,7 +197,8 @@ export function createAutoMemoryService(deps) {
|
|
|
187
197
|
trigger: input.requestTrigger,
|
|
188
198
|
createdAt: timestamp,
|
|
189
199
|
updatedAt: timestamp,
|
|
190
|
-
drafts
|
|
200
|
+
drafts,
|
|
201
|
+
assignments: []
|
|
191
202
|
};
|
|
192
203
|
const before = await readMemorySet(input.taskRepoRoot);
|
|
193
204
|
await writeRunMemorySet(input.taskRepoRoot, runId, "before", before);
|
|
@@ -304,6 +315,7 @@ export function createAutoMemoryService(deps) {
|
|
|
304
315
|
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
305
316
|
currentMemoryPath: path.join(runRoot, "before"),
|
|
306
317
|
activeMemoryPaths: memoryPaths.map((memoryPath) => resolveRepoPath(taskRepoRoot, memoryPath)),
|
|
318
|
+
reviewResultPath: path.join(runRoot, "review-result.json"),
|
|
307
319
|
proposalCandidates,
|
|
308
320
|
...(planningCandidatePath
|
|
309
321
|
? { planningCandidatePath: resolveRepoPath(taskRepoRoot, planningCandidatePath) }
|
|
@@ -324,17 +336,30 @@ export function createAutoMemoryService(deps) {
|
|
|
324
336
|
async function isRoleMemoryTurn(taskRepoRoot, role) {
|
|
325
337
|
const state = await loadActiveState(taskRepoRoot);
|
|
326
338
|
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
327
|
-
|
|
339
|
+
if (draft?.role === role && draft.status === "dispatched") {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
const assignment = state?.status === "documenting" ? currentDurableDocAssignment(state) : undefined;
|
|
343
|
+
if (!assignment) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
if (assignment.status === "resolving-owner") {
|
|
347
|
+
return role === "project-manager";
|
|
348
|
+
}
|
|
349
|
+
return assignment.status === "running" && assignment.owner === role;
|
|
328
350
|
}
|
|
329
351
|
async function handleRoleHook(input) {
|
|
330
352
|
const state = await loadActiveState(input.taskRepoRoot);
|
|
331
|
-
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
353
|
+
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
|
|
332
354
|
if (state) {
|
|
333
355
|
await discardActiveReview(input.taskRepoRoot, state);
|
|
334
356
|
return true;
|
|
335
357
|
}
|
|
336
358
|
return false;
|
|
337
359
|
}
|
|
360
|
+
if (state?.status === "documenting") {
|
|
361
|
+
return handleDurableDocRoleHook(input, state);
|
|
362
|
+
}
|
|
338
363
|
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
339
364
|
if (!state || !draft || draft.role !== input.role) {
|
|
340
365
|
return false;
|
|
@@ -375,7 +400,7 @@ export function createAutoMemoryService(deps) {
|
|
|
375
400
|
}
|
|
376
401
|
async function handleHarnessEngineerHook(input) {
|
|
377
402
|
const state = await loadActiveState(input.taskRepoRoot);
|
|
378
|
-
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
403
|
+
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
|
|
379
404
|
if (state) {
|
|
380
405
|
await discardActiveReview(input.taskRepoRoot, state);
|
|
381
406
|
return true;
|
|
@@ -404,6 +429,9 @@ export function createAutoMemoryService(deps) {
|
|
|
404
429
|
}
|
|
405
430
|
try {
|
|
406
431
|
await recordHarnessEngineerMemoryResult(input.taskRepoRoot, state);
|
|
432
|
+
if (state.assignments.length > 0) {
|
|
433
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
434
|
+
}
|
|
407
435
|
}
|
|
408
436
|
catch (error) {
|
|
409
437
|
await failReview(input.taskRepoRoot, state, `Harness Engineer memory result could not be recorded: ${errorMessage(error)}`);
|
|
@@ -484,9 +512,49 @@ export function createAutoMemoryService(deps) {
|
|
|
484
512
|
await clearActiveState(taskRepoRoot);
|
|
485
513
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
486
514
|
}
|
|
515
|
+
async function retryDurableDocAssignment(baseRepoRoot, taskRepoRoot, assignmentId) {
|
|
516
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
517
|
+
const assignment = state?.status === "documenting"
|
|
518
|
+
? state.assignments.find((candidate) => candidate.id === assignmentId)
|
|
519
|
+
: undefined;
|
|
520
|
+
if (!state || !assignment || assignment.status !== "failed") {
|
|
521
|
+
throw new VcmError({
|
|
522
|
+
code: "DURABLE_DOC_ASSIGNMENT_NOT_FAILED",
|
|
523
|
+
message: `There is no failed durable-document assignment to retry: ${assignmentId}`,
|
|
524
|
+
statusCode: 409
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
assignment.status = assignment.owner ? "pending" : "waiting-owner";
|
|
528
|
+
assignment.updatedAt = now();
|
|
529
|
+
delete assignment.error;
|
|
530
|
+
delete assignment.requestedOwner;
|
|
531
|
+
delete assignment.dispatchedAt;
|
|
532
|
+
delete assignment.baseCommit;
|
|
533
|
+
delete assignment.reportHashBefore;
|
|
534
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
535
|
+
await dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state);
|
|
536
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
537
|
+
}
|
|
538
|
+
async function resolveDurableDocAssignmentOwner(baseRepoRoot, taskRepoRoot, assignmentId, owner) {
|
|
539
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
540
|
+
const assignment = state?.status === "documenting"
|
|
541
|
+
? currentDurableDocAssignment(state)
|
|
542
|
+
: undefined;
|
|
543
|
+
if (!state || !assignment || assignment.id !== assignmentId || assignment.status !== "resolving-owner") {
|
|
544
|
+
throw new VcmError({
|
|
545
|
+
code: "DURABLE_DOC_OWNER_RESOLUTION_NOT_ACTIVE",
|
|
546
|
+
message: `Durable-document assignment is not waiting for PM owner resolution: ${assignmentId}`,
|
|
547
|
+
statusCode: 409
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
assignment.requestedOwner = owner;
|
|
551
|
+
assignment.updatedAt = now();
|
|
552
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
553
|
+
return getState(baseRepoRoot, taskRepoRoot);
|
|
554
|
+
}
|
|
487
555
|
async function assertHarnessEngineerAvailable(taskRepoRoot) {
|
|
488
556
|
const state = await loadActiveState(taskRepoRoot);
|
|
489
|
-
if (state && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
557
|
+
if (state && !preserveDisabledReview(state) && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
490
558
|
await discardActiveReview(taskRepoRoot, state);
|
|
491
559
|
return;
|
|
492
560
|
}
|
|
@@ -502,7 +570,7 @@ export function createAutoMemoryService(deps) {
|
|
|
502
570
|
}
|
|
503
571
|
async function assertNoActiveReview(taskRepoRoot) {
|
|
504
572
|
const state = await loadActiveState(taskRepoRoot);
|
|
505
|
-
if (state && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
573
|
+
if (state && !preserveDisabledReview(state) && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
506
574
|
await discardActiveReview(taskRepoRoot, state);
|
|
507
575
|
return;
|
|
508
576
|
}
|
|
@@ -518,6 +586,166 @@ export function createAutoMemoryService(deps) {
|
|
|
518
586
|
: "Wait for the active memory review to finish."
|
|
519
587
|
});
|
|
520
588
|
}
|
|
589
|
+
async function handleDurableDocRoleHook(input, state) {
|
|
590
|
+
const assignment = currentDurableDocAssignment(state);
|
|
591
|
+
if (!assignment) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
const expectedRole = assignment.status === "resolving-owner"
|
|
595
|
+
? "project-manager"
|
|
596
|
+
: assignment.status === "running"
|
|
597
|
+
? assignment.owner
|
|
598
|
+
: undefined;
|
|
599
|
+
if (expectedRole !== input.role) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
if (input.eventName === "StopFailure") {
|
|
606
|
+
assignment.status = "failed";
|
|
607
|
+
assignment.error = `${input.role} durable-document assignment turn failed.`;
|
|
608
|
+
assignment.updatedAt = now();
|
|
609
|
+
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
if (input.eventName !== "Stop") {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
if (assignment.status === "resolving-owner") {
|
|
616
|
+
if (!assignment.requestedOwner) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
assignment.owner = assignment.requestedOwner;
|
|
620
|
+
delete assignment.requestedOwner;
|
|
621
|
+
assignment.status = "pending";
|
|
622
|
+
assignment.updatedAt = now();
|
|
623
|
+
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
624
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
assignment.commit = await validateDurableDocAssignmentCompletion(input.taskRepoRoot, assignment);
|
|
629
|
+
assignment.status = "completed";
|
|
630
|
+
assignment.completedAt = now();
|
|
631
|
+
assignment.updatedAt = assignment.completedAt;
|
|
632
|
+
delete assignment.error;
|
|
633
|
+
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
634
|
+
const next = currentDurableDocAssignment(state);
|
|
635
|
+
if (next) {
|
|
636
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
await clearActiveState(input.taskRepoRoot);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
assignment.status = "failed";
|
|
644
|
+
assignment.error = `Durable-document assignment could not be completed: ${errorMessage(error)}`;
|
|
645
|
+
assignment.updatedAt = now();
|
|
646
|
+
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
async function dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state) {
|
|
651
|
+
const assignment = currentDurableDocAssignment(state);
|
|
652
|
+
if (!assignment || assignment.status === "failed") {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (assignment.status === "running" || assignment.status === "resolving-owner") {
|
|
656
|
+
const role = assignment.status === "resolving-owner" ? "project-manager" : assignment.owner;
|
|
657
|
+
if (!role) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, state.taskSlug, role);
|
|
661
|
+
if (existing?.activityStatus === "running" && deps.runtime.getSession(existing.id)) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
assignment.status = assignment.owner ? "pending" : "waiting-owner";
|
|
665
|
+
assignment.updatedAt = now();
|
|
666
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
667
|
+
}
|
|
668
|
+
const role = assignment.owner ?? "project-manager";
|
|
669
|
+
try {
|
|
670
|
+
const session = await ensureWorkflowRoleSession(baseRepoRoot, state.taskSlug, role);
|
|
671
|
+
if (session.activityStatus === "running") {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
assignment.status = assignment.owner ? "running" : "resolving-owner";
|
|
675
|
+
assignment.baseCommit = await deps.git.getHeadCommit(taskRepoRoot);
|
|
676
|
+
assignment.reportHashBefore = await hashOptionalFile(taskRepoRoot, assignment.reportPath);
|
|
677
|
+
assignment.dispatchedAt = now();
|
|
678
|
+
assignment.updatedAt = assignment.dispatchedAt;
|
|
679
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
680
|
+
await submitTerminalInput(deps.runtime, session.id, assignment.owner
|
|
681
|
+
? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
|
|
682
|
+
: buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
assignment.status = "failed";
|
|
686
|
+
assignment.error = `Unable to dispatch durable-document assignment: ${errorMessage(error)}`;
|
|
687
|
+
assignment.updatedAt = now();
|
|
688
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function validateDurableDocAssignmentCompletion(taskRepoRoot, assignment) {
|
|
692
|
+
const reportAbsolutePath = resolveRepoPath(taskRepoRoot, assignment.reportPath);
|
|
693
|
+
if (!(await deps.fs.pathExists(reportAbsolutePath))) {
|
|
694
|
+
throw new Error(`required report is missing: ${assignment.reportPath}`);
|
|
695
|
+
}
|
|
696
|
+
const reportContent = await deps.fs.readText(reportAbsolutePath);
|
|
697
|
+
const reportHash = sha256(reportContent);
|
|
698
|
+
if (assignment.reportHashBefore && reportHash === assignment.reportHashBefore) {
|
|
699
|
+
throw new Error(`${assignment.reportPath} was not updated for assignment ${assignment.id}`);
|
|
700
|
+
}
|
|
701
|
+
const check = checkMarkdownArtifact("docs-update-report", assignment.reportPath, reportContent);
|
|
702
|
+
if (check.status !== "ok") {
|
|
703
|
+
const reasons = [
|
|
704
|
+
...check.missingHeadings.map((heading) => `missing heading ${heading}`),
|
|
705
|
+
...check.invalidFields
|
|
706
|
+
];
|
|
707
|
+
throw new Error(`${assignment.reportPath} is invalid: ${reasons.join("; ") || check.status}`);
|
|
708
|
+
}
|
|
709
|
+
const assignmentId = readArtifactSectionValue(reportContent, "Assignment ID")?.trim();
|
|
710
|
+
if (assignmentId !== assignment.id) {
|
|
711
|
+
throw new Error(`Assignment ID must be exactly ${assignment.id}`);
|
|
712
|
+
}
|
|
713
|
+
const decision = readArtifactSectionValue(reportContent, "Decision")?.trim().toLowerCase();
|
|
714
|
+
if (decision !== "synced" && decision !== "unchanged") {
|
|
715
|
+
throw new Error("Decision must be synced or unchanged");
|
|
716
|
+
}
|
|
717
|
+
const currentHead = await deps.git.getHeadCommit(taskRepoRoot);
|
|
718
|
+
if (decision === "synced") {
|
|
719
|
+
if (!assignment.baseCommit || currentHead === assignment.baseCommit) {
|
|
720
|
+
throw new Error("documentation changes were not committed");
|
|
721
|
+
}
|
|
722
|
+
const changedPaths = await deps.git.getChangedPaths(taskRepoRoot, assignment.baseCommit, currentHead);
|
|
723
|
+
if (!changedPaths.includes(assignment.targetPath)) {
|
|
724
|
+
throw new Error(`documentation commit does not include ${assignment.targetPath}`);
|
|
725
|
+
}
|
|
726
|
+
const reportedCommit = readArtifactSectionValue(reportContent, "Commit")?.trim();
|
|
727
|
+
if (!reportedCommit || reportedCommit === "TBD" || !currentHead.startsWith(reportedCommit)) {
|
|
728
|
+
throw new Error(`Commit must identify the current documentation commit ${currentHead}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return currentHead;
|
|
732
|
+
}
|
|
733
|
+
async function hashOptionalFile(taskRepoRoot, relativePath) {
|
|
734
|
+
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
735
|
+
return await deps.fs.pathExists(absolutePath)
|
|
736
|
+
? sha256(await deps.fs.readText(absolutePath))
|
|
737
|
+
: undefined;
|
|
738
|
+
}
|
|
739
|
+
async function persistMemoryAssignmentState(taskRepoRoot, state) {
|
|
740
|
+
state.updatedAt = now();
|
|
741
|
+
await persistActiveState(taskRepoRoot, state);
|
|
742
|
+
const run = await readRun(taskRepoRoot, state.runId);
|
|
743
|
+
await persistRun(taskRepoRoot, {
|
|
744
|
+
...run,
|
|
745
|
+
assignments: state.assignments,
|
|
746
|
+
updatedAt: state.updatedAt
|
|
747
|
+
});
|
|
748
|
+
}
|
|
521
749
|
async function dispatchCurrentDraft(baseRepoRoot, taskRepoRoot, state) {
|
|
522
750
|
const draft = currentDraft(state);
|
|
523
751
|
if (!draft || draft.status !== "pending") {
|
|
@@ -641,8 +869,27 @@ export function createAutoMemoryService(deps) {
|
|
|
641
869
|
hint: "Remove the unnecessary memory commits or leave memory unchanged without committing."
|
|
642
870
|
});
|
|
643
871
|
}
|
|
644
|
-
await
|
|
872
|
+
const reviewResult = await readAndValidateHarnessMemoryReviewResult(taskRepoRoot, state, before, after, currentHead, changedMemoryPaths);
|
|
645
873
|
const timestamp = now();
|
|
874
|
+
const assignments = reviewResult.durableDocAssignments.map((assignment, index) => {
|
|
875
|
+
const owner = inferDurableDocAssignmentOwner(assignment.targetPath);
|
|
876
|
+
return {
|
|
877
|
+
id: `${state.runId}-doc-${index + 1}`,
|
|
878
|
+
runId: state.runId,
|
|
879
|
+
sourceMemoryPath: assignment.sourceMemoryPath,
|
|
880
|
+
sourceEntry: assignment.sourceEntry,
|
|
881
|
+
targetPath: normalizeProjectRelativePath(assignment.targetPath),
|
|
882
|
+
content: assignment.content,
|
|
883
|
+
reason: assignment.reason,
|
|
884
|
+
evidence: assignment.evidence,
|
|
885
|
+
...(owner ? { owner } : {}),
|
|
886
|
+
status: owner ? "pending" : "waiting-owner",
|
|
887
|
+
reportPath: ".ai/vcm/handoffs/docs-update-report.md",
|
|
888
|
+
createdAt: timestamp,
|
|
889
|
+
updatedAt: timestamp
|
|
890
|
+
};
|
|
891
|
+
});
|
|
892
|
+
await writeRunMemorySet(taskRepoRoot, state.runId, "after", after);
|
|
646
893
|
const diff = renderMemoryDiff(before, after);
|
|
647
894
|
const run = await readRun(taskRepoRoot, state.runId);
|
|
648
895
|
await persistRun(taskRepoRoot, {
|
|
@@ -651,10 +898,225 @@ export function createAutoMemoryService(deps) {
|
|
|
651
898
|
updatedAt: timestamp,
|
|
652
899
|
appliedAt: timestamp,
|
|
653
900
|
afterHashes: hashMemorySet(after),
|
|
654
|
-
diff
|
|
901
|
+
diff,
|
|
902
|
+
assignments
|
|
655
903
|
});
|
|
656
904
|
await deps.fs.writeText(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}/applied.patch`), diff);
|
|
657
|
-
|
|
905
|
+
if (assignments.length === 0) {
|
|
906
|
+
await clearActiveState(taskRepoRoot);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
state.status = "documenting";
|
|
910
|
+
state.assignments = assignments;
|
|
911
|
+
state.updatedAt = timestamp;
|
|
912
|
+
delete state.error;
|
|
913
|
+
await persistActiveState(taskRepoRoot, state);
|
|
914
|
+
}
|
|
915
|
+
async function readAndValidateHarnessMemoryReviewResult(taskRepoRoot, state, before, after, currentHead, changedMemoryPaths) {
|
|
916
|
+
const resultPath = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}/review-result.json`);
|
|
917
|
+
if (!(await deps.fs.pathExists(resultPath))) {
|
|
918
|
+
throw new VcmError({
|
|
919
|
+
code: "MEMORY_REVIEW_RESULT_MISSING",
|
|
920
|
+
message: "Harness Engineer did not write review-result.json.",
|
|
921
|
+
statusCode: 409,
|
|
922
|
+
hint: `Write the validated memory decisions to ${resultPath} before ending the retrospective turn.`
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const result = await deps.fs.readJson(resultPath);
|
|
926
|
+
const shapeError = validateMemoryReviewResultShape(result, state.runId);
|
|
927
|
+
if (shapeError) {
|
|
928
|
+
throw new VcmError({
|
|
929
|
+
code: "MEMORY_REVIEW_RESULT_INVALID",
|
|
930
|
+
message: `Harness Engineer review-result.json is invalid: ${shapeError}`,
|
|
931
|
+
statusCode: 409
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
if (changedMemoryPaths.length > 0 && result.memoryCommit !== currentHead) {
|
|
935
|
+
throw new VcmError({
|
|
936
|
+
code: "MEMORY_REVIEW_RESULT_COMMIT_MISMATCH",
|
|
937
|
+
message: `review-result.json memoryCommit must be the current memory commit: ${currentHead}`,
|
|
938
|
+
statusCode: 409
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
if (changedMemoryPaths.length === 0 && result.memoryCommit !== "none") {
|
|
942
|
+
throw new VcmError({
|
|
943
|
+
code: "MEMORY_REVIEW_RESULT_COMMIT_UNEXPECTED",
|
|
944
|
+
message: "review-result.json memoryCommit must be none when memory is unchanged.",
|
|
945
|
+
statusCode: 409
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
const candidates = await readReviewCandidates(taskRepoRoot, state);
|
|
949
|
+
const proposalDecisions = result.decisions.filter((decision) => decision.source === "proposal");
|
|
950
|
+
for (const candidate of candidates) {
|
|
951
|
+
const matches = proposalDecisions.filter((decision) => decision.itemId === candidate.id);
|
|
952
|
+
if (matches.length !== 1) {
|
|
953
|
+
throw new VcmError({
|
|
954
|
+
code: "MEMORY_REVIEW_PROPOSAL_DECISION_MISSING",
|
|
955
|
+
message: `review-result.json must contain exactly one decision for proposal ${candidate.id}.`,
|
|
956
|
+
statusCode: 409
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
validateProposalDecision(candidate, matches[0], after);
|
|
960
|
+
}
|
|
961
|
+
const unknownProposal = proposalDecisions.find((decision) => !candidates.some((candidate) => candidate.id === decision.itemId));
|
|
962
|
+
if (unknownProposal) {
|
|
963
|
+
throw new VcmError({
|
|
964
|
+
code: "MEMORY_REVIEW_PROPOSAL_DECISION_UNKNOWN",
|
|
965
|
+
message: `review-result.json contains an unknown proposal decision: ${unknownProposal.itemId}`,
|
|
966
|
+
statusCode: 409
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
for (const decision of result.decisions.filter((candidate) => candidate.source === "existing")) {
|
|
970
|
+
const memoryPath = memoryTargetToPath(decision.target);
|
|
971
|
+
if (!substantiveMemoryEntries(before[memoryPath]).includes(decision.entry)) {
|
|
972
|
+
throw new VcmError({
|
|
973
|
+
code: "MEMORY_REVIEW_EXISTING_DECISION_UNKNOWN",
|
|
974
|
+
message: `review-result.json contains an unknown existing-memory decision: ${decision.itemId}`,
|
|
975
|
+
statusCode: 409
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
980
|
+
const target = memoryPathToTarget(definition.path);
|
|
981
|
+
for (const entry of substantiveMemoryEntries(before[definition.path])) {
|
|
982
|
+
const matches = result.decisions.filter((decision) => decision.source === "existing"
|
|
983
|
+
&& decision.target === target
|
|
984
|
+
&& decision.entry === entry);
|
|
985
|
+
if (matches.length !== 1) {
|
|
986
|
+
throw new VcmError({
|
|
987
|
+
code: "MEMORY_REVIEW_EXISTING_DECISION_MISSING",
|
|
988
|
+
message: `review-result.json must contain exactly one decision for existing memory entry in ${definition.path}: ${entry}`,
|
|
989
|
+
statusCode: 409
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
validateExistingMemoryDecision(definition.path, entry, matches[0], after);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const moveDecisions = result.decisions.filter((decision) => decision.decision === "move-to-durable-doc");
|
|
996
|
+
if (moveDecisions.length !== result.durableDocAssignments.length) {
|
|
997
|
+
throw new VcmError({
|
|
998
|
+
code: "MEMORY_REVIEW_ASSIGNMENT_COUNT_MISMATCH",
|
|
999
|
+
message: "Every move-to-durable-doc decision must have exactly one durableDocAssignment.",
|
|
1000
|
+
statusCode: 409
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const assignmentKeys = new Set();
|
|
1004
|
+
for (const assignment of result.durableDocAssignments) {
|
|
1005
|
+
validateDurableDocAssignmentInput(assignment, after);
|
|
1006
|
+
const key = `${assignment.sourceMemoryPath}\n${assignment.sourceEntry}\n${assignment.targetPath}`;
|
|
1007
|
+
if (assignmentKeys.has(key)) {
|
|
1008
|
+
throw new VcmError({
|
|
1009
|
+
code: "MEMORY_REVIEW_ASSIGNMENT_DUPLICATE",
|
|
1010
|
+
message: `Duplicate durableDocAssignment for ${assignment.targetPath}: ${assignment.sourceEntry}`,
|
|
1011
|
+
statusCode: 409
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
assignmentKeys.add(key);
|
|
1015
|
+
const matchingDecision = moveDecisions.find((decision) => (memoryTargetToPath(decision.target) === assignment.sourceMemoryPath
|
|
1016
|
+
&& decision.entry === assignment.sourceEntry
|
|
1017
|
+
&& decision.durableDocPath === assignment.targetPath));
|
|
1018
|
+
if (!matchingDecision) {
|
|
1019
|
+
throw new VcmError({
|
|
1020
|
+
code: "MEMORY_REVIEW_ASSIGNMENT_DECISION_MISSING",
|
|
1021
|
+
message: `durableDocAssignment has no matching move decision: ${assignment.targetPath}`,
|
|
1022
|
+
statusCode: 409
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return result;
|
|
1027
|
+
}
|
|
1028
|
+
function validateProposalDecision(candidate, decision, after) {
|
|
1029
|
+
const expectedEntry = candidate.content ?? candidate.existing ?? "";
|
|
1030
|
+
if (decision.entry !== expectedEntry || decision.target !== candidate.target) {
|
|
1031
|
+
throw new VcmError({
|
|
1032
|
+
code: "MEMORY_REVIEW_PROPOSAL_DECISION_MISMATCH",
|
|
1033
|
+
message: `Proposal decision does not match assigned candidate ${candidate.id}.`,
|
|
1034
|
+
statusCode: 409
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
const afterEntries = new Set(substantiveMemoryEntries(after[memoryTargetToPath(decision.target)]));
|
|
1038
|
+
if (candidate.operation === "remove") {
|
|
1039
|
+
if (decision.decision !== "remove" && decision.decision !== "retain") {
|
|
1040
|
+
throw new VcmError({
|
|
1041
|
+
code: "MEMORY_REVIEW_PROPOSAL_DECISION_INVALID",
|
|
1042
|
+
message: `Remove proposal ${candidate.id} must use remove or retain.`,
|
|
1043
|
+
statusCode: 409
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
if (decision.decision === "remove" && afterEntries.has(expectedEntry)) {
|
|
1047
|
+
throw new VcmError({
|
|
1048
|
+
code: "MEMORY_REVIEW_PROPOSAL_REMOVAL_MISMATCH",
|
|
1049
|
+
message: `Accepted removal is still present for proposal ${candidate.id}.`,
|
|
1050
|
+
statusCode: 409
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
if (decision.decision === "retain" && !afterEntries.has(expectedEntry)) {
|
|
1054
|
+
throw new VcmError({
|
|
1055
|
+
code: "MEMORY_REVIEW_PROPOSAL_RETAIN_MISMATCH",
|
|
1056
|
+
message: `Rejected removal is missing from memory for proposal ${candidate.id}.`,
|
|
1057
|
+
statusCode: 409
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (!new Set(["keep-in-memory", "keep-memory-reference", "move-to-durable-doc", "reject"]).has(decision.decision)) {
|
|
1063
|
+
throw new VcmError({
|
|
1064
|
+
code: "MEMORY_REVIEW_PROPOSAL_DECISION_INVALID",
|
|
1065
|
+
message: `Add or update proposal ${candidate.id} uses an invalid decision: ${decision.decision}`,
|
|
1066
|
+
statusCode: 409
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
if ((decision.decision === "keep-in-memory" || decision.decision === "keep-memory-reference")
|
|
1070
|
+
&& !afterEntries.has(decision.finalContent)) {
|
|
1071
|
+
throw new VcmError({
|
|
1072
|
+
code: "MEMORY_REVIEW_PROPOSAL_CONTENT_MISSING",
|
|
1073
|
+
message: `Accepted proposal content is missing from memory for ${candidate.id}.`,
|
|
1074
|
+
statusCode: 409
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
function validateExistingMemoryDecision(memoryPath, entry, decision, after) {
|
|
1079
|
+
const afterEntries = new Set(substantiveMemoryEntries(after[memoryPath]));
|
|
1080
|
+
if (decision.decision === "retain" && !afterEntries.has(entry)) {
|
|
1081
|
+
throw new VcmError({
|
|
1082
|
+
code: "MEMORY_REVIEW_RETAIN_MISMATCH",
|
|
1083
|
+
message: `Retained memory entry is missing from ${memoryPath}: ${entry}`,
|
|
1084
|
+
statusCode: 409
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
if ((decision.decision === "remove" || decision.decision === "move-to-durable-doc") && afterEntries.has(entry)) {
|
|
1088
|
+
throw new VcmError({
|
|
1089
|
+
code: "MEMORY_REVIEW_REMOVAL_MISMATCH",
|
|
1090
|
+
message: `Removed memory entry is still present in ${memoryPath}: ${entry}`,
|
|
1091
|
+
statusCode: 409
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
if (decision.decision === "update") {
|
|
1095
|
+
if (!decision.finalContent || decision.finalContent === "none" || !afterEntries.has(decision.finalContent)) {
|
|
1096
|
+
throw new VcmError({
|
|
1097
|
+
code: "MEMORY_REVIEW_UPDATE_MISMATCH",
|
|
1098
|
+
message: `Updated memory content is missing from ${memoryPath}: ${decision.finalContent}`,
|
|
1099
|
+
statusCode: 409
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function validateDurableDocAssignmentInput(assignment, after) {
|
|
1105
|
+
if (!MEMORY_FILE_DEFINITIONS.some((definition) => definition.path === assignment.sourceMemoryPath)) {
|
|
1106
|
+
throw new VcmError({
|
|
1107
|
+
code: "MEMORY_REVIEW_ASSIGNMENT_SOURCE_INVALID",
|
|
1108
|
+
message: `Unknown memory source path: ${assignment.sourceMemoryPath}`,
|
|
1109
|
+
statusCode: 409
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (substantiveMemoryEntries(after[assignment.sourceMemoryPath]).includes(assignment.sourceEntry)) {
|
|
1113
|
+
throw new VcmError({
|
|
1114
|
+
code: "MEMORY_REVIEW_ASSIGNMENT_SOURCE_NOT_REMOVED",
|
|
1115
|
+
message: `Move-to-durable-doc must remove memory before assignment: ${assignment.sourceEntry}`,
|
|
1116
|
+
statusCode: 409
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
assertDurableDocPath(assignment.targetPath);
|
|
658
1120
|
}
|
|
659
1121
|
async function createAppliedRun(taskRepoRoot, taskSlug, source, before, after) {
|
|
660
1122
|
assertCompleteMemorySet(after);
|
|
@@ -663,7 +1125,7 @@ export function createAutoMemoryService(deps) {
|
|
|
663
1125
|
await writeRunMemorySet(taskRepoRoot, runId, "before", before);
|
|
664
1126
|
await writeRunMemorySet(taskRepoRoot, runId, "after", after);
|
|
665
1127
|
try {
|
|
666
|
-
await applyAndCommitMemorySet(taskRepoRoot, before, after, "
|
|
1128
|
+
await applyAndCommitMemorySet(taskRepoRoot, before, after, "[VCM Harness] Update VCM memory");
|
|
667
1129
|
const diff = renderMemoryDiff(before, after);
|
|
668
1130
|
await persistRun(taskRepoRoot, {
|
|
669
1131
|
version: 1,
|
|
@@ -729,6 +1191,7 @@ export function createAutoMemoryService(deps) {
|
|
|
729
1191
|
finalAcceptanceHash: run.finalAcceptanceHash,
|
|
730
1192
|
trigger: run.trigger,
|
|
731
1193
|
diff: run.diff ?? "",
|
|
1194
|
+
assignments: run.assignments ?? [],
|
|
732
1195
|
canRevert: run.status === "applied" && !run.revertedAt && Boolean(run.afterHashes),
|
|
733
1196
|
error: run.error
|
|
734
1197
|
});
|
|
@@ -780,6 +1243,7 @@ export function createAutoMemoryService(deps) {
|
|
|
780
1243
|
const migrated = stored.drafts.some((draft) => draft.status === "running");
|
|
781
1244
|
const state = {
|
|
782
1245
|
...stored,
|
|
1246
|
+
assignments: stored.assignments ?? [],
|
|
783
1247
|
drafts: stored.drafts.map((draft) => ({
|
|
784
1248
|
...draft,
|
|
785
1249
|
status: draft.status === "running" ? "dispatched" : draft.status
|
|
@@ -840,6 +1304,8 @@ export function createAutoMemoryService(deps) {
|
|
|
840
1304
|
updateFile,
|
|
841
1305
|
revertRun,
|
|
842
1306
|
retryFailedReview,
|
|
1307
|
+
retryDurableDocAssignment,
|
|
1308
|
+
resolveDurableDocAssignmentOwner,
|
|
843
1309
|
prepareTaskRetrospectiveReview,
|
|
844
1310
|
cancelTaskRetrospectiveReview,
|
|
845
1311
|
isRoleMemoryTurn,
|
|
@@ -863,6 +1329,13 @@ export async function assertMemoryBlocksInstalled(fs, taskRepoRoot) {
|
|
|
863
1329
|
function currentDraft(state) {
|
|
864
1330
|
return state.drafts.find((draft) => draft.status !== "completed");
|
|
865
1331
|
}
|
|
1332
|
+
function preserveDisabledReview(state) {
|
|
1333
|
+
return state?.status === "documenting"
|
|
1334
|
+
|| (state?.status === "reviewing" && Boolean(state.reviewPromptDispatchedAt));
|
|
1335
|
+
}
|
|
1336
|
+
function currentDurableDocAssignment(state) {
|
|
1337
|
+
return state.assignments.find((assignment) => assignment.status !== "completed");
|
|
1338
|
+
}
|
|
866
1339
|
function toReviewCandidates(source, currentRole, items) {
|
|
867
1340
|
return items.map((item) => ({
|
|
868
1341
|
id: `${source}:${item.operation}:${item.ordinal}`,
|
|
@@ -883,6 +1356,7 @@ function toActiveReview(state) {
|
|
|
883
1356
|
updatedAt: state.updatedAt,
|
|
884
1357
|
currentRole: state.status === "collecting" ? currentDraft(state)?.role : undefined,
|
|
885
1358
|
drafts: state.drafts,
|
|
1359
|
+
assignments: state.assignments,
|
|
886
1360
|
trigger: state.trigger,
|
|
887
1361
|
error: state.error
|
|
888
1362
|
};
|
|
@@ -913,6 +1387,160 @@ function buildRoleDraftPrompt(taskRepoRoot, state, draft, planningCandidatePath)
|
|
|
913
1387
|
"End the turn after VCM accepts the proposal."
|
|
914
1388
|
].join("\n");
|
|
915
1389
|
}
|
|
1390
|
+
function buildDurableDocOwnerPrompt(taskRepoRoot, assignment) {
|
|
1391
|
+
return [
|
|
1392
|
+
"[VCM Durable Documentation Owner Resolution]",
|
|
1393
|
+
"",
|
|
1394
|
+
`Task worktree: ${taskRepoRoot}`,
|
|
1395
|
+
`Assignment ID: ${assignment.id}`,
|
|
1396
|
+
`Target document: ${assignment.targetPath}`,
|
|
1397
|
+
`Content to preserve: ${assignment.content}`,
|
|
1398
|
+
`Reason: ${assignment.reason}`,
|
|
1399
|
+
"",
|
|
1400
|
+
"Choose architect, coder, or tester as the document owner. If the correct owner cannot be determined, ask the user and wait for the answer.",
|
|
1401
|
+
`After deciding, run: .ai/tools/resolve-durable-doc-assignment --assignment ${assignment.id} --owner <architect|coder|tester>`,
|
|
1402
|
+
"End the turn after VCM accepts the owner."
|
|
1403
|
+
].join("\n");
|
|
1404
|
+
}
|
|
1405
|
+
function buildDurableDocAssignmentPrompt(taskRepoRoot, assignment) {
|
|
1406
|
+
return [
|
|
1407
|
+
"[VCM Durable Documentation Assignment]",
|
|
1408
|
+
"",
|
|
1409
|
+
`Task worktree: ${taskRepoRoot}`,
|
|
1410
|
+
`Assignment ID: ${assignment.id}`,
|
|
1411
|
+
`Target document: ${assignment.targetPath}`,
|
|
1412
|
+
`Content to preserve: ${assignment.content}`,
|
|
1413
|
+
`Reason: ${assignment.reason}`,
|
|
1414
|
+
"Evidence:",
|
|
1415
|
+
...assignment.evidence.map((item) => `- ${item}`),
|
|
1416
|
+
"",
|
|
1417
|
+
"Verify the content, update the target and any directly related durable documentation, run applicable documentation checks, and commit the documentation changes.",
|
|
1418
|
+
"Submit .ai/vcm/handoffs/docs-update-report.md through vcm-artifact. Set its Assignment ID to the exact ID above and record the final commit.",
|
|
1419
|
+
"End the turn after VCM accepts the report."
|
|
1420
|
+
].join("\n");
|
|
1421
|
+
}
|
|
1422
|
+
function inferDurableDocAssignmentOwner(targetPath) {
|
|
1423
|
+
const normalized = normalizeProjectRelativePath(targetPath);
|
|
1424
|
+
if (normalized === "docs/TESTING.md") {
|
|
1425
|
+
return "tester";
|
|
1426
|
+
}
|
|
1427
|
+
if (normalized === "docs/ARCHITECTURE.md"
|
|
1428
|
+
|| normalized === "docs/known-issues.md"
|
|
1429
|
+
|| normalized.endsWith("/ARCHITECTURE.md")) {
|
|
1430
|
+
return "architect";
|
|
1431
|
+
}
|
|
1432
|
+
return undefined;
|
|
1433
|
+
}
|
|
1434
|
+
function memoryPathToTarget(memoryPath) {
|
|
1435
|
+
if (memoryPath === "CLAUDE.md") {
|
|
1436
|
+
return "shared";
|
|
1437
|
+
}
|
|
1438
|
+
const definition = MEMORY_FILE_DEFINITIONS.find((candidate) => candidate.path === memoryPath);
|
|
1439
|
+
if (!definition || !("role" in definition)) {
|
|
1440
|
+
throw new Error(`Unknown memory path: ${memoryPath}`);
|
|
1441
|
+
}
|
|
1442
|
+
return definition.role;
|
|
1443
|
+
}
|
|
1444
|
+
function memoryTargetToPath(target) {
|
|
1445
|
+
if (target === "shared") {
|
|
1446
|
+
return "CLAUDE.md";
|
|
1447
|
+
}
|
|
1448
|
+
const definition = MEMORY_FILE_DEFINITIONS.find((candidate) => "role" in candidate && candidate.role === target);
|
|
1449
|
+
if (!definition) {
|
|
1450
|
+
throw new Error(`Unknown memory target: ${target}`);
|
|
1451
|
+
}
|
|
1452
|
+
return definition.path;
|
|
1453
|
+
}
|
|
1454
|
+
function substantiveMemoryEntries(content) {
|
|
1455
|
+
return content
|
|
1456
|
+
.split(/\r?\n/)
|
|
1457
|
+
.map((line) => line.trim())
|
|
1458
|
+
.filter(Boolean);
|
|
1459
|
+
}
|
|
1460
|
+
function assertDurableDocPath(targetPath) {
|
|
1461
|
+
const normalized = normalizeProjectRelativePath(targetPath);
|
|
1462
|
+
if (!normalized
|
|
1463
|
+
|| normalized.startsWith("../")
|
|
1464
|
+
|| path.posix.isAbsolute(normalized)
|
|
1465
|
+
|| normalized.startsWith(".ai/vcm/")
|
|
1466
|
+
|| !normalized.endsWith(".md")) {
|
|
1467
|
+
throw new VcmError({
|
|
1468
|
+
code: "MEMORY_REVIEW_DURABLE_DOC_PATH_INVALID",
|
|
1469
|
+
message: `Durable-document target must be a project-relative Markdown path outside .ai/vcm: ${targetPath}`,
|
|
1470
|
+
statusCode: 409
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
function normalizeProjectRelativePath(filePath) {
|
|
1475
|
+
return filePath.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1476
|
+
}
|
|
1477
|
+
function validateMemoryReviewResultShape(result, runId) {
|
|
1478
|
+
if (!result || typeof result !== "object") {
|
|
1479
|
+
return "root value must be an object";
|
|
1480
|
+
}
|
|
1481
|
+
if (result.version !== 1) {
|
|
1482
|
+
return "version must be 1";
|
|
1483
|
+
}
|
|
1484
|
+
if (result.runId !== runId) {
|
|
1485
|
+
return `runId must be ${runId}`;
|
|
1486
|
+
}
|
|
1487
|
+
if (typeof result.memoryCommit !== "string" || !result.memoryCommit.trim()) {
|
|
1488
|
+
return "memoryCommit must be a non-empty string";
|
|
1489
|
+
}
|
|
1490
|
+
if (!Array.isArray(result.decisions) || !Array.isArray(result.durableDocAssignments)) {
|
|
1491
|
+
return "decisions and durableDocAssignments must be arrays";
|
|
1492
|
+
}
|
|
1493
|
+
const allowedSources = new Set(["existing", "proposal"]);
|
|
1494
|
+
const allowedTargets = new Set(["shared", "project-manager", "architect", "coder", "tester", "reviewer", "harness-engineer"]);
|
|
1495
|
+
const allowedDecisions = new Set([
|
|
1496
|
+
"retain",
|
|
1497
|
+
"update",
|
|
1498
|
+
"remove",
|
|
1499
|
+
"move-to-durable-doc",
|
|
1500
|
+
"keep-in-memory",
|
|
1501
|
+
"keep-memory-reference",
|
|
1502
|
+
"reject"
|
|
1503
|
+
]);
|
|
1504
|
+
for (const decision of result.decisions) {
|
|
1505
|
+
if (!decision
|
|
1506
|
+
|| typeof decision.itemId !== "string"
|
|
1507
|
+
|| !allowedSources.has(decision.source)
|
|
1508
|
+
|| !allowedTargets.has(decision.target)
|
|
1509
|
+
|| typeof decision.entry !== "string"
|
|
1510
|
+
|| !decision.entry.trim()
|
|
1511
|
+
|| !allowedDecisions.has(decision.decision)
|
|
1512
|
+
|| typeof decision.reason !== "string"
|
|
1513
|
+
|| !decision.reason.trim()
|
|
1514
|
+
|| typeof decision.impactIfAbsent !== "string"
|
|
1515
|
+
|| !decision.impactIfAbsent.trim()
|
|
1516
|
+
|| !Array.isArray(decision.evidence)
|
|
1517
|
+
|| decision.evidence.length === 0
|
|
1518
|
+
|| decision.evidence.some((item) => typeof item !== "string" || !item.trim())
|
|
1519
|
+
|| typeof decision.finalContent !== "string"
|
|
1520
|
+
|| !decision.finalContent.trim()
|
|
1521
|
+
|| typeof decision.durableDocPath !== "string"
|
|
1522
|
+
|| !decision.durableDocPath.trim()) {
|
|
1523
|
+
return "every decision must use the complete review-result decision schema";
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
for (const assignment of result.durableDocAssignments) {
|
|
1527
|
+
if (!assignment
|
|
1528
|
+
|| typeof assignment.sourceMemoryPath !== "string"
|
|
1529
|
+
|| typeof assignment.sourceEntry !== "string"
|
|
1530
|
+
|| !assignment.sourceEntry.trim()
|
|
1531
|
+
|| typeof assignment.targetPath !== "string"
|
|
1532
|
+
|| typeof assignment.content !== "string"
|
|
1533
|
+
|| !assignment.content.trim()
|
|
1534
|
+
|| typeof assignment.reason !== "string"
|
|
1535
|
+
|| !assignment.reason.trim()
|
|
1536
|
+
|| !Array.isArray(assignment.evidence)
|
|
1537
|
+
|| assignment.evidence.length === 0
|
|
1538
|
+
|| assignment.evidence.some((item) => typeof item !== "string" || !item.trim())) {
|
|
1539
|
+
return "every durableDocAssignment must use the complete assignment schema";
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return undefined;
|
|
1543
|
+
}
|
|
916
1544
|
function requireMemoryFileDefinition(filePath) {
|
|
917
1545
|
const normalized = filePath.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
918
1546
|
const definition = MEMORY_FILE_DEFINITIONS.find((candidate) => candidate.path === normalized);
|