vibe-coding-master 0.8.5 → 0.8.7
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 +20 -4
- package/dist/backend/server.js +1 -1
- package/dist/backend/services/artifact-service.js +15 -6
- package/dist/backend/services/auto-memory-service.js +127 -51
- package/dist/backend/services/claude-hook-service.js +1 -0
- package/dist/backend/services/harness-feedback-service.js +14 -1
- package/dist/backend/services/memory-review-paths.js +3 -0
- package/dist/backend/templates/handoff.js +3 -1
- package/dist/backend/templates/harness/architect-agent.js +2 -1
- package/dist/backend/templates/harness/coder-agent.js +2 -1
- package/dist/backend/templates/harness/tester-agent.js +2 -2
- package/dist/shared/validation/artifact-check.js +8 -2
- package/dist/shared/validation/artifact-contract.js +2 -0
- package/dist-frontend/assets/{index-c2YwYw6n.js → index-CqZm9xCA.js} +31 -31
- package/dist-frontend/assets/{index-BIFZPhDN.css → index-xgiQeUD5.css} +1 -1
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -1
package/README.md
CHANGED
|
@@ -589,10 +589,26 @@ before documentation work starts. VCM assigns architecture and known-issues
|
|
|
589
589
|
documents to Architect and testing documentation to Tester. For another target,
|
|
590
590
|
PM selects Architect, Coder, or Tester and asks the user only when ownership
|
|
591
591
|
cannot be determined. These assignments are backend-dispatched work rather than
|
|
592
|
-
a Docs-Only Flow. Each owner
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
592
|
+
a Docs-Only Flow. Each owner submits `docs-update-report` through `vcm-artifact`
|
|
593
|
+
with the exact assigned `--path` and Assignment ID. Reports are isolated under
|
|
594
|
+
`.ai/vcm/memory-review/runs/<run-id>/assignments/<assignment-id>/report.md`.
|
|
595
|
+
The reported commit must modify the target document within the assignment's
|
|
596
|
+
original commit range; unrelated later commits do not invalidate it. Existing
|
|
597
|
+
correct documentation can be reported as `unchanged` without a new commit.
|
|
598
|
+
For `synced`, the entire `Commit` section is one bare lowercase hexadecimal
|
|
599
|
+
hash (7-40 characters), without Markdown or explanatory text. The template,
|
|
600
|
+
artifact submission, and assignment completion use the same format contract.
|
|
601
|
+
|
|
602
|
+
Assignments run one at a time. A failed assignment does not block later items.
|
|
603
|
+
After the remaining items finish, unresolved failures are reported in Harness
|
|
604
|
+
Studio with their causes and individual Retry buttons. Retry preserves earlier
|
|
605
|
+
committed work and the original baseline, including after a backend restart.
|
|
606
|
+
An old assignment whose baseline was already overwritten is not automatically
|
|
607
|
+
rebased; verify the current document and use `unchanged` when it already preserves
|
|
608
|
+
the assigned content. Errors identify the reported commit and recorded range.
|
|
609
|
+
Migration content and review snapshots remain in the task-local run directory;
|
|
610
|
+
failures do not automatically restore removed memory. Turning Auto Memory off
|
|
611
|
+
does not discard or cancel already-applied document migrations.
|
|
596
612
|
|
|
597
613
|
When Auto Memory is disabled, Review Task Harness does not collect proposals or
|
|
598
614
|
ask Harness Engineer to update memory. When enabled, both automatic and manual
|
package/dist/backend/server.js
CHANGED
|
@@ -246,7 +246,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
246
246
|
const workflowControlService = createWorkflowControlService({ fs });
|
|
247
247
|
const artifactService = createArtifactService(fs, {
|
|
248
248
|
workflowControlService,
|
|
249
|
-
|
|
249
|
+
getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role)
|
|
250
250
|
});
|
|
251
251
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
252
252
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
|
|
3
|
-
import { checkMarkdownArtifact } from "../../shared/validation/artifact-check.js";
|
|
3
|
+
import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
4
4
|
import { getManagedArtifactDefinition, isArtifactKind, isManagedArtifactKind } from "../../shared/validation/artifact-registry.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
@@ -208,10 +208,21 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
if (isArtifactKind(input.kind)) {
|
|
211
|
-
|
|
211
|
+
const assignment = input.kind === "docs-update-report"
|
|
212
|
+
? await deps.getDurableDocAssignment?.(input.repoRoot, input.role)
|
|
213
|
+
: undefined;
|
|
214
|
+
if (assignment) {
|
|
215
|
+
if (input.artifactPath !== assignment.reportPath) {
|
|
216
|
+
throw artifactRejected(input.kind, [`Use the assigned --path ${assignment.reportPath}.`]);
|
|
217
|
+
}
|
|
218
|
+
if (readArtifactSectionValue(normalized, "Assignment ID")?.trim() !== assignment.id) {
|
|
219
|
+
throw artifactRejected(input.kind, [`Assignment ID must be exactly ${assignment.id}.`]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (input.artifactPath?.trim()) {
|
|
212
223
|
throw artifactRejected(input.kind, ["--path is not allowed for fixed artifact kinds."]);
|
|
213
224
|
}
|
|
214
|
-
const artifactPath = path.posix.join(input.handoffDir, definition.fileName ?? "");
|
|
225
|
+
const artifactPath = assignment?.reportPath ?? path.posix.join(input.handoffDir, definition.fileName ?? "");
|
|
215
226
|
const validation = validateManagedArtifactContent(input.kind, normalized, {
|
|
216
227
|
path: artifactPath,
|
|
217
228
|
mode: input.mode
|
|
@@ -222,9 +233,7 @@ export function createArtifactService(fs, deps = {}) {
|
|
|
222
233
|
if ((input.kind === "docs-update-report" || input.kind === "docs-sync-report")
|
|
223
234
|
&& deps.workflowControlService) {
|
|
224
235
|
// Memory-owned documentation work is independent of PM's active dispatch.
|
|
225
|
-
|
|
226
|
-
&& await deps.isRoleMemoryTurn?.(input.repoRoot, input.role);
|
|
227
|
-
if (!memoryUpdate) {
|
|
236
|
+
if (!assignment) {
|
|
228
237
|
await deps.workflowControlService.assertDocsArtifactAllowed({
|
|
229
238
|
taskRepoRoot: input.repoRoot,
|
|
230
239
|
stateRoot: input.stateRoot ?? ".ai/vcm",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
3
|
+
import { checkMarkdownArtifact, readArtifactSectionContent, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
4
|
+
import { DOCS_UPDATE_COMMIT_RULE } from "../../shared/validation/artifact-contract.js";
|
|
4
5
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
6
|
import { VcmError } from "../errors.js";
|
|
6
7
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
8
|
import { readVcmMemoryHostFrame, readVcmMemoryBlock, replaceVcmMemoryBlock } from "../templates/harness/memory-block.js";
|
|
8
|
-
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, durableDocAssignmentReportPath, memoryReviewRoleDraftPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
10
|
import { parseMemoryProposal, validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
11
|
const MEMORY_FILE_DEFINITIONS = [
|
|
11
12
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
@@ -18,6 +19,20 @@ const MEMORY_FILE_DEFINITIONS = [
|
|
|
18
19
|
];
|
|
19
20
|
export function createAutoMemoryService(deps) {
|
|
20
21
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
22
|
+
const taskLocks = new Map();
|
|
23
|
+
async function withTaskLock(taskRepoRoot, operation) {
|
|
24
|
+
const previous = taskLocks.get(taskRepoRoot) ?? Promise.resolve();
|
|
25
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
26
|
+
taskLocks.set(taskRepoRoot, current);
|
|
27
|
+
try {
|
|
28
|
+
return await current;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
if (taskLocks.get(taskRepoRoot) === current) {
|
|
32
|
+
taskLocks.delete(taskRepoRoot);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
async function readMemorySet(repoRoot) {
|
|
22
37
|
const memory = {};
|
|
23
38
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
@@ -151,7 +166,7 @@ export function createAutoMemoryService(deps) {
|
|
|
151
166
|
else if (active?.status === "reviewing" && active.reviewPromptDispatchedAt) {
|
|
152
167
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
153
168
|
}
|
|
154
|
-
else if (active) {
|
|
169
|
+
else if (active && !preserveDisabledReview(active)) {
|
|
155
170
|
await discardActiveReview(input.taskRepoRoot, active);
|
|
156
171
|
}
|
|
157
172
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
@@ -361,6 +376,11 @@ export function createAutoMemoryService(deps) {
|
|
|
361
376
|
}
|
|
362
377
|
return assignment.status === "running" && assignment.owner === role;
|
|
363
378
|
}
|
|
379
|
+
async function getDurableDocAssignment(taskRepoRoot, role) {
|
|
380
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
381
|
+
const assignment = state?.status === "documenting" ? currentDurableDocAssignment(state) : undefined;
|
|
382
|
+
return assignment?.status === "running" && assignment.owner === role ? assignment : undefined;
|
|
383
|
+
}
|
|
364
384
|
async function handleRoleHook(input) {
|
|
365
385
|
const state = await loadActiveState(input.taskRepoRoot);
|
|
366
386
|
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
|
|
@@ -522,12 +542,19 @@ export function createAutoMemoryService(deps) {
|
|
|
522
542
|
statusCode: 409
|
|
523
543
|
});
|
|
524
544
|
}
|
|
545
|
+
if (state.assignments.length > 0) {
|
|
546
|
+
throw new VcmError({
|
|
547
|
+
code: "DURABLE_DOC_ASSIGNMENTS_FAILED",
|
|
548
|
+
message: "Memory was already applied. Retry the failed durable-document assignments individually.",
|
|
549
|
+
statusCode: 409
|
|
550
|
+
});
|
|
551
|
+
}
|
|
525
552
|
await clearActiveState(taskRepoRoot);
|
|
526
553
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
527
554
|
}
|
|
528
555
|
async function retryDurableDocAssignment(baseRepoRoot, taskRepoRoot, assignmentId) {
|
|
529
556
|
const state = await loadActiveState(taskRepoRoot);
|
|
530
|
-
const assignment = state
|
|
557
|
+
const assignment = state && (state.status === "documenting" || state.status === "failed")
|
|
531
558
|
? state.assignments.find((candidate) => candidate.id === assignmentId)
|
|
532
559
|
: undefined;
|
|
533
560
|
if (!state || !assignment || assignment.status !== "failed") {
|
|
@@ -539,11 +566,10 @@ export function createAutoMemoryService(deps) {
|
|
|
539
566
|
}
|
|
540
567
|
assignment.status = assignment.owner ? "pending" : "waiting-owner";
|
|
541
568
|
assignment.updatedAt = now();
|
|
542
|
-
delete assignment.error;
|
|
543
569
|
delete assignment.requestedOwner;
|
|
544
570
|
delete assignment.dispatchedAt;
|
|
545
|
-
|
|
546
|
-
delete
|
|
571
|
+
state.status = "documenting";
|
|
572
|
+
delete state.error;
|
|
547
573
|
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
548
574
|
await dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state);
|
|
549
575
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
@@ -620,6 +646,7 @@ export function createAutoMemoryService(deps) {
|
|
|
620
646
|
assignment.error = `${input.role} durable-document assignment turn failed.`;
|
|
621
647
|
assignment.updatedAt = now();
|
|
622
648
|
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
649
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
623
650
|
return true;
|
|
624
651
|
}
|
|
625
652
|
if (input.eventName !== "Stop") {
|
|
@@ -644,13 +671,6 @@ export function createAutoMemoryService(deps) {
|
|
|
644
671
|
assignment.updatedAt = assignment.completedAt;
|
|
645
672
|
delete assignment.error;
|
|
646
673
|
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
647
|
-
const next = currentDurableDocAssignment(state);
|
|
648
|
-
if (next) {
|
|
649
|
-
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
await clearActiveState(input.taskRepoRoot);
|
|
653
|
-
}
|
|
654
674
|
}
|
|
655
675
|
catch (error) {
|
|
656
676
|
assignment.status = "failed";
|
|
@@ -658,11 +678,13 @@ export function createAutoMemoryService(deps) {
|
|
|
658
678
|
assignment.updatedAt = now();
|
|
659
679
|
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
660
680
|
}
|
|
681
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
661
682
|
return true;
|
|
662
683
|
}
|
|
663
684
|
async function dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state) {
|
|
664
|
-
|
|
665
|
-
if (!assignment
|
|
685
|
+
let assignment = currentDurableDocAssignment(state);
|
|
686
|
+
if (!assignment) {
|
|
687
|
+
await finishDurableDocAssignments(taskRepoRoot, state);
|
|
666
688
|
return;
|
|
667
689
|
}
|
|
668
690
|
if (assignment.status === "running" || assignment.status === "resolving-owner") {
|
|
@@ -678,28 +700,50 @@ export function createAutoMemoryService(deps) {
|
|
|
678
700
|
assignment.updatedAt = now();
|
|
679
701
|
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
680
702
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (session.activityStatus === "running") {
|
|
703
|
+
while (assignment) {
|
|
704
|
+
try {
|
|
705
|
+
await dispatchDurableDocAssignment(baseRepoRoot, taskRepoRoot, state, assignment);
|
|
685
706
|
return;
|
|
686
707
|
}
|
|
687
|
-
|
|
708
|
+
catch (error) {
|
|
709
|
+
assignment.status = "failed";
|
|
710
|
+
assignment.error = `Unable to dispatch durable-document assignment: ${errorMessage(error)}`;
|
|
711
|
+
assignment.updatedAt = now();
|
|
712
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
713
|
+
assignment = currentDurableDocAssignment(state);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
await finishDurableDocAssignments(taskRepoRoot, state);
|
|
717
|
+
}
|
|
718
|
+
async function dispatchDurableDocAssignment(baseRepoRoot, taskRepoRoot, state, assignment) {
|
|
719
|
+
const session = await ensureWorkflowRoleSession(baseRepoRoot, state.taskSlug, assignment.owner ?? "project-manager");
|
|
720
|
+
if (session.activityStatus === "running") {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
assignment.status = assignment.owner ? "running" : "resolving-owner";
|
|
724
|
+
assignment.reportPath = durableDocAssignmentReportPath(assignment.runId, assignment.id);
|
|
725
|
+
if (!assignment.baseCommit) {
|
|
688
726
|
assignment.baseCommit = await deps.git.getHeadCommit(taskRepoRoot);
|
|
689
727
|
assignment.reportHashBefore = await hashOptionalFile(taskRepoRoot, assignment.reportPath);
|
|
690
|
-
assignment.dispatchedAt = now();
|
|
691
|
-
assignment.updatedAt = assignment.dispatchedAt;
|
|
692
|
-
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
693
|
-
await submitTerminalInput(deps.runtime, session.id, assignment.owner
|
|
694
|
-
? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
|
|
695
|
-
: buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
|
|
696
728
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
729
|
+
assignment.dispatchedAt = now();
|
|
730
|
+
assignment.updatedAt = assignment.dispatchedAt;
|
|
731
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
732
|
+
await submitTerminalInput(deps.runtime, session.id, assignment.owner
|
|
733
|
+
? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
|
|
734
|
+
: buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
|
|
735
|
+
}
|
|
736
|
+
async function finishDurableDocAssignments(taskRepoRoot, state) {
|
|
737
|
+
const failed = state.assignments.filter((assignment) => assignment.status === "failed");
|
|
738
|
+
if (failed.length > 0) {
|
|
739
|
+
state.status = "failed";
|
|
740
|
+
state.error = `Memory was applied, but ${failed.length} durable-document assignment(s) failed: ${failed.map((assignment) => assignment.targetPath).join(", ")}. Retry the failed assignments; their content is retained in the memory review run.`;
|
|
701
741
|
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
742
|
+
return;
|
|
702
743
|
}
|
|
744
|
+
delete state.error;
|
|
745
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
746
|
+
await clearActiveState(taskRepoRoot);
|
|
703
747
|
}
|
|
704
748
|
async function validateDurableDocAssignmentCompletion(taskRepoRoot, assignment) {
|
|
705
749
|
const reportAbsolutePath = resolveRepoPath(taskRepoRoot, assignment.reportPath);
|
|
@@ -727,22 +771,46 @@ export function createAutoMemoryService(deps) {
|
|
|
727
771
|
if (decision !== "synced" && decision !== "unchanged") {
|
|
728
772
|
throw new Error("Decision must be synced or unchanged");
|
|
729
773
|
}
|
|
774
|
+
if (!(await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, assignment.targetPath)))) {
|
|
775
|
+
throw new Error(`target document is missing: ${assignment.targetPath}`);
|
|
776
|
+
}
|
|
730
777
|
const currentHead = await deps.git.getHeadCommit(taskRepoRoot);
|
|
731
778
|
if (decision === "synced") {
|
|
732
|
-
|
|
733
|
-
throw new Error("documentation changes were not committed");
|
|
734
|
-
}
|
|
735
|
-
const changedPaths = await deps.git.getChangedPaths(taskRepoRoot, assignment.baseCommit, currentHead);
|
|
736
|
-
if (!changedPaths.includes(assignment.targetPath)) {
|
|
737
|
-
throw new Error(`documentation commit does not include ${assignment.targetPath}`);
|
|
738
|
-
}
|
|
739
|
-
const reportedCommit = readArtifactSectionValue(reportContent, "Commit")?.trim();
|
|
740
|
-
if (!reportedCommit || reportedCommit === "TBD" || !currentHead.startsWith(reportedCommit)) {
|
|
741
|
-
throw new Error(`Commit must identify the current documentation commit ${currentHead}`);
|
|
742
|
-
}
|
|
779
|
+
return validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead);
|
|
743
780
|
}
|
|
744
781
|
return currentHead;
|
|
745
782
|
}
|
|
783
|
+
async function validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead) {
|
|
784
|
+
const reportedCommit = readArtifactSectionContent(reportContent, "Commit");
|
|
785
|
+
let commit;
|
|
786
|
+
try {
|
|
787
|
+
commit = (await deps.git.getCommitInfo(taskRepoRoot, reportedCommit)).sha;
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
const detail = error instanceof VcmError && error.hint ? `${errorMessage(error)} ${error.hint}` : errorMessage(error);
|
|
791
|
+
throw new Error(`Commit ${reportedCommit} could not be resolved to a unique Git commit: ${detail}`);
|
|
792
|
+
}
|
|
793
|
+
const existingContentHint = `If ${assignment.targetPath} already preserves the assigned content, report unchanged with verification evidence; do not create a redundant commit.`;
|
|
794
|
+
if (!assignment.baseCommit) {
|
|
795
|
+
throw new Error(`Assignment ${assignment.id} has no recorded commit baseline; cannot validate Commit ${commit}. ${existingContentHint}`);
|
|
796
|
+
}
|
|
797
|
+
if (currentHead === assignment.baseCommit) {
|
|
798
|
+
throw new Error(`No commits follow assignment baseline ${assignment.baseCommit} (HEAD is the same commit); received Commit ${commit}. The document may already have been committed before this baseline. ${existingContentHint}`);
|
|
799
|
+
}
|
|
800
|
+
const commits = await deps.git.getCommitList(taskRepoRoot, `${assignment.baseCommit}..${currentHead}`);
|
|
801
|
+
if (!commits.some((candidate) => candidate.sha === commit)) {
|
|
802
|
+
throw new Error(`Commit ${commit} exists but is outside assignment range ${assignment.baseCommit}..${currentHead}. ${existingContentHint}`);
|
|
803
|
+
}
|
|
804
|
+
const commitPaths = await deps.git.getChangedPaths(taskRepoRoot, `${commit}^`, commit);
|
|
805
|
+
if (!commitPaths.includes(assignment.targetPath)) {
|
|
806
|
+
throw new Error(`reported commit ${commit} does not modify ${assignment.targetPath}`);
|
|
807
|
+
}
|
|
808
|
+
const changedPaths = await deps.git.getChangedPaths(taskRepoRoot, assignment.baseCommit, currentHead);
|
|
809
|
+
if (!changedPaths.includes(assignment.targetPath)) {
|
|
810
|
+
throw new Error(`Commit ${commit} modified ${assignment.targetPath}, but no net change remains in ${assignment.baseCommit}..${currentHead}. Verify the current document before reporting completion.`);
|
|
811
|
+
}
|
|
812
|
+
return commit;
|
|
813
|
+
}
|
|
746
814
|
async function hashOptionalFile(taskRepoRoot, relativePath) {
|
|
747
815
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
748
816
|
return await deps.fs.pathExists(absolutePath)
|
|
@@ -756,6 +824,7 @@ export function createAutoMemoryService(deps) {
|
|
|
756
824
|
await persistRun(taskRepoRoot, {
|
|
757
825
|
...run,
|
|
758
826
|
assignments: state.assignments,
|
|
827
|
+
error: state.error,
|
|
759
828
|
updatedAt: state.updatedAt
|
|
760
829
|
});
|
|
761
830
|
}
|
|
@@ -897,7 +966,7 @@ export function createAutoMemoryService(deps) {
|
|
|
897
966
|
evidence: assignment.evidence,
|
|
898
967
|
...(owner ? { owner } : {}),
|
|
899
968
|
status: owner ? "pending" : "waiting-owner",
|
|
900
|
-
reportPath:
|
|
969
|
+
reportPath: durableDocAssignmentReportPath(state.runId, `${state.runId}-doc-${index + 1}`),
|
|
901
970
|
createdAt: timestamp,
|
|
902
971
|
updatedAt: timestamp
|
|
903
972
|
};
|
|
@@ -1317,20 +1386,21 @@ export function createAutoMemoryService(deps) {
|
|
|
1317
1386
|
return `sha256:${sha256(content)}`;
|
|
1318
1387
|
}
|
|
1319
1388
|
return {
|
|
1320
|
-
reconcileTask,
|
|
1389
|
+
reconcileTask: (input) => withTaskLock(input.taskRepoRoot, () => reconcileTask(input)),
|
|
1321
1390
|
getState,
|
|
1322
1391
|
getTaskRetrospectiveReadiness,
|
|
1323
1392
|
getFile,
|
|
1324
1393
|
updateFile,
|
|
1325
1394
|
revertRun,
|
|
1326
1395
|
retryFailedReview,
|
|
1327
|
-
retryDurableDocAssignment,
|
|
1328
|
-
resolveDurableDocAssignmentOwner,
|
|
1396
|
+
retryDurableDocAssignment: (base, task, id) => withTaskLock(task, () => retryDurableDocAssignment(base, task, id)),
|
|
1397
|
+
resolveDurableDocAssignmentOwner: (base, task, id, owner) => withTaskLock(task, () => resolveDurableDocAssignmentOwner(base, task, id, owner)),
|
|
1329
1398
|
prepareTaskRetrospectiveReview,
|
|
1330
1399
|
cancelTaskRetrospectiveReview,
|
|
1331
1400
|
isRoleMemoryTurn,
|
|
1332
|
-
|
|
1333
|
-
|
|
1401
|
+
getDurableDocAssignment,
|
|
1402
|
+
handleRoleHook: (input) => withTaskLock(input.taskRepoRoot, () => handleRoleHook(input)),
|
|
1403
|
+
handleHarnessEngineerHook: (input) => withTaskLock(input.taskRepoRoot, () => handleHarnessEngineerHook(input)),
|
|
1334
1404
|
assertHarnessEngineerAvailable
|
|
1335
1405
|
};
|
|
1336
1406
|
}
|
|
@@ -1350,11 +1420,12 @@ function currentDraft(state) {
|
|
|
1350
1420
|
return state.drafts.find((draft) => draft.status !== "completed");
|
|
1351
1421
|
}
|
|
1352
1422
|
function preserveDisabledReview(state) {
|
|
1353
|
-
return state?.
|
|
1423
|
+
return Boolean(state?.assignments.length)
|
|
1354
1424
|
|| (state?.status === "reviewing" && Boolean(state.reviewPromptDispatchedAt));
|
|
1355
1425
|
}
|
|
1356
1426
|
function currentDurableDocAssignment(state) {
|
|
1357
|
-
return state.assignments.find((assignment) => assignment.status
|
|
1427
|
+
return state.assignments.find((assignment) => assignment.status === "running" || assignment.status === "resolving-owner")
|
|
1428
|
+
?? state.assignments.find((assignment) => assignment.status === "pending" || assignment.status === "waiting-owner");
|
|
1358
1429
|
}
|
|
1359
1430
|
function toReviewCandidates(source, currentRole, items) {
|
|
1360
1431
|
return items.map((item) => ({
|
|
@@ -1435,7 +1506,12 @@ function buildDurableDocAssignmentPrompt(taskRepoRoot, assignment) {
|
|
|
1435
1506
|
...assignment.evidence.map((item) => `- ${item}`),
|
|
1436
1507
|
"",
|
|
1437
1508
|
"Verify the content, update the target and any directly related durable documentation, run applicable documentation checks, and commit the documentation changes.",
|
|
1438
|
-
|
|
1509
|
+
`Assigned report path: ${assignment.reportPath}`,
|
|
1510
|
+
`Submit with .ai/tools/vcm-artifact docs-update-report --file <candidate> --path ${assignment.reportPath} --mode final`,
|
|
1511
|
+
"Set Assignment ID to the exact ID above.",
|
|
1512
|
+
DOCS_UPDATE_COMMIT_RULE,
|
|
1513
|
+
"For synced, record the commit that changed the target document; it need not be HEAD. If the document already preserves the content, use unchanged with evidence; do not create a redundant commit.",
|
|
1514
|
+
...(assignment.error ? [`Previous failure: ${assignment.error}`] : []),
|
|
1439
1515
|
"End the turn after VCM accepts the report."
|
|
1440
1516
|
].join("\n");
|
|
1441
1517
|
}
|
|
@@ -160,6 +160,7 @@ export function createClaudeHookService(deps) {
|
|
|
160
160
|
taskSlug: activeTask.taskSlug,
|
|
161
161
|
eventName,
|
|
162
162
|
memoryReviewStatus: memoryState?.status ?? "idle",
|
|
163
|
+
memoryReviewHasAssignments: Boolean(memoryState?.active?.assignments.length),
|
|
163
164
|
memoryReviewError: memoryState?.active?.error
|
|
164
165
|
})
|
|
165
166
|
: false;
|
|
@@ -164,6 +164,7 @@ export function createHarnessFeedbackService(deps) {
|
|
|
164
164
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
165
165
|
...processedMarker,
|
|
166
166
|
status: "failed",
|
|
167
|
+
durableDocFailure: input.memoryReviewHasAssignments,
|
|
167
168
|
failedAt: timestamp,
|
|
168
169
|
updatedAt: timestamp,
|
|
169
170
|
error: input.memoryReviewError
|
|
@@ -185,17 +186,26 @@ export function createHarnessFeedbackService(deps) {
|
|
|
185
186
|
}
|
|
186
187
|
async function completeWaitingTaskRetrospective(repoRoot, taskSlug, memoryStatus) {
|
|
187
188
|
const marker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
188
|
-
if (!marker || marker.status !== "waiting-docs") {
|
|
189
|
+
if (!marker || (marker.status !== "waiting-docs" && !(marker.status === "failed" && marker.durableDocFailure))) {
|
|
189
190
|
return false;
|
|
190
191
|
}
|
|
191
192
|
if (memoryStatus === "documenting") {
|
|
193
|
+
if (marker.status === "failed") {
|
|
194
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
195
|
+
...marker, status: "waiting-docs", updatedAt: now(), error: undefined, failedAt: undefined
|
|
196
|
+
});
|
|
197
|
+
}
|
|
192
198
|
return true;
|
|
193
199
|
}
|
|
194
200
|
if (memoryStatus === "failed") {
|
|
201
|
+
if (marker.status === "failed") {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
195
204
|
const timestamp = now();
|
|
196
205
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
197
206
|
...marker,
|
|
198
207
|
status: "failed",
|
|
208
|
+
durableDocFailure: true,
|
|
199
209
|
failedAt: timestamp,
|
|
200
210
|
updatedAt: timestamp,
|
|
201
211
|
error: "Task Harness Retrospective durable-document assignment failed."
|
|
@@ -214,6 +224,9 @@ export function createHarnessFeedbackService(deps) {
|
|
|
214
224
|
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
215
225
|
...processedMarker,
|
|
216
226
|
status: "completed",
|
|
227
|
+
durableDocFailure: undefined,
|
|
228
|
+
error: undefined,
|
|
229
|
+
failedAt: undefined,
|
|
217
230
|
completedAt: timestamp,
|
|
218
231
|
updatedAt: timestamp
|
|
219
232
|
});
|
|
@@ -6,6 +6,9 @@ const MEMORY_REVIEW_ROLE_DRAFT_PATTERN = new RegExp(`^${escapeRegExp(MEMORY_REVI
|
|
|
6
6
|
export function memoryReviewRoleDraftPath(runId, role) {
|
|
7
7
|
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/drafts/${role}.md`;
|
|
8
8
|
}
|
|
9
|
+
export function durableDocAssignmentReportPath(runId, assignmentId) {
|
|
10
|
+
return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/assignments/${assignmentId}/report.md`;
|
|
11
|
+
}
|
|
9
12
|
export function isMemoryProposalSubmissionPath(artifactPath, role) {
|
|
10
13
|
if (artifactPath === ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH) {
|
|
11
14
|
return role === "architect";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
1
|
+
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_RULE, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS, renderArtifactOptions } from "../../shared/validation/artifact-contract.js";
|
|
2
2
|
const CURRENT_HANDOFF_NOTICE = "<!-- VCM current handoff: replace this file with one complete, self-contained snapshot of the current result. Restate all still-relevant evidence; do not refer to a prior revision, route message, Session, or transcript as evidence. -->";
|
|
3
3
|
export function renderArchitectureBriefTemplate(taskSlug) {
|
|
4
4
|
return `# Architecture Brief: ${taskSlug}
|
|
@@ -534,6 +534,8 @@ export function renderDocsUpdateReportTemplate(taskSlug, assignmentId = "docs-on
|
|
|
534
534
|
|
|
535
535
|
${CURRENT_HANDOFF_NOTICE}
|
|
536
536
|
|
|
537
|
+
${DOCS_UPDATE_COMMIT_RULE}
|
|
538
|
+
|
|
537
539
|
## Summary
|
|
538
540
|
|
|
539
541
|
TBD
|
|
@@ -301,7 +301,8 @@ Each rewritten \`architecture-diagnosis.md\` must be a complete, self-contained
|
|
|
301
301
|
- For a \`[VCM Durable Documentation Assignment]\`, update the assigned durable
|
|
302
302
|
document and directly related documentation, run applicable documentation
|
|
303
303
|
checks, commit the result, and submit \`docs-update-report.md\` with the exact
|
|
304
|
-
assigned Assignment ID.
|
|
304
|
+
assigned Assignment ID. Use \`vcm-artifact docs-update-report --path <assigned-report-path>\`;
|
|
305
|
+
do not use the shared Docs-Only report path. This backend assignment does not start Docs-Only Flow.
|
|
305
306
|
|
|
306
307
|
- In Docs-Only Flow, verify claims against current code and durable docs, update the PM-assigned project documents and related documentation needed for consistency, run applicable documentation checks, commit the changes, and submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` before reporting to PM.
|
|
307
308
|
- In Code-Change Flow, Architect Debug Flow, and a code-producing Architecture Diagnosis Flow, perform post-validation docs sync only when project-manager requests it after tester completes.
|
|
@@ -82,7 +82,8 @@ ${renderRoleMemoryRules("coder")}
|
|
|
82
82
|
- For a \`[VCM Durable Documentation Assignment]\`, verify and update the assigned
|
|
83
83
|
durable document and directly related documentation, run applicable checks,
|
|
84
84
|
commit the result, and submit \`docs-update-report.md\` with the exact assigned
|
|
85
|
-
Assignment ID.
|
|
85
|
+
Assignment ID. Use \`vcm-artifact docs-update-report --path <assigned-report-path>\`;
|
|
86
|
+
do not use the shared Docs-Only report path. This backend assignment does not start Docs-Only Flow.
|
|
86
87
|
|
|
87
88
|
- In Docs-Only Flow, commit the documentation changes and submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` with the decision, changed and reviewed documents, evidence, checks, commit, and remaining documentation issues. Do not submit \`coder-completion.md\` for Docs-Only work.
|
|
88
89
|
- Submit \`.ai/vcm/handoffs/coder-completion.md\` before routing back to project-manager: write a candidate outside \`.ai/vcm\`, then run \`.ai/tools/vcm-artifact coder-completion --file <candidate> --mode draft|final\`. This file is the complete, self-contained current implementation completion evidence, not a log. Each revision must restate every Scaffold Manifest disposition, changed file, helper, deviation, generated-context result, baseline-test change, L0/L1 command and result, worker result, commit, and objective failure still needed to review the current implementation without a prior revision. Replace stale content instead of appending history.
|
|
@@ -42,8 +42,8 @@ ${renderRoleMemoryRules("tester")}
|
|
|
42
42
|
- For a \`[VCM Durable Documentation Assignment]\`, verify and update the assigned
|
|
43
43
|
durable document and directly related documentation, run applicable
|
|
44
44
|
documentation checks, commit the result, and submit \`docs-update-report.md\`
|
|
45
|
-
with the exact assigned Assignment ID.
|
|
46
|
-
Docs-Only Flow.
|
|
45
|
+
with the exact assigned Assignment ID. Use \`vcm-artifact docs-update-report --path <assigned-report-path>\`;
|
|
46
|
+
do not use the shared Docs-Only report path. This backend assignment does not start Docs-Only Flow.
|
|
47
47
|
|
|
48
48
|
- Verify the assigned documentation against current project evidence, update the required documentation, run applicable documentation checks, and commit the changes.
|
|
49
49
|
- Submit \`.ai/vcm/handoffs/docs-update-report.md\` through \`vcm-artifact\` with the decision, changed and reviewed documents, evidence, checks, commit, and remaining documentation issues.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
|
|
1
|
+
import { ARCHITECT_DEBUG_DISPOSITIONS, ARCHITECT_DEBUG_STATUSES, ARCHITECTURE_BRIEF_STATUSES, ARCHITECTURE_DIAGNOSIS_DISPOSITIONS, ARCHITECTURE_EVIDENCE_STATUSES, ARCHITECTURE_PLAN_RESULTS, CODER_COMPLETION_DECISIONS, DOCS_REPORT_DECISIONS, DOCS_UPDATE_COMMIT_PATTERN, DOCS_UPDATE_COMMIT_RULE, DOCS_SYNC_CORRECTION_OWNERS, FINAL_ACCEPTANCE_DECISIONS, L3_ACTIONS, L3_REQUIRED_VALUES, PLANNING_PROGRESS_STATUSES, STRICT_NONE_VALUE, TEST_INFRASTRUCTURE_STATUSES, TEST_RESULTS } from "./artifact-contract.js";
|
|
2
2
|
import { getArtifactDefinition } from "./artifact-registry.js";
|
|
3
3
|
const PLACEHOLDER_PATTERN = /(^|\n)\s*(TBD|Not run yet\.?|status:\s*draft)\s*(\n|$)/i;
|
|
4
4
|
export function checkMarkdownArtifact(kind, artifactPath, content, options = {}) {
|
|
@@ -252,7 +252,13 @@ function validateArtifactFields(kind, content, mode) {
|
|
|
252
252
|
return invalidFields;
|
|
253
253
|
}
|
|
254
254
|
if (kind === "docs-update-report") {
|
|
255
|
-
|
|
255
|
+
const invalidFields = validateDecision(content, DOCS_REPORT_DECISIONS);
|
|
256
|
+
const decision = readArtifactSectionContent(content, "Decision")?.trim().toLowerCase();
|
|
257
|
+
const commit = readArtifactSectionContent(content, "Commit");
|
|
258
|
+
if (mode === "final" && decision === "synced" && !DOCS_UPDATE_COMMIT_PATTERN.test(commit ?? "")) {
|
|
259
|
+
invalidFields.push(`${DOCS_UPDATE_COMMIT_RULE} Received Commit: ${JSON.stringify(commit ?? "")}.`);
|
|
260
|
+
}
|
|
261
|
+
return invalidFields;
|
|
256
262
|
}
|
|
257
263
|
if (kind === "docs-sync-report") {
|
|
258
264
|
const invalidFields = validateDecision(content, DOCS_REPORT_DECISIONS);
|
|
@@ -30,6 +30,8 @@ export const TEST_INFRASTRUCTURE_STATUSES = [
|
|
|
30
30
|
export const L3_REQUIRED_VALUES = ["yes", "no"];
|
|
31
31
|
export const L3_ACTIONS = ["run-existing", "updated", "added"];
|
|
32
32
|
export const DOCS_REPORT_DECISIONS = ["synced", "unchanged", "blocked"];
|
|
33
|
+
export const DOCS_UPDATE_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/;
|
|
34
|
+
export const DOCS_UPDATE_COMMIT_RULE = "When Decision is synced, Commit must contain only one bare lowercase hexadecimal commit hash (7-40 characters, for example abc1234) on a single line. No backticks, bullets, links, subjects, or extra lines; put explanations in Evidence Reviewed. An unchanged result does not require a new commit.";
|
|
33
35
|
export const DOCS_SYNC_CORRECTION_OWNERS = ["none", "architect", "coder", "tester"];
|
|
34
36
|
export const FINAL_ACCEPTANCE_DECISIONS = [
|
|
35
37
|
"accepted",
|