vibe-coding-master 0.8.4 → 0.8.6
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 +23 -8
- package/dist/backend/api/task-routes.js +6 -0
- package/dist/backend/gateway/gateway-service.js +18 -3
- package/dist/backend/server.js +5 -2
- package/dist/backend/services/artifact-service.js +15 -6
- package/dist/backend/services/auto-memory-service.js +114 -50
- package/dist/backend/services/claude-hook-service.js +17 -1
- package/dist/backend/services/gate-review-service.js +4 -1
- package/dist/backend/services/harness-feedback-service.js +14 -1
- package/dist/backend/services/memory-review-paths.js +3 -0
- package/dist/backend/services/translation-service.js +49 -1
- package/dist/backend/services/user-question-reply.js +10 -0
- package/dist/backend/services/workflow-control-service.js +47 -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/project-manager-agent.js +1 -1
- package/dist/backend/templates/harness/tester-agent.js +2 -2
- package/dist/backend/templates/harness/vcm-ask-user-skill.js +2 -2
- package/dist/backend/templates/harness/vcm-workflow-review-skill.js +1 -1
- package/dist-frontend/assets/{index-ClaVB8jM.js → index-CqZm9xCA.js} +31 -31
- package/dist-frontend/assets/index-xgiQeUD5.css +32 -0
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -1
- package/dist-frontend/assets/index-Dc0_KSf4.css +0 -32
package/README.md
CHANGED
|
@@ -192,13 +192,18 @@ artifacts, Gate Review state, Round/Turn state, and the role rules remain
|
|
|
192
192
|
authoritative.
|
|
193
193
|
|
|
194
194
|
Whenever Project Manager asks the user a question, it first uses
|
|
195
|
-
`vcm-ask-user
|
|
196
|
-
|
|
195
|
+
`vcm-ask-user` with the complete question and necessary context. VCM records the
|
|
196
|
+
wait and cancels any pending role dispatch in one state update. Stop Hook then
|
|
197
|
+
ends the PM turn without route scanning and records the question as the official
|
|
198
|
+
reply. The existing Pause Alert displays it even when translation is off; the
|
|
199
|
+
translation panel and Gateway reuse that same reply instead of PM's separate
|
|
200
|
+
closing text. While waiting, new dispatches, Gate requests/retries, and workflow
|
|
201
|
+
completion are blocked; already-running roles may still return results. Only
|
|
197
202
|
a new direct user message clears the wait, and the next role dispatch requires
|
|
198
203
|
a fresh Workflow Review. PM treats text emitted while a Round continues as
|
|
199
204
|
intermediate output, not as delivered user communication. When the Round pauses
|
|
200
|
-
or completes, its Round Final Reply
|
|
201
|
-
question without relying on earlier PM output.
|
|
205
|
+
or completes, its Round Final Reply contains the complete user-facing result or
|
|
206
|
+
registered question without relying on earlier PM output.
|
|
202
207
|
|
|
203
208
|
Typical flow:
|
|
204
209
|
|
|
@@ -584,10 +589,20 @@ before documentation work starts. VCM assigns architecture and known-issues
|
|
|
584
589
|
documents to Architect and testing documentation to Tester. For another target,
|
|
585
590
|
PM selects Architect, Coder, or Tester and asks the user only when ownership
|
|
586
591
|
cannot be determined. These assignments are backend-dispatched work rather than
|
|
587
|
-
a Docs-Only Flow. Each owner
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
+
|
|
599
|
+
Assignments run one at a time. A failed assignment does not block later items.
|
|
600
|
+
After the remaining items finish, unresolved failures are reported in Harness
|
|
601
|
+
Studio with their causes and individual Retry buttons. Retry preserves earlier
|
|
602
|
+
committed work and the original baseline, including after a backend restart.
|
|
603
|
+
Migration content and review snapshots remain in the task-local run directory;
|
|
604
|
+
failures do not automatically restore removed memory. Turning Auto Memory off
|
|
605
|
+
does not discard or cancel already-applied document migrations.
|
|
591
606
|
|
|
592
607
|
When Auto Memory is disabled, Review Task Harness does not collect proposals or
|
|
593
608
|
ask Harness Engineer to update memory. When enabled, both automatic and manual
|
|
@@ -65,6 +65,12 @@ export function registerTaskRoutes(app, deps) {
|
|
|
65
65
|
taskSlug
|
|
66
66
|
}) ?? Promise.resolve(degradedWorkflowState(taskSlug))
|
|
67
67
|
]);
|
|
68
|
+
if (roundState.flowPause?.paused && deps.workflowControlService) {
|
|
69
|
+
const workflow = await deps.workflowControlService.getState(context);
|
|
70
|
+
const reply = workflow.userQuestionReplies?.find((item) => item.requestedAt === workflow.awaitingUser?.requestedAt);
|
|
71
|
+
if (reply)
|
|
72
|
+
roundState.flowPause = { ...roundState.flowPause, message: reply.question };
|
|
73
|
+
}
|
|
68
74
|
const displayedRoundState = await delayFlowPauseForTranslation(deps, {
|
|
69
75
|
repoRoot: project.repoRoot,
|
|
70
76
|
taskRepoRoot,
|
|
@@ -2,6 +2,7 @@ import { getRoleDefinition, isVcmRoleName, VCM_ROLE_NAMES } from "../../shared/c
|
|
|
2
2
|
import { VcmError } from "../errors.js";
|
|
3
3
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
|
|
5
|
+
import { findUserQuestionReply } from "../services/user-question-reply.js";
|
|
5
6
|
import { resolveExistingClaudeTranscriptPath } from "../services/claude-transcript-service.js";
|
|
6
7
|
import { isFinalTurnTextEvent, readTranscriptTextEvents } from "../services/claude-transcript-reply.js";
|
|
7
8
|
import { parseGatewayCommand } from "./gateway-command-parser.js";
|
|
@@ -1162,11 +1163,16 @@ export function createGatewayService(deps) {
|
|
|
1162
1163
|
|| settings.targetRole !== role) {
|
|
1163
1164
|
return;
|
|
1164
1165
|
}
|
|
1166
|
+
const questionReply = role === "project-manager" && deps.workflowControlService
|
|
1167
|
+
? await readRegisteredQuestionReply(input)
|
|
1168
|
+
: undefined;
|
|
1165
1169
|
const transcriptPath = resolveExistingClaudeTranscriptPath(input.session);
|
|
1166
|
-
if (!transcriptPath)
|
|
1170
|
+
if (!questionReply && !transcriptPath)
|
|
1167
1171
|
return;
|
|
1168
|
-
|
|
1169
|
-
|
|
1172
|
+
const events = questionReply ? [{
|
|
1173
|
+
id: questionReply.id, timestamp: questionReply.completedAt,
|
|
1174
|
+
text: questionReply.question, stopReason: "end_turn"
|
|
1175
|
+
}] : await readTranscriptTextEvents(transcriptPath);
|
|
1170
1176
|
const round = await waitForGatewayRoundFinal(input.repoRoot, input.taskSlug, role);
|
|
1171
1177
|
if (!round) {
|
|
1172
1178
|
return;
|
|
@@ -1285,6 +1291,15 @@ export function createGatewayService(deps) {
|
|
|
1285
1291
|
};
|
|
1286
1292
|
}
|
|
1287
1293
|
};
|
|
1294
|
+
async function readRegisteredQuestionReply(input) {
|
|
1295
|
+
const task = await deps.taskService.loadTask(input.repoRoot, input.taskSlug);
|
|
1296
|
+
const config = await deps.projectService.loadConfig(input.repoRoot);
|
|
1297
|
+
const state = await deps.workflowControlService.getState({
|
|
1298
|
+
taskRepoRoot: getTaskRuntimeRepoRoot(task), taskSlug: input.taskSlug,
|
|
1299
|
+
stateRoot: config.stateRoot, handoffDir: task.handoffDir
|
|
1300
|
+
});
|
|
1301
|
+
return findUserQuestionReply(state, input.session.id, input.session.lastTurnEndedAt ?? input.session.updatedAt);
|
|
1302
|
+
}
|
|
1288
1303
|
function getLatestRoleReply(settings) {
|
|
1289
1304
|
if (!settings.currentProjectId || !settings.currentTaskSlug) {
|
|
1290
1305
|
return undefined;
|
package/dist/backend/server.js
CHANGED
|
@@ -136,7 +136,8 @@ export async function createServer(deps, options = {}) {
|
|
|
136
136
|
roundService: deps.roundService,
|
|
137
137
|
taskWorkflowService: deps.taskWorkflowService,
|
|
138
138
|
architectRestartService: deps.architectRestartService,
|
|
139
|
-
translationService: deps.translationService
|
|
139
|
+
translationService: deps.translationService,
|
|
140
|
+
workflowControlService: deps.workflowControlService
|
|
140
141
|
});
|
|
141
142
|
registerSessionRoutes(app, {
|
|
142
143
|
projectService: deps.projectService,
|
|
@@ -245,7 +246,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
245
246
|
const workflowControlService = createWorkflowControlService({ fs });
|
|
246
247
|
const artifactService = createArtifactService(fs, {
|
|
247
248
|
workflowControlService,
|
|
248
|
-
|
|
249
|
+
getDurableDocAssignment: (taskRepoRoot, role) => autoMemoryService.getDurableDocAssignment(taskRepoRoot, role)
|
|
249
250
|
});
|
|
250
251
|
const projectService = createProjectService({ fs, git, appSettings });
|
|
251
252
|
const taskService = createTaskService({ fs, git, artifactService, projectService });
|
|
@@ -353,6 +354,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
353
354
|
});
|
|
354
355
|
const transcripts = createClaudeTranscriptService();
|
|
355
356
|
const translationService = createTranslationService({
|
|
357
|
+
workflowControlService,
|
|
356
358
|
runtime,
|
|
357
359
|
sessionRegistry: registry,
|
|
358
360
|
transcripts,
|
|
@@ -388,6 +390,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
388
390
|
roleContextRestartService
|
|
389
391
|
});
|
|
390
392
|
const gatewayService = createGatewayService({
|
|
393
|
+
workflowControlService,
|
|
391
394
|
fs,
|
|
392
395
|
settings: gatewaySettings,
|
|
393
396
|
audit: gatewayAudit,
|
|
@@ -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",
|
|
@@ -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 { 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";
|
|
8
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, durableDocAssignmentReportPath, 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" },
|
|
@@ -18,6 +18,20 @@ const MEMORY_FILE_DEFINITIONS = [
|
|
|
18
18
|
];
|
|
19
19
|
export function createAutoMemoryService(deps) {
|
|
20
20
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
21
|
+
const taskLocks = new Map();
|
|
22
|
+
async function withTaskLock(taskRepoRoot, operation) {
|
|
23
|
+
const previous = taskLocks.get(taskRepoRoot) ?? Promise.resolve();
|
|
24
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
25
|
+
taskLocks.set(taskRepoRoot, current);
|
|
26
|
+
try {
|
|
27
|
+
return await current;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (taskLocks.get(taskRepoRoot) === current) {
|
|
31
|
+
taskLocks.delete(taskRepoRoot);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
21
35
|
async function readMemorySet(repoRoot) {
|
|
22
36
|
const memory = {};
|
|
23
37
|
for (const definition of MEMORY_FILE_DEFINITIONS) {
|
|
@@ -151,7 +165,7 @@ export function createAutoMemoryService(deps) {
|
|
|
151
165
|
else if (active?.status === "reviewing" && active.reviewPromptDispatchedAt) {
|
|
152
166
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
153
167
|
}
|
|
154
|
-
else if (active) {
|
|
168
|
+
else if (active && !preserveDisabledReview(active)) {
|
|
155
169
|
await discardActiveReview(input.taskRepoRoot, active);
|
|
156
170
|
}
|
|
157
171
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
@@ -361,6 +375,11 @@ export function createAutoMemoryService(deps) {
|
|
|
361
375
|
}
|
|
362
376
|
return assignment.status === "running" && assignment.owner === role;
|
|
363
377
|
}
|
|
378
|
+
async function getDurableDocAssignment(taskRepoRoot, role) {
|
|
379
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
380
|
+
const assignment = state?.status === "documenting" ? currentDurableDocAssignment(state) : undefined;
|
|
381
|
+
return assignment?.status === "running" && assignment.owner === role ? assignment : undefined;
|
|
382
|
+
}
|
|
364
383
|
async function handleRoleHook(input) {
|
|
365
384
|
const state = await loadActiveState(input.taskRepoRoot);
|
|
366
385
|
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled && !preserveDisabledReview(state)) {
|
|
@@ -522,12 +541,19 @@ export function createAutoMemoryService(deps) {
|
|
|
522
541
|
statusCode: 409
|
|
523
542
|
});
|
|
524
543
|
}
|
|
544
|
+
if (state.assignments.length > 0) {
|
|
545
|
+
throw new VcmError({
|
|
546
|
+
code: "DURABLE_DOC_ASSIGNMENTS_FAILED",
|
|
547
|
+
message: "Memory was already applied. Retry the failed durable-document assignments individually.",
|
|
548
|
+
statusCode: 409
|
|
549
|
+
});
|
|
550
|
+
}
|
|
525
551
|
await clearActiveState(taskRepoRoot);
|
|
526
552
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
527
553
|
}
|
|
528
554
|
async function retryDurableDocAssignment(baseRepoRoot, taskRepoRoot, assignmentId) {
|
|
529
555
|
const state = await loadActiveState(taskRepoRoot);
|
|
530
|
-
const assignment = state
|
|
556
|
+
const assignment = state && (state.status === "documenting" || state.status === "failed")
|
|
531
557
|
? state.assignments.find((candidate) => candidate.id === assignmentId)
|
|
532
558
|
: undefined;
|
|
533
559
|
if (!state || !assignment || assignment.status !== "failed") {
|
|
@@ -539,11 +565,10 @@ export function createAutoMemoryService(deps) {
|
|
|
539
565
|
}
|
|
540
566
|
assignment.status = assignment.owner ? "pending" : "waiting-owner";
|
|
541
567
|
assignment.updatedAt = now();
|
|
542
|
-
delete assignment.error;
|
|
543
568
|
delete assignment.requestedOwner;
|
|
544
569
|
delete assignment.dispatchedAt;
|
|
545
|
-
|
|
546
|
-
delete
|
|
570
|
+
state.status = "documenting";
|
|
571
|
+
delete state.error;
|
|
547
572
|
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
548
573
|
await dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state);
|
|
549
574
|
return getState(baseRepoRoot, taskRepoRoot);
|
|
@@ -620,6 +645,7 @@ export function createAutoMemoryService(deps) {
|
|
|
620
645
|
assignment.error = `${input.role} durable-document assignment turn failed.`;
|
|
621
646
|
assignment.updatedAt = now();
|
|
622
647
|
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
648
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
623
649
|
return true;
|
|
624
650
|
}
|
|
625
651
|
if (input.eventName !== "Stop") {
|
|
@@ -644,13 +670,6 @@ export function createAutoMemoryService(deps) {
|
|
|
644
670
|
assignment.updatedAt = assignment.completedAt;
|
|
645
671
|
delete assignment.error;
|
|
646
672
|
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
673
|
}
|
|
655
674
|
catch (error) {
|
|
656
675
|
assignment.status = "failed";
|
|
@@ -658,11 +677,13 @@ export function createAutoMemoryService(deps) {
|
|
|
658
677
|
assignment.updatedAt = now();
|
|
659
678
|
await persistMemoryAssignmentState(input.taskRepoRoot, state);
|
|
660
679
|
}
|
|
680
|
+
await dispatchCurrentDurableDocAssignment(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
661
681
|
return true;
|
|
662
682
|
}
|
|
663
683
|
async function dispatchCurrentDurableDocAssignment(baseRepoRoot, taskRepoRoot, state) {
|
|
664
|
-
|
|
665
|
-
if (!assignment
|
|
684
|
+
let assignment = currentDurableDocAssignment(state);
|
|
685
|
+
if (!assignment) {
|
|
686
|
+
await finishDurableDocAssignments(taskRepoRoot, state);
|
|
666
687
|
return;
|
|
667
688
|
}
|
|
668
689
|
if (assignment.status === "running" || assignment.status === "resolving-owner") {
|
|
@@ -678,28 +699,50 @@ export function createAutoMemoryService(deps) {
|
|
|
678
699
|
assignment.updatedAt = now();
|
|
679
700
|
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
680
701
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (session.activityStatus === "running") {
|
|
702
|
+
while (assignment) {
|
|
703
|
+
try {
|
|
704
|
+
await dispatchDurableDocAssignment(baseRepoRoot, taskRepoRoot, state, assignment);
|
|
685
705
|
return;
|
|
686
706
|
}
|
|
687
|
-
|
|
707
|
+
catch (error) {
|
|
708
|
+
assignment.status = "failed";
|
|
709
|
+
assignment.error = `Unable to dispatch durable-document assignment: ${errorMessage(error)}`;
|
|
710
|
+
assignment.updatedAt = now();
|
|
711
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
712
|
+
assignment = currentDurableDocAssignment(state);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
await finishDurableDocAssignments(taskRepoRoot, state);
|
|
716
|
+
}
|
|
717
|
+
async function dispatchDurableDocAssignment(baseRepoRoot, taskRepoRoot, state, assignment) {
|
|
718
|
+
const session = await ensureWorkflowRoleSession(baseRepoRoot, state.taskSlug, assignment.owner ?? "project-manager");
|
|
719
|
+
if (session.activityStatus === "running") {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
assignment.status = assignment.owner ? "running" : "resolving-owner";
|
|
723
|
+
assignment.reportPath = durableDocAssignmentReportPath(assignment.runId, assignment.id);
|
|
724
|
+
if (!assignment.baseCommit) {
|
|
688
725
|
assignment.baseCommit = await deps.git.getHeadCommit(taskRepoRoot);
|
|
689
726
|
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
727
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
728
|
+
assignment.dispatchedAt = now();
|
|
729
|
+
assignment.updatedAt = assignment.dispatchedAt;
|
|
730
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
731
|
+
await submitTerminalInput(deps.runtime, session.id, assignment.owner
|
|
732
|
+
? buildDurableDocAssignmentPrompt(taskRepoRoot, assignment)
|
|
733
|
+
: buildDurableDocOwnerPrompt(taskRepoRoot, assignment));
|
|
734
|
+
}
|
|
735
|
+
async function finishDurableDocAssignments(taskRepoRoot, state) {
|
|
736
|
+
const failed = state.assignments.filter((assignment) => assignment.status === "failed");
|
|
737
|
+
if (failed.length > 0) {
|
|
738
|
+
state.status = "failed";
|
|
739
|
+
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
740
|
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
741
|
+
return;
|
|
702
742
|
}
|
|
743
|
+
delete state.error;
|
|
744
|
+
await persistMemoryAssignmentState(taskRepoRoot, state);
|
|
745
|
+
await clearActiveState(taskRepoRoot);
|
|
703
746
|
}
|
|
704
747
|
async function validateDurableDocAssignmentCompletion(taskRepoRoot, assignment) {
|
|
705
748
|
const reportAbsolutePath = resolveRepoPath(taskRepoRoot, assignment.reportPath);
|
|
@@ -727,22 +770,36 @@ export function createAutoMemoryService(deps) {
|
|
|
727
770
|
if (decision !== "synced" && decision !== "unchanged") {
|
|
728
771
|
throw new Error("Decision must be synced or unchanged");
|
|
729
772
|
}
|
|
773
|
+
if (!(await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, assignment.targetPath)))) {
|
|
774
|
+
throw new Error(`target document is missing: ${assignment.targetPath}`);
|
|
775
|
+
}
|
|
730
776
|
const currentHead = await deps.git.getHeadCommit(taskRepoRoot);
|
|
731
777
|
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
|
-
}
|
|
778
|
+
return validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead);
|
|
743
779
|
}
|
|
744
780
|
return currentHead;
|
|
745
781
|
}
|
|
782
|
+
async function validateDurableDocCommit(taskRepoRoot, assignment, reportContent, currentHead) {
|
|
783
|
+
if (!assignment.baseCommit || currentHead === assignment.baseCommit) {
|
|
784
|
+
throw new Error("documentation changes were not committed");
|
|
785
|
+
}
|
|
786
|
+
const changedPaths = await deps.git.getChangedPaths(taskRepoRoot, assignment.baseCommit, currentHead);
|
|
787
|
+
if (!changedPaths.includes(assignment.targetPath)) {
|
|
788
|
+
throw new Error(`documentation commit does not include ${assignment.targetPath}`);
|
|
789
|
+
}
|
|
790
|
+
const reportedCommit = readArtifactSectionValue(reportContent, "Commit")?.trim();
|
|
791
|
+
const commits = await deps.git.getCommitList(taskRepoRoot, `${assignment.baseCommit}..${currentHead}`);
|
|
792
|
+
const matches = reportedCommit ? commits.filter((commit) => commit.sha.startsWith(reportedCommit)) : [];
|
|
793
|
+
if (matches.length !== 1) {
|
|
794
|
+
throw new Error(`Commit must uniquely identify a documentation commit in ${assignment.baseCommit}..${currentHead}; received ${reportedCommit || "empty"}`);
|
|
795
|
+
}
|
|
796
|
+
const commit = matches[0].sha;
|
|
797
|
+
const commitPaths = await deps.git.getChangedPaths(taskRepoRoot, `${commit}^`, commit);
|
|
798
|
+
if (!commitPaths.includes(assignment.targetPath)) {
|
|
799
|
+
throw new Error(`reported commit ${commit} does not modify ${assignment.targetPath}`);
|
|
800
|
+
}
|
|
801
|
+
return commit;
|
|
802
|
+
}
|
|
746
803
|
async function hashOptionalFile(taskRepoRoot, relativePath) {
|
|
747
804
|
const absolutePath = resolveRepoPath(taskRepoRoot, relativePath);
|
|
748
805
|
return await deps.fs.pathExists(absolutePath)
|
|
@@ -756,6 +813,7 @@ export function createAutoMemoryService(deps) {
|
|
|
756
813
|
await persistRun(taskRepoRoot, {
|
|
757
814
|
...run,
|
|
758
815
|
assignments: state.assignments,
|
|
816
|
+
error: state.error,
|
|
759
817
|
updatedAt: state.updatedAt
|
|
760
818
|
});
|
|
761
819
|
}
|
|
@@ -897,7 +955,7 @@ export function createAutoMemoryService(deps) {
|
|
|
897
955
|
evidence: assignment.evidence,
|
|
898
956
|
...(owner ? { owner } : {}),
|
|
899
957
|
status: owner ? "pending" : "waiting-owner",
|
|
900
|
-
reportPath:
|
|
958
|
+
reportPath: durableDocAssignmentReportPath(state.runId, `${state.runId}-doc-${index + 1}`),
|
|
901
959
|
createdAt: timestamp,
|
|
902
960
|
updatedAt: timestamp
|
|
903
961
|
};
|
|
@@ -1317,20 +1375,21 @@ export function createAutoMemoryService(deps) {
|
|
|
1317
1375
|
return `sha256:${sha256(content)}`;
|
|
1318
1376
|
}
|
|
1319
1377
|
return {
|
|
1320
|
-
reconcileTask,
|
|
1378
|
+
reconcileTask: (input) => withTaskLock(input.taskRepoRoot, () => reconcileTask(input)),
|
|
1321
1379
|
getState,
|
|
1322
1380
|
getTaskRetrospectiveReadiness,
|
|
1323
1381
|
getFile,
|
|
1324
1382
|
updateFile,
|
|
1325
1383
|
revertRun,
|
|
1326
1384
|
retryFailedReview,
|
|
1327
|
-
retryDurableDocAssignment,
|
|
1328
|
-
resolveDurableDocAssignmentOwner,
|
|
1385
|
+
retryDurableDocAssignment: (base, task, id) => withTaskLock(task, () => retryDurableDocAssignment(base, task, id)),
|
|
1386
|
+
resolveDurableDocAssignmentOwner: (base, task, id, owner) => withTaskLock(task, () => resolveDurableDocAssignmentOwner(base, task, id, owner)),
|
|
1329
1387
|
prepareTaskRetrospectiveReview,
|
|
1330
1388
|
cancelTaskRetrospectiveReview,
|
|
1331
1389
|
isRoleMemoryTurn,
|
|
1332
|
-
|
|
1333
|
-
|
|
1390
|
+
getDurableDocAssignment,
|
|
1391
|
+
handleRoleHook: (input) => withTaskLock(input.taskRepoRoot, () => handleRoleHook(input)),
|
|
1392
|
+
handleHarnessEngineerHook: (input) => withTaskLock(input.taskRepoRoot, () => handleHarnessEngineerHook(input)),
|
|
1334
1393
|
assertHarnessEngineerAvailable
|
|
1335
1394
|
};
|
|
1336
1395
|
}
|
|
@@ -1350,11 +1409,12 @@ function currentDraft(state) {
|
|
|
1350
1409
|
return state.drafts.find((draft) => draft.status !== "completed");
|
|
1351
1410
|
}
|
|
1352
1411
|
function preserveDisabledReview(state) {
|
|
1353
|
-
return state?.
|
|
1412
|
+
return Boolean(state?.assignments.length)
|
|
1354
1413
|
|| (state?.status === "reviewing" && Boolean(state.reviewPromptDispatchedAt));
|
|
1355
1414
|
}
|
|
1356
1415
|
function currentDurableDocAssignment(state) {
|
|
1357
|
-
return state.assignments.find((assignment) => assignment.status
|
|
1416
|
+
return state.assignments.find((assignment) => assignment.status === "running" || assignment.status === "resolving-owner")
|
|
1417
|
+
?? state.assignments.find((assignment) => assignment.status === "pending" || assignment.status === "waiting-owner");
|
|
1358
1418
|
}
|
|
1359
1419
|
function toReviewCandidates(source, currentRole, items) {
|
|
1360
1420
|
return items.map((item) => ({
|
|
@@ -1435,7 +1495,11 @@ function buildDurableDocAssignmentPrompt(taskRepoRoot, assignment) {
|
|
|
1435
1495
|
...assignment.evidence.map((item) => `- ${item}`),
|
|
1436
1496
|
"",
|
|
1437
1497
|
"Verify the content, update the target and any directly related durable documentation, run applicable documentation checks, and commit the documentation changes.",
|
|
1438
|
-
|
|
1498
|
+
`Assigned report path: ${assignment.reportPath}`,
|
|
1499
|
+
`Submit with .ai/tools/vcm-artifact docs-update-report --file <candidate> --path ${assignment.reportPath} --mode final`,
|
|
1500
|
+
"Set Assignment ID to the exact ID above.",
|
|
1501
|
+
"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.",
|
|
1502
|
+
...(assignment.error ? [`Previous failure: ${assignment.error}`] : []),
|
|
1439
1503
|
"End the turn after VCM accepts the report."
|
|
1440
1504
|
].join("\n");
|
|
1441
1505
|
}
|
|
@@ -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;
|
|
@@ -352,7 +353,7 @@ export function createClaudeHookService(deps) {
|
|
|
352
353
|
dispatchedCount: 0,
|
|
353
354
|
stopDecision: {
|
|
354
355
|
behavior: "block",
|
|
355
|
-
reason: `Possible unregistered user question: ${JSON.stringify(question.slice(0, 600))}. If you are asking the user, use vcm-ask-user with the
|
|
356
|
+
reason: `Possible unregistered user question: ${JSON.stringify(question.slice(0, 600))}. If you are asking the user, use vcm-ask-user with the complete question and necessary context, then end the turn without advancing or routing the workflow. VCM delivers the registered question. If this only reports or quotes another question, clarify that context and end normally; do not invent a user question or register a false wait.`
|
|
356
357
|
}
|
|
357
358
|
};
|
|
358
359
|
}
|
|
@@ -507,6 +508,21 @@ export function createClaudeHookService(deps) {
|
|
|
507
508
|
if (!session) {
|
|
508
509
|
return completedHookResult(input, eventName);
|
|
509
510
|
}
|
|
511
|
+
if (eventName === "Stop" && input.role === "project-manager") {
|
|
512
|
+
const reply = await deps.workflowControlService?.completeUserQuestion?.(createWorkflowControlContext(context), {
|
|
513
|
+
sessionId: session.id,
|
|
514
|
+
startedAt: session.lastTurnStartedAt ?? session.updatedAt,
|
|
515
|
+
endedAt: session.lastTurnEndedAt ?? session.updatedAt
|
|
516
|
+
});
|
|
517
|
+
if (reply) {
|
|
518
|
+
await deps.translationService.recordUserQuestionReply?.({
|
|
519
|
+
repoRoot: context.project.repoRoot,
|
|
520
|
+
taskRepoRoot: context.taskRepoRoot,
|
|
521
|
+
taskSlug: context.taskSlug,
|
|
522
|
+
reply
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
510
526
|
const boundToTask = await isHookSessionBoundToTask(context, input.role);
|
|
511
527
|
if (boundToTask) {
|
|
512
528
|
await deps.roundService.recordClaudeHookEvent({
|
|
@@ -72,7 +72,10 @@ export function createGateReviewService(deps) {
|
|
|
72
72
|
}
|
|
73
73
|
async function requestReviewGateInternal(repoRoot, taskSlug, gate, options = {}) {
|
|
74
74
|
const context = await getContext(repoRoot, taskSlug);
|
|
75
|
-
|
|
75
|
+
const run = () => withGateStateLock(context, () => requestReviewGateLocked(context, gate, options));
|
|
76
|
+
return deps.workflowControlService?.runWhileNotAwaitingUser
|
|
77
|
+
? deps.workflowControlService.runWhileNotAwaitingUser(context, run)
|
|
78
|
+
: run();
|
|
76
79
|
}
|
|
77
80
|
async function requestReviewGateLocked(context, gate, options) {
|
|
78
81
|
let index = await loadIndex(deps.fs, context, now());
|
|
@@ -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";
|