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.
Files changed (26) hide show
  1. package/README.md +26 -8
  2. package/dist/backend/api/harness-routes.js +0 -1
  3. package/dist/backend/api/task-routes.js +4 -2
  4. package/dist/backend/server.js +11 -7
  5. package/dist/backend/services/architect-restart-service.js +107 -20
  6. package/dist/backend/services/auto-memory-service.js +118 -61
  7. package/dist/backend/services/claude-hook-service.js +12 -2
  8. package/dist/backend/services/gate-review-service.js +62 -2
  9. package/dist/backend/services/harness-feedback-service.js +138 -7
  10. package/dist/backend/services/memory-proposal-validation.js +80 -0
  11. package/dist/backend/services/memory-review-paths.js +7 -0
  12. package/dist/backend/services/memory-review-validation.js +94 -0
  13. package/dist/backend/templates/harness/architect-agent.js +3 -2
  14. package/dist/backend/templates/harness/claude-root.js +3 -3
  15. package/dist/backend/templates/harness/gate-review.js +79 -9
  16. package/dist/backend/templates/harness/harness-engineer-agent.js +53 -12
  17. package/dist/backend/templates/harness/project-manager-agent.js +15 -13
  18. package/dist/backend/templates/harness/restart-architect-skill.js +16 -2
  19. package/dist/backend/templates/harness/role-memory.js +2 -2
  20. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +55 -7
  21. package/dist/shared/types/architect-restart.js +1 -0
  22. package/dist-frontend/assets/index-CguKU8Q5.css +32 -0
  23. package/dist-frontend/assets/{index-Bq1kZK1u.js → index-Cmr9W7-n.js} +22 -22
  24. package/dist-frontend/index.html +2 -2
  25. package/package.json +1 -1
  26. 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 after post-task memory processing completes
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. Roles cannot edit active memory directly.
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. Harness Engineer review and retrospective work remain tool
492
- role activity and do not participate in Round completion.
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 complete the memory phase before retrospective analysis. A
497
- failed memory review must be retried from Harness Studio before retrospective
498
- can continue.
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;
@@ -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.stopped = false;
28
- existing.deliveredMessageId = undefined;
29
- existing.acceptedMessageId = undefined;
30
- existing.gateAccepted = false;
31
- existing.executing = false;
32
- return { taskSlug, sessionId: session.id, status: "scheduled" };
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
- executing: false
60
+ status: "pending",
61
+ memoryCandidatePath
41
62
  });
42
- return { taskSlug, sessionId: session.id, status: "scheduled" };
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.executing
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
- || session.status !== "running"
120
- || session.activityStatus !== "idle") {
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
- pending.executing = true;
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.executing = false;
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
- const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
9
- const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
10
- const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
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 complete for this Final Acceptance before Task Harness Retrospective."
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
- if (!content || !/^Decision:\s*(update|no-change)\s*$/im.test(content)) {
259
- await failReview(input.taskRepoRoot, state, `${input.role} memory draft is missing a valid Decision: update or Decision: no-change field.`);
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 Engineer memory review turn failed.");
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
- await submitTerminalInput(deps.runtime, session.id, buildRoleDraftPrompt(taskRepoRoot, state, draft));
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
- return [
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
- `Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`,
726
- "",
727
- "End the turn after writing the draft."
728
- ].join("\n");
729
- }
730
- function buildHarnessReviewPrompt(taskRepoRoot, state) {
731
- const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
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
- "[VCM Task Harness Review: Memory Review]",
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
- "Each snapshot file contains only the matching <VCM-memory> block content, not the full host file.",
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
- if (memoryHandled) {
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,