vibe-coding-master 0.7.28 → 0.7.30
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 +26 -8
- package/dist/backend/api/harness-routes.js +0 -1
- package/dist/backend/api/task-routes.js +4 -2
- package/dist/backend/server.js +11 -7
- package/dist/backend/services/architect-restart-service.js +107 -20
- package/dist/backend/services/auto-memory-service.js +118 -61
- package/dist/backend/services/claude-hook-service.js +12 -2
- package/dist/backend/services/gate-review-service.js +62 -2
- package/dist/backend/services/harness-feedback-service.js +138 -7
- package/dist/backend/services/memory-proposal-validation.js +80 -0
- package/dist/backend/services/memory-review-paths.js +7 -0
- package/dist/backend/services/memory-review-validation.js +94 -0
- package/dist/backend/templates/harness/architect-agent.js +3 -2
- package/dist/backend/templates/harness/claude-root.js +3 -3
- package/dist/backend/templates/harness/gate-review.js +79 -9
- package/dist/backend/templates/harness/harness-engineer-agent.js +53 -12
- package/dist/backend/templates/harness/project-manager-agent.js +15 -13
- package/dist/backend/templates/harness/restart-architect-skill.js +16 -2
- package/dist/backend/templates/harness/role-memory.js +2 -2
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +55 -7
- package/dist/shared/types/architect-restart.js +1 -0
- package/dist-frontend/assets/index-CguKU8Q5.css +32 -0
- package/dist-frontend/assets/{index-Bq1kZK1u.js → index-Cmr9W7-n.js} +22 -22
- package/dist-frontend/index.html +2 -2
- package/package.json +1 -1
- package/dist-frontend/assets/index-Ci7z8tW3.css +0 -32
package/README.md
CHANGED
|
@@ -451,7 +451,7 @@ Use it to:
|
|
|
451
451
|
- view and edit shared or role-specific VCM memory
|
|
452
452
|
- review and revert memory changes recorded in the active task worktree
|
|
453
453
|
- copy file paths for discussion
|
|
454
|
-
- review task harness
|
|
454
|
+
- review task harness and consolidate optional Auto Memory in one retrospective
|
|
455
455
|
- inspect commit diffs for harness changes
|
|
456
456
|
- merge task harness commits back to the connected repository branch when
|
|
457
457
|
appropriate
|
|
@@ -467,7 +467,14 @@ files.
|
|
|
467
467
|
Review Task Harness after Final Acceptance, Project Manager, Architect, Coder,
|
|
468
468
|
Tester, and an enabled Reviewer submit proposals in sequence through
|
|
469
469
|
`vcm-propose-memory`. Harness Engineer verifies and consolidates them before VCM
|
|
470
|
-
applies the result
|
|
470
|
+
applies the result as part of the same Task Harness Retrospective. Roles cannot
|
|
471
|
+
edit active memory directly.
|
|
472
|
+
|
|
473
|
+
When Auto Memory is enabled, the planning Architect writes a provisional memory
|
|
474
|
+
candidate before its post-planning Session restart. VCM snapshots that candidate
|
|
475
|
+
into the later memory-review run. The replacement Architect validates it against
|
|
476
|
+
the completed implementation and tests, and Harness Engineer reviews it with all
|
|
477
|
+
final role proposals before anything becomes active memory.
|
|
471
478
|
|
|
472
479
|
Shared memory is stored in the root `CLAUDE.md` `<VCM-memory>` block. Role memory
|
|
473
480
|
is stored in the matching `.claude/agents/*.md` block. VCM changes only block
|
|
@@ -481,21 +488,32 @@ Post-task processing is ordered by the backend:
|
|
|
481
488
|
```text
|
|
482
489
|
Final Acceptance
|
|
483
490
|
-> Review Task Harness
|
|
491
|
+
-> Snapshot Architect planning-session memory candidate, when present
|
|
484
492
|
-> Workflow-role memory proposals, when Auto Memory is enabled
|
|
485
|
-
-> Harness Engineer memory review, when Auto Memory is enabled
|
|
486
493
|
-> Task Harness Retrospective
|
|
494
|
+
-> Review pending Harness Feedback
|
|
495
|
+
-> Review Auto Memory proposals, when Auto Memory is enabled
|
|
496
|
+
-> Apply reviewed memory, when Auto Memory is enabled
|
|
487
497
|
```
|
|
488
498
|
|
|
489
499
|
Memory proposal prompts sent to Project Manager, Architect, Coder, Tester, and
|
|
490
500
|
an enabled Reviewer use their normal task sessions and participate in
|
|
491
|
-
Round/Turn tracking.
|
|
492
|
-
role
|
|
501
|
+
Round/Turn tracking. Each proposed add, update, or removal identifies its
|
|
502
|
+
shared or current-role target and its supporting evidence. Adds and updates
|
|
503
|
+
also state why the memory is necessary, the impact of omitting it, and whether
|
|
504
|
+
the knowledge belongs in memory or a durable document. Harness Engineer first
|
|
505
|
+
reviews every existing memory entry, recording its retention reason, removal
|
|
506
|
+
impact, and durable-document disposition, then evaluates the proposals and
|
|
507
|
+
records the resulting changes in the Retrospective report. VCM validates that
|
|
508
|
+
report structure before applying the reviewed memory. Harness Engineer review
|
|
509
|
+
and retrospective work remain tool role activity and do not participate in
|
|
510
|
+
Round completion.
|
|
493
511
|
|
|
494
512
|
When Auto Memory is disabled, Review Task Harness does not collect proposals or
|
|
495
513
|
ask Harness Engineer to update memory. When enabled, both automatic and manual
|
|
496
|
-
review requests
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
review requests collect proposals before starting the retrospective. A failed
|
|
515
|
+
proposal collection or combined retrospective memory review must be retried
|
|
516
|
+
from Harness Studio.
|
|
499
517
|
|
|
500
518
|
## Closing a Task
|
|
501
519
|
|
|
@@ -174,7 +174,6 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
174
174
|
if (!memoryReadiness.ready) {
|
|
175
175
|
return deps.harnessFeedbackService.getState(project.repoRoot, task.taskSlug);
|
|
176
176
|
}
|
|
177
|
-
await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
|
|
178
177
|
return deps.harnessFeedbackService.startTaskRetrospective(project.repoRoot, {
|
|
179
178
|
taskSlug: task.taskSlug,
|
|
180
179
|
taskRepoRoot: task.worktreePath,
|
|
@@ -70,7 +70,8 @@ export function registerTaskRoutes(app, deps) {
|
|
|
70
70
|
messages,
|
|
71
71
|
orchestration,
|
|
72
72
|
roundState,
|
|
73
|
-
workflowState
|
|
73
|
+
workflowState,
|
|
74
|
+
architectRestart: deps.architectRestartService.getState(project.repoRoot, taskSlug)
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
@@ -84,7 +85,8 @@ export function registerTaskRoutes(app, deps) {
|
|
|
84
85
|
updatedAt: new Date().toISOString()
|
|
85
86
|
},
|
|
86
87
|
roundState: degradedRoundState(taskSlug),
|
|
87
|
-
workflowState: degradedWorkflowState(taskSlug)
|
|
88
|
+
workflowState: degradedWorkflowState(taskSlug),
|
|
89
|
+
architectRestart: deps.architectRestartService.getState(repoRoot, taskSlug)
|
|
88
90
|
};
|
|
89
91
|
}
|
|
90
92
|
throw error;
|
package/dist/backend/server.js
CHANGED
|
@@ -130,7 +130,8 @@ export async function createServer(deps, options = {}) {
|
|
|
130
130
|
messageService: deps.messageService,
|
|
131
131
|
taskLaunchService: deps.taskLaunchService,
|
|
132
132
|
roundService: deps.roundService,
|
|
133
|
-
taskWorkflowService: deps.taskWorkflowService
|
|
133
|
+
taskWorkflowService: deps.taskWorkflowService,
|
|
134
|
+
architectRestartService: deps.architectRestartService
|
|
134
135
|
});
|
|
135
136
|
registerSessionRoutes(app, {
|
|
136
137
|
projectService: deps.projectService,
|
|
@@ -251,11 +252,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
251
252
|
runFixedInstaller: createScriptFixedHarnessInstaller(path.join(appRoot, "scripts/install-vcm-harness.mjs")),
|
|
252
253
|
vcmVersion
|
|
253
254
|
});
|
|
254
|
-
const harnessFeedbackService = createHarnessFeedbackService({
|
|
255
|
-
fs,
|
|
256
|
-
runtime,
|
|
257
|
-
sessionService
|
|
258
|
-
});
|
|
259
255
|
const autoMemoryService = createAutoMemoryService({
|
|
260
256
|
fs,
|
|
261
257
|
git,
|
|
@@ -266,6 +262,12 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
266
262
|
return true;
|
|
267
263
|
}
|
|
268
264
|
});
|
|
265
|
+
const harnessFeedbackService = createHarnessFeedbackService({
|
|
266
|
+
fs,
|
|
267
|
+
runtime,
|
|
268
|
+
sessionService,
|
|
269
|
+
autoMemoryService
|
|
270
|
+
});
|
|
269
271
|
const commandDispatcher = createCommandDispatcher({
|
|
270
272
|
runtime,
|
|
271
273
|
sessionService,
|
|
@@ -280,7 +282,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
280
282
|
const architectRestartService = createArchitectRestartService({
|
|
281
283
|
fs,
|
|
282
284
|
taskService,
|
|
283
|
-
sessionService
|
|
285
|
+
sessionService,
|
|
286
|
+
appSettings
|
|
284
287
|
});
|
|
285
288
|
const messageService = createMessageService({
|
|
286
289
|
fs,
|
|
@@ -388,6 +391,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
388
391
|
runtime,
|
|
389
392
|
harnessService,
|
|
390
393
|
autoMemoryService,
|
|
394
|
+
harnessFeedbackService,
|
|
391
395
|
gatewayService,
|
|
392
396
|
jobGuard: createJobGuardService(),
|
|
393
397
|
translationWorkerService,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
3
|
-
import { VcmError } from "../errors.js";
|
|
3
|
+
import { toVcmError, VcmError } from "../errors.js";
|
|
4
4
|
import { getTaskRuntimeRepoRoot } from "./task-service.js";
|
|
5
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH } from "./memory-review-paths.js";
|
|
6
|
+
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
5
7
|
const ARCHITECT_ROLE = "architect";
|
|
6
8
|
const PM_ROLE = "project-manager";
|
|
7
9
|
const COMPLETE_PLAN_PATTERN = /^Planning Result:\s*complete\s*$/im;
|
|
@@ -24,26 +26,65 @@ export function createArchitectRestartService(deps) {
|
|
|
24
26
|
const key = taskKey(repoRoot, taskSlug);
|
|
25
27
|
const existing = pendingByTask.get(key);
|
|
26
28
|
if (existing?.sessionId === session.id) {
|
|
27
|
-
existing.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
if (existing.status === "blocked") {
|
|
30
|
+
existing.status = "pending";
|
|
31
|
+
existing.blocker = undefined;
|
|
32
|
+
await tryRestart(existing);
|
|
33
|
+
return {
|
|
34
|
+
taskSlug,
|
|
35
|
+
sessionId: session.id,
|
|
36
|
+
status: "scheduled",
|
|
37
|
+
...(existing.memoryCandidatePath
|
|
38
|
+
? { memoryCandidatePath: existing.memoryCandidatePath }
|
|
39
|
+
: {})
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
taskSlug,
|
|
44
|
+
sessionId: session.id,
|
|
45
|
+
status: "already_scheduled",
|
|
46
|
+
...(existing.memoryCandidatePath
|
|
47
|
+
? { memoryCandidatePath: existing.memoryCandidatePath }
|
|
48
|
+
: {})
|
|
49
|
+
};
|
|
33
50
|
}
|
|
51
|
+
const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
|
|
52
|
+
? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
|
|
53
|
+
: undefined;
|
|
34
54
|
pendingByTask.set(key, {
|
|
35
55
|
repoRoot,
|
|
36
56
|
taskSlug,
|
|
37
57
|
sessionId: session.id,
|
|
38
58
|
stopped: false,
|
|
39
59
|
gateAccepted: false,
|
|
40
|
-
|
|
60
|
+
status: "pending",
|
|
61
|
+
memoryCandidatePath
|
|
41
62
|
});
|
|
42
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
taskSlug,
|
|
65
|
+
sessionId: session.id,
|
|
66
|
+
status: "scheduled",
|
|
67
|
+
...(memoryCandidatePath ? { memoryCandidatePath } : {})
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
getState(repoRoot, taskSlug) {
|
|
71
|
+
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
72
|
+
if (!pending) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
taskSlug: pending.taskSlug,
|
|
77
|
+
sessionId: pending.sessionId,
|
|
78
|
+
status: pending.status,
|
|
79
|
+
...(pending.memoryCandidatePath
|
|
80
|
+
? { memoryCandidatePath: pending.memoryCandidatePath }
|
|
81
|
+
: {}),
|
|
82
|
+
...(pending.blocker ? { blocker: { ...pending.blocker } } : {})
|
|
83
|
+
};
|
|
43
84
|
},
|
|
44
85
|
async recordArchitectStop(repoRoot, taskSlug, sessionId) {
|
|
45
86
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
46
|
-
if (!pending || pending.sessionId !== sessionId) {
|
|
87
|
+
if (!pending || pending.sessionId !== sessionId || pending.status === "blocked") {
|
|
47
88
|
return;
|
|
48
89
|
}
|
|
49
90
|
pending.stopped = true;
|
|
@@ -54,7 +95,7 @@ export function createArchitectRestartService(deps) {
|
|
|
54
95
|
return;
|
|
55
96
|
}
|
|
56
97
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
57
|
-
if (!pending) {
|
|
98
|
+
if (!pending || pending.status === "blocked") {
|
|
58
99
|
return;
|
|
59
100
|
}
|
|
60
101
|
pending.deliveredMessageId = message.id;
|
|
@@ -65,7 +106,7 @@ export function createArchitectRestartService(deps) {
|
|
|
65
106
|
return;
|
|
66
107
|
}
|
|
67
108
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
68
|
-
if (!pending) {
|
|
109
|
+
if (!pending || pending.status === "blocked") {
|
|
69
110
|
return;
|
|
70
111
|
}
|
|
71
112
|
pending.acceptedMessageId = message.id;
|
|
@@ -73,7 +114,7 @@ export function createArchitectRestartService(deps) {
|
|
|
73
114
|
},
|
|
74
115
|
async recordArchitectureGateDisposition(repoRoot, taskSlug, accepted) {
|
|
75
116
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
76
|
-
if (!pending) {
|
|
117
|
+
if (!pending || pending.status === "blocked") {
|
|
77
118
|
return;
|
|
78
119
|
}
|
|
79
120
|
pending.gateAccepted = accepted;
|
|
@@ -106,7 +147,7 @@ export function createArchitectRestartService(deps) {
|
|
|
106
147
|
}
|
|
107
148
|
}
|
|
108
149
|
async function tryRestart(pending) {
|
|
109
|
-
if (pending.
|
|
150
|
+
if (pending.status !== "pending"
|
|
110
151
|
|| !pending.stopped
|
|
111
152
|
|| !pending.deliveredMessageId
|
|
112
153
|
|| pending.deliveredMessageId !== pending.acceptedMessageId
|
|
@@ -115,14 +156,29 @@ export function createArchitectRestartService(deps) {
|
|
|
115
156
|
}
|
|
116
157
|
const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
|
|
117
158
|
if (!session
|
|
118
|
-
|| session.id !== pending.sessionId
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
|| session.id !== pending.sessionId) {
|
|
160
|
+
blockPending(pending, new VcmError({
|
|
161
|
+
code: "ARCHITECT_RESTART_SESSION_UNAVAILABLE",
|
|
162
|
+
message: "Architect restart is blocked because the scheduled Architect session no longer exists.",
|
|
163
|
+
statusCode: 409
|
|
164
|
+
}));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (session.status !== "running") {
|
|
168
|
+
blockPending(pending, new VcmError({
|
|
169
|
+
code: "ARCHITECT_RESTART_SESSION_NOT_RUNNING",
|
|
170
|
+
message: "Architect restart is blocked because the scheduled Architect session is not running.",
|
|
171
|
+
statusCode: 409
|
|
172
|
+
}));
|
|
121
173
|
return;
|
|
122
174
|
}
|
|
123
|
-
|
|
175
|
+
if (session.activityStatus !== "idle") {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
pending.status = "executing";
|
|
124
179
|
try {
|
|
125
180
|
await requireCompletePlan(pending.repoRoot, pending.taskSlug);
|
|
181
|
+
await requirePlanningMemoryCandidate(pending);
|
|
126
182
|
await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
127
183
|
permissionMode: session.permissionMode,
|
|
128
184
|
model: session.model,
|
|
@@ -131,8 +187,32 @@ export function createArchitectRestartService(deps) {
|
|
|
131
187
|
});
|
|
132
188
|
pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
|
|
133
189
|
}
|
|
134
|
-
catch {
|
|
135
|
-
pending
|
|
190
|
+
catch (error) {
|
|
191
|
+
blockPending(pending, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function blockPending(pending, error) {
|
|
195
|
+
const normalized = toVcmError(error);
|
|
196
|
+
pending.status = "blocked";
|
|
197
|
+
pending.blocker = {
|
|
198
|
+
code: normalized.code,
|
|
199
|
+
message: normalized.message,
|
|
200
|
+
blockedAt: new Date().toISOString()
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function requirePlanningMemoryCandidate(pending) {
|
|
204
|
+
if (!pending.memoryCandidatePath) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const task = await deps.taskService.loadTask(pending.repoRoot, pending.taskSlug);
|
|
208
|
+
const candidatePath = resolveRepoPath(getTaskRuntimeRepoRoot(task), pending.memoryCandidatePath);
|
|
209
|
+
if (!(await deps.fs.pathExists(candidatePath))) {
|
|
210
|
+
throw invalidMemoryCandidateError("the assigned candidate file does not exist.");
|
|
211
|
+
}
|
|
212
|
+
const content = await deps.fs.readText(candidatePath);
|
|
213
|
+
const validationError = validateMemoryProposal(content);
|
|
214
|
+
if (validationError) {
|
|
215
|
+
throw invalidMemoryCandidateError(`the assigned candidate ${validationError}.`);
|
|
136
216
|
}
|
|
137
217
|
}
|
|
138
218
|
}
|
|
@@ -149,3 +229,10 @@ function incompletePlanError(reason) {
|
|
|
149
229
|
statusCode: 409
|
|
150
230
|
});
|
|
151
231
|
}
|
|
232
|
+
function invalidMemoryCandidateError(reason) {
|
|
233
|
+
return new VcmError({
|
|
234
|
+
code: "ARCHITECT_MEMORY_CANDIDATE_INVALID",
|
|
235
|
+
message: `Architect restart is waiting for its planning-session memory candidate because ${reason}`,
|
|
236
|
+
statusCode: 409
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -5,9 +5,9 @@ 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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH, architectPlanningCandidateSnapshotPath, MEMORY_REVIEW_RUNS_ROOT, MEMORY_REVIEW_STATE_PATH } from "./memory-review-paths.js";
|
|
9
|
+
import { validateMemoryProposal } from "./memory-proposal-validation.js";
|
|
10
|
+
import { validateMemoryReviewReport } from "./memory-review-validation.js";
|
|
11
11
|
const MEMORY_FILE_DEFINITIONS = [
|
|
12
12
|
{ path: "CLAUDE.md", title: "Shared Memory" },
|
|
13
13
|
{ path: ".claude/agents/project-manager.md", title: "Project Manager Memory", role: "project-manager" },
|
|
@@ -132,7 +132,6 @@ export function createAutoMemoryService(deps) {
|
|
|
132
132
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
133
133
|
}
|
|
134
134
|
if (active?.status === "reviewing") {
|
|
135
|
-
await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, active);
|
|
136
135
|
return getState(input.baseRepoRoot, input.taskRepoRoot);
|
|
137
136
|
}
|
|
138
137
|
if (active || !input.roundReady || !input.requestTrigger) {
|
|
@@ -174,6 +173,7 @@ export function createAutoMemoryService(deps) {
|
|
|
174
173
|
const before = await readMemorySet(input.taskRepoRoot);
|
|
175
174
|
await writeRunMemorySet(input.taskRepoRoot, runId, "before", before);
|
|
176
175
|
await writeRunMemorySet(input.taskRepoRoot, runId, "after", before);
|
|
176
|
+
await snapshotArchitectPlanningCandidate(input.taskRepoRoot, runId);
|
|
177
177
|
await persistRun(input.taskRepoRoot, {
|
|
178
178
|
version: 1,
|
|
179
179
|
runId,
|
|
@@ -202,6 +202,13 @@ export function createAutoMemoryService(deps) {
|
|
|
202
202
|
const active = await loadActiveState(input.taskRepoRoot);
|
|
203
203
|
if (active) {
|
|
204
204
|
const disposition = active.status;
|
|
205
|
+
if (disposition === "reviewing") {
|
|
206
|
+
return {
|
|
207
|
+
ready: true,
|
|
208
|
+
disposition,
|
|
209
|
+
trigger: active.trigger
|
|
210
|
+
};
|
|
211
|
+
}
|
|
205
212
|
return {
|
|
206
213
|
ready: false,
|
|
207
214
|
disposition,
|
|
@@ -218,9 +225,60 @@ export function createAutoMemoryService(deps) {
|
|
|
218
225
|
return {
|
|
219
226
|
ready: false,
|
|
220
227
|
disposition: "pending",
|
|
221
|
-
reason: "Auto Memory must
|
|
228
|
+
reason: "Auto Memory proposals must be collected before Task Harness Retrospective."
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function prepareTaskRetrospectiveReview(taskRepoRoot, retrospectiveReportPath) {
|
|
232
|
+
if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
236
|
+
if (!state) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
if (state.status !== "reviewing") {
|
|
240
|
+
throw new VcmError({
|
|
241
|
+
code: "AUTO_MEMORY_NOT_READY",
|
|
242
|
+
message: "Auto Memory proposals are not ready for Task Harness Retrospective.",
|
|
243
|
+
statusCode: 409,
|
|
244
|
+
hint: "Wait for every workflow role to finish its memory proposal, then retry Task Harness Retrospective."
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (state.reviewPromptDispatchedAt) {
|
|
248
|
+
throw new VcmError({
|
|
249
|
+
code: "AUTO_MEMORY_REVIEW_RUNNING",
|
|
250
|
+
message: "Task Harness Retrospective is already reviewing Auto Memory.",
|
|
251
|
+
statusCode: 409
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const timestamp = now();
|
|
255
|
+
state.reviewPromptDispatchedAt = timestamp;
|
|
256
|
+
state.retrospectiveReportPath = retrospectiveReportPath;
|
|
257
|
+
state.updatedAt = timestamp;
|
|
258
|
+
await persistActiveState(taskRepoRoot, state);
|
|
259
|
+
const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
|
|
260
|
+
const planningCandidatePath = await findPlanningCandidateSnapshot(taskRepoRoot, state.runId);
|
|
261
|
+
return {
|
|
262
|
+
runId: state.runId,
|
|
263
|
+
roleDraftsPath: path.join(runRoot, "drafts"),
|
|
264
|
+
currentMemoryPath: path.join(runRoot, "before"),
|
|
265
|
+
reviewedMemoryPath: path.join(runRoot, "after"),
|
|
266
|
+
proposalRoles: state.drafts.map((draft) => draft.role),
|
|
267
|
+
...(planningCandidatePath
|
|
268
|
+
? { planningCandidatePath: resolveRepoPath(taskRepoRoot, planningCandidatePath) }
|
|
269
|
+
: {})
|
|
222
270
|
};
|
|
223
271
|
}
|
|
272
|
+
async function cancelTaskRetrospectiveReview(taskRepoRoot, runId) {
|
|
273
|
+
const state = await loadActiveState(taskRepoRoot);
|
|
274
|
+
if (!state || state.runId !== runId || state.status !== "reviewing") {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
delete state.reviewPromptDispatchedAt;
|
|
278
|
+
delete state.retrospectiveReportPath;
|
|
279
|
+
state.updatedAt = now();
|
|
280
|
+
await persistActiveState(taskRepoRoot, state);
|
|
281
|
+
}
|
|
224
282
|
async function isRoleMemoryTurn(taskRepoRoot, role) {
|
|
225
283
|
const state = await loadActiveState(taskRepoRoot);
|
|
226
284
|
const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
|
|
@@ -255,8 +313,9 @@ export function createAutoMemoryService(deps) {
|
|
|
255
313
|
return true;
|
|
256
314
|
}
|
|
257
315
|
const content = (await deps.fs.readText(draftAbsolutePath)).trim();
|
|
258
|
-
|
|
259
|
-
|
|
316
|
+
const validationError = content ? validateMemoryProposal(content) : "is empty";
|
|
317
|
+
if (validationError) {
|
|
318
|
+
await failReview(input.taskRepoRoot, state, `${input.role} memory draft ${validationError}.`);
|
|
260
319
|
return true;
|
|
261
320
|
}
|
|
262
321
|
draft.status = "completed";
|
|
@@ -270,7 +329,6 @@ export function createAutoMemoryService(deps) {
|
|
|
270
329
|
state.status = "reviewing";
|
|
271
330
|
await persistActiveState(input.taskRepoRoot, state);
|
|
272
331
|
await updateRunStatus(input.taskRepoRoot, state.runId, "reviewing", state.updatedAt);
|
|
273
|
-
await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, state);
|
|
274
332
|
return true;
|
|
275
333
|
}
|
|
276
334
|
async function handleHarnessEngineerHook(input) {
|
|
@@ -292,10 +350,23 @@ export function createAutoMemoryService(deps) {
|
|
|
292
350
|
return true;
|
|
293
351
|
}
|
|
294
352
|
if (input.eventName === "StopFailure") {
|
|
295
|
-
await failReview(input.taskRepoRoot, state, "Harness
|
|
353
|
+
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective memory review turn failed.");
|
|
296
354
|
return true;
|
|
297
355
|
}
|
|
298
356
|
if (input.eventName === "Stop") {
|
|
357
|
+
if (!state.retrospectiveReportPath
|
|
358
|
+
|| !(await deps.fs.pathExists(state.retrospectiveReportPath))
|
|
359
|
+
|| !(await deps.fs.readText(state.retrospectiveReportPath)).trim()) {
|
|
360
|
+
await failReview(input.taskRepoRoot, state, "Task Harness Retrospective did not write the required retrospective report.");
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
const report = await deps.fs.readText(state.retrospectiveReportPath);
|
|
364
|
+
const currentMemorySnapshot = await readRunMemorySet(input.taskRepoRoot, state.runId, "before");
|
|
365
|
+
const reportError = validateMemoryReviewReport(report, state.drafts.map((draft) => draft.role), hasSubstantiveMemory(currentMemorySnapshot));
|
|
366
|
+
if (reportError) {
|
|
367
|
+
await failReview(input.taskRepoRoot, state, `Task Harness Retrospective memory review report ${reportError}.`);
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
299
370
|
await applyReviewedMemory(input.taskRepoRoot, state);
|
|
300
371
|
return true;
|
|
301
372
|
}
|
|
@@ -419,42 +490,15 @@ export function createAutoMemoryService(deps) {
|
|
|
419
490
|
draft.status = "dispatched";
|
|
420
491
|
state.updatedAt = now();
|
|
421
492
|
await persistActiveState(taskRepoRoot, state);
|
|
422
|
-
|
|
493
|
+
const planningCandidatePath = draft.role === "architect"
|
|
494
|
+
? await findPlanningCandidateSnapshot(taskRepoRoot, state.runId)
|
|
495
|
+
: undefined;
|
|
496
|
+
await submitTerminalInput(deps.runtime, session.id, buildRoleDraftPrompt(taskRepoRoot, state, draft, planningCandidatePath));
|
|
423
497
|
}
|
|
424
498
|
catch (error) {
|
|
425
499
|
await failReview(taskRepoRoot, state, `Unable to start ${draft.role} memory draft: ${errorMessage(error)}`);
|
|
426
500
|
}
|
|
427
501
|
}
|
|
428
|
-
async function dispatchHarnessReview(baseRepoRoot, taskRepoRoot, state) {
|
|
429
|
-
if (state.status !== "reviewing" || state.reviewPromptDispatchedAt) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (deps.isHarnessEngineerAvailable && !(await deps.isHarnessEngineerAvailable(baseRepoRoot))) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer");
|
|
437
|
-
if (existing?.activityStatus === "running") {
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
const input = { cols: 120, rows: 32 };
|
|
441
|
-
const session = existing?.status === "running"
|
|
442
|
-
? existing
|
|
443
|
-
: existing?.claudeSessionId
|
|
444
|
-
? await deps.sessionService.resumeRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer", input)
|
|
445
|
-
: await deps.sessionService.startRoleSession(baseRepoRoot, state.taskSlug, "harness-engineer", input);
|
|
446
|
-
if (session.status !== "running" || session.activityStatus === "running" || !deps.runtime.getSession(session.id)) {
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
state.reviewPromptDispatchedAt = now();
|
|
450
|
-
state.updatedAt = state.reviewPromptDispatchedAt;
|
|
451
|
-
await persistActiveState(taskRepoRoot, state);
|
|
452
|
-
await submitTerminalInput(deps.runtime, session.id, buildHarnessReviewPrompt(taskRepoRoot, state));
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
await failReview(taskRepoRoot, state, `Unable to start Harness Engineer memory review: ${errorMessage(error)}`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
502
|
async function ensureWorkflowRoleSession(baseRepoRoot, taskSlug, role) {
|
|
459
503
|
const existing = await deps.sessionService.getRoleSession(baseRepoRoot, taskSlug, role);
|
|
460
504
|
if (existing?.status === "running" && deps.runtime.getSession(existing.id)) {
|
|
@@ -467,6 +511,20 @@ export function createAutoMemoryService(deps) {
|
|
|
467
511
|
}
|
|
468
512
|
return deps.sessionService.startRoleSession(baseRepoRoot, taskSlug, role, options);
|
|
469
513
|
}
|
|
514
|
+
async function snapshotArchitectPlanningCandidate(taskRepoRoot, runId) {
|
|
515
|
+
const sourcePath = resolveRepoPath(taskRepoRoot, ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH);
|
|
516
|
+
if (!(await deps.fs.pathExists(sourcePath))) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const snapshotPath = resolveRepoPath(taskRepoRoot, architectPlanningCandidateSnapshotPath(runId));
|
|
520
|
+
await deps.fs.writeText(snapshotPath, ensureTrailingNewline(await deps.fs.readText(sourcePath)));
|
|
521
|
+
}
|
|
522
|
+
async function findPlanningCandidateSnapshot(taskRepoRoot, runId) {
|
|
523
|
+
const relativePath = architectPlanningCandidateSnapshotPath(runId);
|
|
524
|
+
return await deps.fs.pathExists(resolveRepoPath(taskRepoRoot, relativePath))
|
|
525
|
+
? relativePath
|
|
526
|
+
: undefined;
|
|
527
|
+
}
|
|
470
528
|
async function applyReviewedMemory(taskRepoRoot, state) {
|
|
471
529
|
try {
|
|
472
530
|
const before = await readRunMemorySet(taskRepoRoot, state.runId, "before");
|
|
@@ -675,6 +733,8 @@ export function createAutoMemoryService(deps) {
|
|
|
675
733
|
updateFile,
|
|
676
734
|
revertRun,
|
|
677
735
|
retryFailedReview,
|
|
736
|
+
prepareTaskRetrospectiveReview,
|
|
737
|
+
cancelTaskRetrospectiveReview,
|
|
678
738
|
isRoleMemoryTurn,
|
|
679
739
|
handleRoleHook,
|
|
680
740
|
handleHarnessEngineerHook,
|
|
@@ -710,39 +770,30 @@ function toActiveReview(state) {
|
|
|
710
770
|
error: state.error
|
|
711
771
|
};
|
|
712
772
|
}
|
|
713
|
-
function buildRoleDraftPrompt(taskRepoRoot, state, draft) {
|
|
773
|
+
function buildRoleDraftPrompt(taskRepoRoot, state, draft, planningCandidatePath) {
|
|
714
774
|
const roleDefinition = MEMORY_FILE_DEFINITIONS.find((definition) => "role" in definition && definition.role === draft.role);
|
|
715
775
|
if (!roleDefinition) {
|
|
716
776
|
throw new Error(`Missing memory definition for role: ${draft.role}`);
|
|
717
777
|
}
|
|
718
|
-
|
|
778
|
+
const prompt = [
|
|
719
779
|
"[VCM Task Harness Review: Memory Proposal]",
|
|
720
780
|
"",
|
|
721
781
|
"Use the vcm-propose-memory skill to submit the assigned proposal.",
|
|
722
782
|
`Task worktree: ${taskRepoRoot}`,
|
|
723
783
|
`Current shared memory block: ${resolveRepoPath(taskRepoRoot, "CLAUDE.md")}`,
|
|
724
784
|
`Current role memory block: ${resolveRepoPath(taskRepoRoot, roleDefinition.path)}`,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
785
|
+
...(planningCandidatePath
|
|
786
|
+
? [
|
|
787
|
+
`Planning-session memory candidate: ${resolveRepoPath(taskRepoRoot, planningCandidatePath)}`,
|
|
788
|
+
"Review that candidate against final task evidence. Carry forward only facts that remain verified after implementation and testing."
|
|
789
|
+
]
|
|
790
|
+
: []),
|
|
791
|
+
`Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`
|
|
792
|
+
];
|
|
732
793
|
return [
|
|
733
|
-
|
|
734
|
-
"",
|
|
735
|
-
"Auto Memory is enabled. Review the role proposals and task evidence, then produce the complete next memory set.",
|
|
736
|
-
`Task worktree: ${taskRepoRoot}`,
|
|
737
|
-
`Role drafts: ${path.join(runRoot, "drafts")}`,
|
|
738
|
-
`Current memory snapshot: ${path.join(runRoot, "before")}`,
|
|
739
|
-
`Write the complete reviewed memory set to: ${path.join(runRoot, "after")}`,
|
|
794
|
+
...prompt,
|
|
740
795
|
"",
|
|
741
|
-
"
|
|
742
|
-
"Keep only verified, durable, reusable project knowledge. Merge duplicates, remove stale entries, and keep role-specific knowledge in the matching role file.",
|
|
743
|
-
"Do not record task narrative, temporary state, unverified conclusions, or Harness rules.",
|
|
744
|
-
"Do not edit product code, active harness files, active memory blocks, or review metadata.",
|
|
745
|
-
"All existing files already exist in the after directory. Edit those files in place and end the turn when review is complete."
|
|
796
|
+
"End the turn after writing the draft."
|
|
746
797
|
].join("\n");
|
|
747
798
|
}
|
|
748
799
|
function requireMemoryFileDefinition(filePath) {
|
|
@@ -792,6 +843,12 @@ function hashMemorySet(memory) {
|
|
|
792
843
|
sha256(memory[definition.path] ?? "")
|
|
793
844
|
]));
|
|
794
845
|
}
|
|
846
|
+
function hasSubstantiveMemory(memory) {
|
|
847
|
+
return Object.values(memory).some((content) => {
|
|
848
|
+
const normalized = content.trim();
|
|
849
|
+
return Boolean(normalized && normalized !== "No accumulated project memory yet.");
|
|
850
|
+
});
|
|
851
|
+
}
|
|
795
852
|
function sameHashes(left, right) {
|
|
796
853
|
return MEMORY_FILE_DEFINITIONS.every((definition) => left[definition.path] === right[definition.path]);
|
|
797
854
|
}
|
|
@@ -133,7 +133,7 @@ export function createClaudeHookService(deps) {
|
|
|
133
133
|
if (!session) {
|
|
134
134
|
return completedHookResult(input, eventName);
|
|
135
135
|
}
|
|
136
|
-
const activeTask = deps.autoMemoryService
|
|
136
|
+
const activeTask = deps.autoMemoryService || deps.harnessFeedbackService
|
|
137
137
|
? (await deps.taskService.listTasks(context.project.repoRoot))
|
|
138
138
|
.find((task) => task.cleanupStatus !== "cleaned" && (projectScoped || task.taskSlug === input.taskSlug))
|
|
139
139
|
: undefined;
|
|
@@ -145,7 +145,17 @@ export function createClaudeHookService(deps) {
|
|
|
145
145
|
eventName
|
|
146
146
|
})
|
|
147
147
|
: false;
|
|
148
|
-
|
|
148
|
+
const memoryState = activeTask && deps.autoMemoryService
|
|
149
|
+
? await deps.autoMemoryService.getState(context.project.repoRoot, getTaskRuntimeRepoRoot(activeTask))
|
|
150
|
+
: undefined;
|
|
151
|
+
const retrospectiveHandled = activeTask
|
|
152
|
+
? await deps.harnessFeedbackService?.handleTaskRetrospectiveHook(context.project.repoRoot, {
|
|
153
|
+
taskSlug: activeTask.taskSlug,
|
|
154
|
+
eventName,
|
|
155
|
+
memoryReviewSucceeded: memoryState?.status !== "failed"
|
|
156
|
+
})
|
|
157
|
+
: false;
|
|
158
|
+
if (memoryHandled || retrospectiveHandled) {
|
|
149
159
|
return {
|
|
150
160
|
ok: true,
|
|
151
161
|
eventName,
|