vibe-coding-master 0.7.28 → 0.7.29

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 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,
@@ -251,11 +251,6 @@ export function createDefaultServerDeps(options = {}) {
251
251
  runFixedInstaller: createScriptFixedHarnessInstaller(path.join(appRoot, "scripts/install-vcm-harness.mjs")),
252
252
  vcmVersion
253
253
  });
254
- const harnessFeedbackService = createHarnessFeedbackService({
255
- fs,
256
- runtime,
257
- sessionService
258
- });
259
254
  const autoMemoryService = createAutoMemoryService({
260
255
  fs,
261
256
  git,
@@ -266,6 +261,12 @@ export function createDefaultServerDeps(options = {}) {
266
261
  return true;
267
262
  }
268
263
  });
264
+ const harnessFeedbackService = createHarnessFeedbackService({
265
+ fs,
266
+ runtime,
267
+ sessionService,
268
+ autoMemoryService
269
+ });
269
270
  const commandDispatcher = createCommandDispatcher({
270
271
  runtime,
271
272
  sessionService,
@@ -280,7 +281,8 @@ export function createDefaultServerDeps(options = {}) {
280
281
  const architectRestartService = createArchitectRestartService({
281
282
  fs,
282
283
  taskService,
283
- sessionService
284
+ sessionService,
285
+ appSettings
284
286
  });
285
287
  const messageService = createMessageService({
286
288
  fs,
@@ -388,6 +390,7 @@ export function createDefaultServerDeps(options = {}) {
388
390
  runtime,
389
391
  harnessService,
390
392
  autoMemoryService,
393
+ harnessFeedbackService,
391
394
  gatewayService,
392
395
  jobGuard: createJobGuardService(),
393
396
  translationWorkerService,
@@ -2,6 +2,8 @@ import path from "node:path";
2
2
  import { resolveRepoPath } from "../adapters/filesystem.js";
3
3
  import { 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;
@@ -21,6 +23,17 @@ export function createArchitectRestartService(deps) {
21
23
  async schedule(repoRoot, taskSlug) {
22
24
  const session = await requireRunningArchitect(repoRoot, taskSlug);
23
25
  await requireCompletePlan(repoRoot, taskSlug);
26
+ const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
27
+ ? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
28
+ : undefined;
29
+ const task = await deps.taskService.loadTask(repoRoot, taskSlug);
30
+ const candidatePath = resolveRepoPath(getTaskRuntimeRepoRoot(task), ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH);
31
+ if (deps.fs.removePath) {
32
+ await deps.fs.removePath(candidatePath, { force: true });
33
+ }
34
+ else if (await deps.fs.pathExists(candidatePath)) {
35
+ await deps.fs.writeText(candidatePath, "");
36
+ }
24
37
  const key = taskKey(repoRoot, taskSlug);
25
38
  const existing = pendingByTask.get(key);
26
39
  if (existing?.sessionId === session.id) {
@@ -29,7 +42,13 @@ export function createArchitectRestartService(deps) {
29
42
  existing.acceptedMessageId = undefined;
30
43
  existing.gateAccepted = false;
31
44
  existing.executing = false;
32
- return { taskSlug, sessionId: session.id, status: "scheduled" };
45
+ existing.memoryCandidatePath = memoryCandidatePath;
46
+ return {
47
+ taskSlug,
48
+ sessionId: session.id,
49
+ status: "scheduled",
50
+ ...(memoryCandidatePath ? { memoryCandidatePath } : {})
51
+ };
33
52
  }
34
53
  pendingByTask.set(key, {
35
54
  repoRoot,
@@ -37,9 +56,15 @@ export function createArchitectRestartService(deps) {
37
56
  sessionId: session.id,
38
57
  stopped: false,
39
58
  gateAccepted: false,
40
- executing: false
59
+ executing: false,
60
+ memoryCandidatePath
41
61
  });
42
- return { taskSlug, sessionId: session.id, status: "scheduled" };
62
+ return {
63
+ taskSlug,
64
+ sessionId: session.id,
65
+ status: "scheduled",
66
+ ...(memoryCandidatePath ? { memoryCandidatePath } : {})
67
+ };
43
68
  },
44
69
  async recordArchitectStop(repoRoot, taskSlug, sessionId) {
45
70
  const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
@@ -123,6 +148,7 @@ export function createArchitectRestartService(deps) {
123
148
  pending.executing = true;
124
149
  try {
125
150
  await requireCompletePlan(pending.repoRoot, pending.taskSlug);
151
+ await requirePlanningMemoryCandidate(pending);
126
152
  await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
127
153
  permissionMode: session.permissionMode,
128
154
  model: session.model,
@@ -135,6 +161,21 @@ export function createArchitectRestartService(deps) {
135
161
  pending.executing = false;
136
162
  }
137
163
  }
164
+ async function requirePlanningMemoryCandidate(pending) {
165
+ if (!pending.memoryCandidatePath) {
166
+ return;
167
+ }
168
+ const task = await deps.taskService.loadTask(pending.repoRoot, pending.taskSlug);
169
+ const candidatePath = resolveRepoPath(getTaskRuntimeRepoRoot(task), pending.memoryCandidatePath);
170
+ if (!(await deps.fs.pathExists(candidatePath))) {
171
+ throw invalidMemoryCandidateError("the assigned candidate file does not exist.");
172
+ }
173
+ const content = await deps.fs.readText(candidatePath);
174
+ const validationError = validateMemoryProposal(content);
175
+ if (validationError) {
176
+ throw invalidMemoryCandidateError(`the assigned candidate ${validationError}.`);
177
+ }
178
+ }
138
179
  }
139
180
  function isArchitectToPm(message) {
140
181
  return message.fromRole === ARCHITECT_ROLE && message.toRole === PM_ROLE;
@@ -149,3 +190,10 @@ function incompletePlanError(reason) {
149
190
  statusCode: 409
150
191
  });
151
192
  }
193
+ function invalidMemoryCandidateError(reason) {
194
+ return new VcmError({
195
+ code: "ARCHITECT_MEMORY_CANDIDATE_INVALID",
196
+ message: `Architect restart is waiting for its planning-session memory candidate because ${reason}`,
197
+ statusCode: 409
198
+ });
199
+ }
@@ -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,
@@ -49,7 +49,7 @@ export function createHarnessFeedbackService(deps) {
49
49
  });
50
50
  }
51
51
  const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
52
- if (existingMarker) {
52
+ if (existingMarker && existingMarker.status !== "failed") {
53
53
  throw new VcmError({
54
54
  code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
55
55
  message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
@@ -77,19 +77,82 @@ export function createHarnessFeedbackService(deps) {
77
77
  const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
78
78
  const timestamp = now();
79
79
  const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
80
- await persistTaskRetrospectiveMarker(repoRoot, {
80
+ const analysisAbsolutePath = resolveRepoPath(repoRoot, analysisPath);
81
+ const memoryReview = await deps.autoMemoryService?.prepareTaskRetrospectiveReview(input.taskRepoRoot, analysisAbsolutePath);
82
+ const marker = {
81
83
  version: 1,
82
84
  taskSlug,
83
85
  trigger: input.trigger,
84
- status: "triggered",
86
+ status: "running",
85
87
  analysisPath,
86
88
  finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
89
+ ...(memoryReview ? { memoryRunId: memoryReview.runId } : {}),
87
90
  createdAt: timestamp,
88
91
  updatedAt: timestamp
89
- });
90
- await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath));
92
+ };
93
+ const pendingFeedback = await listPendingFeedback(repoRoot);
94
+ try {
95
+ await persistTaskRetrospectiveMarker(repoRoot, marker);
96
+ await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedback.map((item) => item.path), memoryReview));
97
+ }
98
+ catch (error) {
99
+ if (memoryReview) {
100
+ await deps.autoMemoryService?.cancelTaskRetrospectiveReview(input.taskRepoRoot, memoryReview.runId);
101
+ }
102
+ const failedAt = now();
103
+ await persistTaskRetrospectiveMarker(repoRoot, {
104
+ ...marker,
105
+ status: "failed",
106
+ failedAt,
107
+ updatedAt: failedAt,
108
+ error: errorMessage(error)
109
+ });
110
+ throw error;
111
+ }
91
112
  return getState(repoRoot);
92
113
  }
114
+ async function handleTaskRetrospectiveHook(repoRoot, input) {
115
+ const marker = await loadTaskRetrospectiveMarker(repoRoot, input.taskSlug);
116
+ if (!marker || (marker.status !== "running" && marker.status !== "triggered")) {
117
+ return false;
118
+ }
119
+ if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
120
+ return true;
121
+ }
122
+ const timestamp = now();
123
+ if (input.eventName === "StopFailure") {
124
+ await persistTaskRetrospectiveMarker(repoRoot, {
125
+ ...marker,
126
+ status: "failed",
127
+ failedAt: timestamp,
128
+ updatedAt: timestamp,
129
+ error: "Harness Engineer Task Harness Retrospective turn failed."
130
+ });
131
+ return true;
132
+ }
133
+ const reportPath = resolveRepoPath(repoRoot, marker.analysisPath);
134
+ const reportReady = await deps.fs.pathExists(reportPath)
135
+ && Boolean((await deps.fs.readText(reportPath)).trim());
136
+ if (!reportReady || (marker.memoryRunId && !input.memoryReviewSucceeded)) {
137
+ await persistTaskRetrospectiveMarker(repoRoot, {
138
+ ...marker,
139
+ status: "failed",
140
+ failedAt: timestamp,
141
+ updatedAt: timestamp,
142
+ error: !reportReady
143
+ ? "Harness Engineer did not write the required Task Harness Retrospective report."
144
+ : "Task Harness Retrospective memory review failed."
145
+ });
146
+ return true;
147
+ }
148
+ await persistTaskRetrospectiveMarker(repoRoot, {
149
+ ...marker,
150
+ status: "completed",
151
+ completedAt: timestamp,
152
+ updatedAt: timestamp
153
+ });
154
+ return true;
155
+ }
93
156
  async function assertHarnessEngineerAvailable(_repoRoot) {
94
157
  return undefined;
95
158
  }
@@ -162,12 +225,76 @@ export function createHarnessFeedbackService(deps) {
162
225
  summary: metadata.summary
163
226
  };
164
227
  }
165
- function buildTaskRetrospectivePrompt(repoRoot, analysisPath) {
228
+ function buildTaskRetrospectivePrompt(repoRoot, analysisPath, pendingFeedbackPaths, memoryReview) {
229
+ const pendingFeedback = pendingFeedbackPaths.length > 0
230
+ ? pendingFeedbackPaths.map((feedbackPath) => `- ${resolveRepoPath(repoRoot, feedbackPath)}`)
231
+ : ["none"];
166
232
  return [
167
233
  "[VCM Task Harness Retrospective]",
168
234
  "",
169
235
  "Review the completed task from the current active task worktree.",
170
236
  "",
237
+ `Pending Feedback Directory: ${resolveRepoPath(repoRoot, PENDING_DIR)}`,
238
+ "",
239
+ "Pending Feedback:",
240
+ ...pendingFeedback,
241
+ ...(pendingFeedbackPaths.length > 0
242
+ ? [
243
+ "",
244
+ "Process every listed feedback inside this retrospective. Record every disposition in the retrospective report, then delete the processed feedback files before ending the turn."
245
+ ]
246
+ : []),
247
+ ...(memoryReview
248
+ ? [
249
+ "",
250
+ "Auto Memory Review:",
251
+ `Role drafts: ${memoryReview.roleDraftsPath}`,
252
+ `Current memory snapshot: ${memoryReview.currentMemoryPath}`,
253
+ ...(memoryReview.planningCandidatePath
254
+ ? [`Architect planning-session candidate: ${memoryReview.planningCandidatePath}`]
255
+ : []),
256
+ `Write the complete reviewed memory set to: ${memoryReview.reviewedMemoryPath}`,
257
+ "",
258
+ "Review every memory candidate against final task evidence while performing this retrospective.",
259
+ "Each snapshot file contains only the matching <VCM-memory> block content. Edit every existing reviewed-memory file in place.",
260
+ "Before evaluating proposals, review every substantive entry in every current memory snapshot against current code, documentation, and final task evidence.",
261
+ "For each existing entry, decide retain, update, remove, or move-to-durable-doc. Record the decision reason, the impact of removing it, and whether memory or a durable document is the correct source.",
262
+ "Complete this full existing-memory review even when every proposal says no-change.",
263
+ "Then evaluate every proposal, including its stated need, absence impact, and durable-document disposition.",
264
+ "Keep only verified, durable, reusable project knowledge. Merge duplicates and keep role-specific knowledge in the matching role file.",
265
+ "Do not record task narrative, temporary state, unverified conclusions, or Harness rules in memory.",
266
+ "Existing-memory Target must be shared or the exact role name. Decision must be retain, update, remove, or move-to-durable-doc.",
267
+ "Durable doc disposition must be memory, durable-doc, or memory-reference. Use Durable doc path: none with memory and an actual path with the other dispositions.",
268
+ "Do not keep full content in memory when durable-doc is correct. Use memory-reference only when an ongoing role needs the document pointer.",
269
+ "Use none as the complete Existing Memory Decisions body only when no substantive existing memory entry exists.",
270
+ "Use this exact block in the retrospective report and replace each option or placeholder with one allowed value or a concise summary:",
271
+ "",
272
+ "## Memory Review",
273
+ "Existing memory reviewed: complete",
274
+ "",
275
+ "### Proposal Dispositions",
276
+ ...memoryReview.proposalRoles.map((role) => `- ${role}: accepted|rejected|no-change`),
277
+ "",
278
+ "### Existing Memory Decisions",
279
+ "#### Item 1",
280
+ "Target: shared",
281
+ "Existing: <exact existing memory entry>",
282
+ "Decision: retain",
283
+ "Reason: <why this decision is correct>",
284
+ "Impact if removed: <specific future role or task failure>",
285
+ "Durable doc disposition: memory",
286
+ "Durable doc path: none",
287
+ "Evidence: <current code, durable documentation, or final task evidence>",
288
+ "",
289
+ "### Existing Memory Changes",
290
+ "- retained: <summary or none>",
291
+ "- updated: <summary or none>",
292
+ "- removed: <summary or none>",
293
+ "",
294
+ "Reviewed memory set: complete"
295
+ ]
296
+ : []),
297
+ "",
171
298
  `Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
172
299
  "End your turn after writing the result."
173
300
  ].join("\n");
@@ -190,7 +317,7 @@ export function createHarnessFeedbackService(deps) {
190
317
  return deps.fs.readJson(markerPath);
191
318
  }
192
319
  async function persistTaskRetrospectiveMarker(repoRoot, marker) {
193
- const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(String(marker.taskSlug ?? "")));
320
+ const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(marker.taskSlug));
194
321
  await deps.fs.ensureDir(path.dirname(markerPath));
195
322
  await deps.fs.writeJsonAtomic(markerPath, marker);
196
323
  }
@@ -214,6 +341,7 @@ export function createHarnessFeedbackService(deps) {
214
341
  getState,
215
342
  sendPendingFeedback,
216
343
  startTaskRetrospective,
344
+ handleTaskRetrospectiveHook,
217
345
  assertHarnessEngineerAvailable
218
346
  };
219
347
  }
@@ -245,3 +373,6 @@ function sanitizeFeedbackId(value) {
245
373
  function sha256(content) {
246
374
  return createHash("sha256").update(content).digest("hex");
247
375
  }
376
+ function errorMessage(error) {
377
+ return error instanceof Error ? error.message : String(error);
378
+ }
@@ -0,0 +1,80 @@
1
+ const OPERATIONS = ["Add", "Update", "Remove"];
2
+ export function validateMemoryProposal(content) {
3
+ if (!/^# Memory Proposal\s*$/m.test(content)) {
4
+ return "is missing the # Memory Proposal heading";
5
+ }
6
+ const decisionMatches = [...content.matchAll(/^Decision:[ \t]*(update|no-change)[ \t]*$/gm)];
7
+ if (decisionMatches.length !== 1) {
8
+ return "must contain exactly one Decision: update or Decision: no-change field";
9
+ }
10
+ const levelTwoHeadings = [...content.matchAll(/^## ([^\r\n]+?)[ \t]*$/gm)];
11
+ const sections = [...content.matchAll(/^## (Add|Update|Remove)[ \t]*$/gm)];
12
+ if (levelTwoHeadings.length !== OPERATIONS.length
13
+ || sections.length !== OPERATIONS.length
14
+ || sections.some((section, index) => section[1] !== OPERATIONS[index])) {
15
+ return "must contain only the Add, Update, and Remove sections, exactly once and in that order";
16
+ }
17
+ let itemCount = 0;
18
+ for (let index = 0; index < sections.length; index += 1) {
19
+ const section = sections[index];
20
+ const operation = section[1];
21
+ const bodyStart = (section.index ?? 0) + section[0].length;
22
+ const bodyEnd = index + 1 < sections.length
23
+ ? sections[index + 1].index ?? content.length
24
+ : content.length;
25
+ const result = validateOperationBody(operation, content.slice(bodyStart, bodyEnd));
26
+ if (typeof result === "string") {
27
+ return result;
28
+ }
29
+ itemCount += result;
30
+ }
31
+ const decision = decisionMatches[0][1];
32
+ if (decision === "no-change" && itemCount !== 0) {
33
+ return "uses Decision: no-change but contains a memory item";
34
+ }
35
+ if (decision === "update" && itemCount === 0) {
36
+ return "uses Decision: update without a structured memory item";
37
+ }
38
+ return undefined;
39
+ }
40
+ function validateOperationBody(operation, rawBody) {
41
+ const body = rawBody.trim();
42
+ if (body === "none") {
43
+ return 0;
44
+ }
45
+ const itemHeadings = [...body.matchAll(/^### Item \d+\s*$/gm)];
46
+ if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
47
+ return `${operation} must contain none or one or more ### Item N blocks`;
48
+ }
49
+ for (let index = 0; index < itemHeadings.length; index += 1) {
50
+ const heading = itemHeadings[index];
51
+ const itemStart = (heading.index ?? 0) + heading[0].length;
52
+ const itemEnd = index + 1 < itemHeadings.length
53
+ ? itemHeadings[index + 1].index ?? body.length
54
+ : body.length;
55
+ const item = body.slice(itemStart, itemEnd).trim();
56
+ const expectedPattern = operation === "Add"
57
+ ? /^Target:[ \t]*(shared|current-role)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
58
+ : operation === "Update"
59
+ ? /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nContent:[ \t]*(\S.*)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if absent:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/
60
+ : /^Target:[ \t]*(shared|current-role)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/;
61
+ const fieldMatch = expectedPattern.exec(item);
62
+ if (!fieldMatch) {
63
+ const fields = operation === "Add"
64
+ ? "Target, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
65
+ : operation === "Update"
66
+ ? "Target, Existing, Content, Reason, Impact if absent, Durable doc disposition, Durable doc path, and Evidence"
67
+ : "Target, Existing, and Evidence";
68
+ return `${operation} ${heading[0].trim()} must contain one-line ${fields} fields in that order`;
69
+ }
70
+ if (operation === "Add" || operation === "Update") {
71
+ const disposition = fieldMatch[operation === "Add" ? 5 : 6];
72
+ const durableDocPath = fieldMatch[operation === "Add" ? 6 : 7];
73
+ if ((disposition === "memory" && durableDocPath !== "none")
74
+ || (disposition !== "memory" && durableDocPath === "none")) {
75
+ return `${operation} ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
76
+ }
77
+ }
78
+ }
79
+ return itemHeadings.length;
80
+ }
@@ -0,0 +1,7 @@
1
+ export const MEMORY_REVIEW_ROOT = ".ai/vcm/memory-review";
2
+ export const MEMORY_REVIEW_RUNS_ROOT = `${MEMORY_REVIEW_ROOT}/runs`;
3
+ export const MEMORY_REVIEW_STATE_PATH = `${MEMORY_REVIEW_ROOT}/state.json`;
4
+ export const ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH = `${MEMORY_REVIEW_ROOT}/candidates/architect/planning.md`;
5
+ export function architectPlanningCandidateSnapshotPath(runId) {
6
+ return `${MEMORY_REVIEW_RUNS_ROOT}/${runId}/sources/architect-planning.md`;
7
+ }
@@ -0,0 +1,94 @@
1
+ export function validateMemoryReviewReport(content, proposalRoles, hasExistingMemory = false) {
2
+ const memoryReview = /^## Memory Review\s*$/m.exec(content);
3
+ if (!memoryReview || memoryReview.index === undefined) {
4
+ return "is missing the ## Memory Review section";
5
+ }
6
+ const sectionStart = memoryReview.index + memoryReview[0].length;
7
+ const nextSection = /^## (?!#)/m.exec(content.slice(sectionStart));
8
+ const section = content.slice(sectionStart, nextSection?.index === undefined ? content.length : sectionStart + nextSection.index);
9
+ if (!/^Existing memory reviewed:[ \t]*complete[ \t]*$/m.test(section)) {
10
+ return "must declare Existing memory reviewed: complete";
11
+ }
12
+ if (!/^Reviewed memory set:[ \t]*complete[ \t]*$/m.test(section)) {
13
+ return "must declare Reviewed memory set: complete";
14
+ }
15
+ const dispositions = extractReportSubsection(section, "Proposal Dispositions", "Existing Memory Decisions");
16
+ if (dispositions === undefined) {
17
+ return "is missing the Proposal Dispositions subsection";
18
+ }
19
+ for (const role of proposalRoles) {
20
+ const matches = dispositions.match(new RegExp(`^- ${escapeRegExp(role)}:[ \\t]*(accepted|rejected|no-change)[ \\t]*$`, "gm"));
21
+ if (matches?.length !== 1) {
22
+ return `must record exactly one accepted, rejected, or no-change disposition for ${role}`;
23
+ }
24
+ }
25
+ const existingDecisions = extractReportSubsection(section, "Existing Memory Decisions", "Existing Memory Changes");
26
+ if (existingDecisions === undefined) {
27
+ return "is missing the Existing Memory Decisions subsection";
28
+ }
29
+ const existingDecisionError = validateExistingMemoryDecisions(existingDecisions, hasExistingMemory);
30
+ if (existingDecisionError) {
31
+ return existingDecisionError;
32
+ }
33
+ const existingChanges = extractReportSubsection(section, "Existing Memory Changes");
34
+ if (existingChanges === undefined) {
35
+ return "is missing the Existing Memory Changes subsection";
36
+ }
37
+ for (const field of ["retained", "updated", "removed"]) {
38
+ const matches = existingChanges.match(new RegExp(`^- ${field}:[ \\t]*\\S.*$`, "gm"));
39
+ if (matches?.length !== 1) {
40
+ return `must record exactly one non-empty ${field} summary`;
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ function validateExistingMemoryDecisions(content, hasExistingMemory) {
46
+ const body = content.trim();
47
+ if (body === "none") {
48
+ return hasExistingMemory
49
+ ? "Existing Memory Decisions cannot be none while substantive existing memory is present"
50
+ : undefined;
51
+ }
52
+ const itemHeadings = [...body.matchAll(/^#### Item \d+[ \t]*$/gm)];
53
+ if (itemHeadings.length === 0 || body.slice(0, itemHeadings[0].index).trim()) {
54
+ return "Existing Memory Decisions must contain none or one or more #### Item N blocks";
55
+ }
56
+ for (let index = 0; index < itemHeadings.length; index += 1) {
57
+ const heading = itemHeadings[index];
58
+ const itemStart = (heading.index ?? 0) + heading[0].length;
59
+ const itemEnd = index + 1 < itemHeadings.length
60
+ ? itemHeadings[index + 1].index ?? body.length
61
+ : body.length;
62
+ const item = body.slice(itemStart, itemEnd).trim();
63
+ const match = /^Target:[ \t]*(shared|project-manager|architect|coder|tester|reviewer|harness-engineer)[ \t]*\nExisting:[ \t]*(\S.*)[ \t]*\nDecision:[ \t]*(retain|update|remove|move-to-durable-doc)[ \t]*\nReason:[ \t]*(\S.*)[ \t]*\nImpact if removed:[ \t]*(\S.*)[ \t]*\nDurable doc disposition:[ \t]*(memory|durable-doc|memory-reference)[ \t]*\nDurable doc path:[ \t]*(\S.*)[ \t]*\nEvidence:[ \t]*(\S.*)[ \t]*$/.exec(item);
64
+ if (!match) {
65
+ return `Existing Memory Decisions ${heading[0].trim()} must contain one-line Target, Existing, Decision, Reason, Impact if removed, Durable doc disposition, Durable doc path, and Evidence fields in that order`;
66
+ }
67
+ const disposition = match[6];
68
+ const durableDocPath = match[7];
69
+ if ((disposition === "memory" && durableDocPath !== "none")
70
+ || (disposition !== "memory" && durableDocPath === "none")) {
71
+ return `Existing Memory Decisions ${heading[0].trim()} must use Durable doc path: none only with Durable doc disposition: memory`;
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ function extractReportSubsection(content, heading, nextHeading) {
77
+ const start = new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
78
+ if (!start || start.index === undefined) {
79
+ return undefined;
80
+ }
81
+ const bodyStart = start.index + start[0].length;
82
+ if (nextHeading) {
83
+ const end = new RegExp(`^### ${escapeRegExp(nextHeading)}\\s*$`, "m")
84
+ .exec(content.slice(bodyStart));
85
+ return end?.index === undefined
86
+ ? undefined
87
+ : content.slice(bodyStart, bodyStart + end.index);
88
+ }
89
+ const reviewedSet = /^Reviewed memory set:/m.exec(content.slice(bodyStart));
90
+ return content.slice(bodyStart, reviewedSet?.index === undefined ? content.length : bodyStart + reviewedSet.index);
91
+ }
92
+ function escapeRegExp(value) {
93
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
94
+ }
@@ -116,7 +116,8 @@ ${renderRoleMemoryRules("architect")}
116
116
  #### Planning Completion
117
117
 
118
118
  - After the complete plan, scaffold, reconciliation, L0 evidence, and commits are ready, use the \`restart-architect\` skill before writing the completed Architect-to-PM route message.
119
- - After VCM reports the restart is scheduled, write the route message with both architecture artifacts and the plan, then end the turn. VCM keeps this session for any architecture-plan Gate revision and restarts it only after the Gate is accepted. Do not wait for or inspect the replacement session.
119
+ - If VCM returns a \`memoryCandidatePath\`, use \`vcm-propose-memory\` to write the planning-session memory candidate to that path before routing. Include only verified, durable, reusable project knowledge from planning; do not record task narrative, temporary state, unverified conclusions, or Harness rules.
120
+ - After the required candidate is written, write the route message with both architecture artifacts and the plan, then end the turn. VCM keeps this session for any architecture-plan Gate revision and restarts it only after the Gate is accepted. Do not wait for or inspect the replacement session.
120
121
 
121
122
  ### Complete Task Planning
122
123
 
@@ -7,7 +7,7 @@ export function renderRootClaudeHarnessRules() {
7
7
  - Project-manager uses \`vcm-task-state\` to declare the current workflow checkpoint. This state is recoverable context only; flow rules and task artifacts remain authoritative.
8
8
  - Use \`vcm-long-running-validation\` for long-running validation. Follow the background job limits below.
9
9
  - Use \`vcm-report-harness-issue\` when you notice a reusable VCM harness problem. Record feedback; do not contact Harness Engineer directly.
10
- - The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns a memory proposal during Task Harness Review.
10
+ - The root \`<VCM-memory>\` block is shared project memory. Treat every \`<VCM-memory>\` block as read-only and use \`vcm-propose-memory\` only when VCM assigns an exact memory proposal or candidate path.
11
11
  - Only the user may approve scope reduction, skipped required validation, Gate Review skip or override, skipped required docs sync, accepted unresolved task-scope risk, or weakening of baseline Harness rules. PM may record and route the user's approval but cannot grant it.
12
12
  - Project-manager runs \`vcm-gate-review\` unconditionally at every Gate Review trigger point and on VCM Gate Review callbacks; the tool reports the authoritative enable state.
13
13
 
@@ -38,20 +38,23 @@ You are not part of the task workflow round state.
38
38
  permitted bootstrap edits directly in the active task worktree and commit them
39
39
  yourself.
40
40
  - Retrospective Mode: analyze a completed task for reusable harness problems.
41
- Do not edit harness or memory files.
42
- - Memory Review Mode: when Auto Memory is enabled and VCM starts the memory
43
- phase of Task Harness Review, review role proposals and write only the review
44
- output files assigned by VCM.
41
+ When the assigned prompt includes Auto Memory Review, also review the memory
42
+ proposals and write only the reviewed-memory output files assigned by VCM.
45
43
  - VCM Feedback Mode: draft VCM product, installer, UI, or fixed-template issue
46
44
  feedback. Do not submit without explicit in-session user authorization.
47
45
 
48
46
  ## Change Policy
49
47
 
50
- - Apply edits only in Bootstrap Apply Mode, Memory Review Mode, or when VCM
51
- explicitly asks you to apply an approved harness change.
48
+ - Apply edits only in Bootstrap Apply Mode, to assigned reviewed-memory output
49
+ files during Retrospective Mode, or when VCM explicitly asks you to apply an
50
+ approved harness change.
52
51
  - When applying edits, work only in the active task worktree named by VCM. Do not
53
52
  edit the base repository root unless VCM explicitly says so.
54
- - In Proposal Mode and Retrospective Mode, do not edit files.
53
+ - In Proposal Mode, do not edit files.
54
+ - In Retrospective Mode, write the assigned retrospective report and, only when
55
+ Auto Memory Review is included in the prompt, the assigned reviewed-memory
56
+ output files. After every assigned pending feedback has a recorded
57
+ disposition, delete those processed feedback files.
55
58
  - Commit every applied harness change yourself before ending your turn.
56
59
  - Do not overwrite VCM fixed managed blocks.
57
60
  - Keep project-specific customization outside VCM managed blocks.
@@ -64,11 +67,26 @@ You are not part of the task workflow round state.
64
67
  ## Memory Management
65
68
 
66
69
  - Own VCM-managed project memory in the root and role \`<VCM-memory>\` blocks.
67
- - When Auto Memory is disabled, do not request proposals, start Memory Review
68
- Mode, or update memory.
69
- - During VCM-assigned Memory Review, verify every role proposal against task
70
- evidence, merge duplicates, remove stale entries, and keep role-specific
71
- knowledge in the matching role memory output.
70
+ - When Auto Memory is disabled, do not request proposals or update memory.
71
+ - During a Retrospective that includes Auto Memory Review, first inspect every
72
+ entry in every current memory snapshot. Verify each entry against current
73
+ code, durable documentation, and final task evidence. For every substantive
74
+ entry, decide whether to retain, update, remove, or move it to a durable
75
+ document; record the decision reason, the impact of removing it, and whether
76
+ memory or a durable document is the correct source. Complete this full review
77
+ even when every proposal says \`no-change\`.
78
+ - After reviewing existing memory, verify every role proposal against task
79
+ evidence, including any Architect planning-session candidate assigned by VCM.
80
+ Independently verify the stated need, absence impact, and durable-document
81
+ disposition. Treat every candidate as a proposal rather than authority, merge
82
+ duplicates, remove stale entries, and keep role-specific knowledge in the
83
+ matching role memory output.
84
+ - Do not keep the full content in memory when a durable document is the correct
85
+ source. Use a short memory reference only when the role needs that document
86
+ pointer across tasks.
87
+ - Record every proposal disposition and the retained, updated, and removed
88
+ existing-memory decisions and summary in the exact Memory Review report block
89
+ assigned by VCM.
72
90
  - Do not record task narrative, temporary state, unverified conclusions, or
73
91
  Harness rules in memory.
74
92
  - Edit only the review output paths assigned by VCM. Do not edit active
@@ -91,6 +109,28 @@ context, validation reports, Gate Review reports, final acceptance artifacts,
91
109
  memory drafts, applied memory diffs, current memory, and user corrections during
92
110
  the task.
93
111
 
112
+ Pending Harness Feedback is part of the retrospective, not a separate phase.
113
+ At the start of the retrospective, read every feedback file assigned by VCM
114
+ from \`.ai/vcm/harness-feedback/pending/\`.
115
+
116
+ For each pending feedback:
117
+
118
+ - verify it against the current harness, task evidence, and project behavior
119
+ - decide whether it is confirmed, rejected, duplicate, or already covered
120
+ - record the feedback path, decision, evidence, impact, and required action in
121
+ the retrospective report
122
+
123
+ Process every assigned feedback before completing the retrospective. A
124
+ feedback item is processed even when it is rejected or already covered.
125
+
126
+ Write the complete retrospective report before deleting any feedback file.
127
+ After the report contains a disposition for every assigned feedback, delete
128
+ those feedback files from \`.ai/vcm/harness-feedback/pending/\`.
129
+
130
+ Do not delete a feedback file unless its disposition is already recorded in
131
+ the retrospective report. Do not leave an assigned feedback file pending after
132
+ its disposition has been recorded.
133
+
94
134
  For each finding, decide whether it is:
95
135
 
96
136
  - a reusable harness problem that should be fixed
@@ -108,6 +148,7 @@ Do not edit harness files during retrospective analysis. Write a concise analysi
108
148
  - impact
109
149
  - recommended harness change, or reason no harness change is needed
110
150
  - affected roles, skills, tools, or docs
151
+ - pending feedback path and disposition
111
152
 
112
153
  ## VCM Feedback
113
154
 
@@ -9,7 +9,12 @@ Run:
9
9
  .ai/tools/request-architect-restart
10
10
  \`\`\`
11
11
 
12
- If VCM reports \`scheduled\`, write the completed Architect-to-PM route message and end the turn. VCM keeps the current Architect session through any architecture-plan Gate revision rounds and restarts it only after the route is accepted by PM and that Gate is approved or explicitly excepted.
12
+ If VCM reports \`scheduled\` with a non-empty \`memoryCandidatePath\`, use
13
+ \`vcm-propose-memory\` to write a planning-session memory candidate to that exact
14
+ path before writing the completed route. This candidate is provisional input for
15
+ the later Auto Memory review; it does not edit active memory.
16
+
17
+ Then write the completed Architect-to-PM route message and end the turn. VCM keeps the current Architect session through any architecture-plan Gate revision rounds and restarts it only after the route is accepted by PM and that Gate is approved or explicitly excepted.
13
18
 
14
19
  Do not use this skill for incomplete planning, user clarification, Debug Mode, Architecture Diagnosis Mode, or docs sync.`;
15
20
  }
@@ -54,7 +59,12 @@ def main():
54
59
  try:
55
60
  with urllib.request.urlopen(request, timeout=5) as response:
56
61
  payload = json.loads(response.read().decode("utf-8"))
57
- emit(payload.get("status", "scheduled"), taskSlug=task_slug, sessionId=payload.get("sessionId"))
62
+ emit(
63
+ payload.get("status", "scheduled"),
64
+ taskSlug=task_slug,
65
+ sessionId=payload.get("sessionId"),
66
+ memoryCandidatePath=payload.get("memoryCandidatePath"),
67
+ )
58
68
  return 0
59
69
  except urllib.error.HTTPError as error:
60
70
  try:
@@ -5,8 +5,8 @@ role turns. Update reviewed memory only through the output paths assigned by VCM
5
5
  or explicit user edits in Harness Studio. When Auto Memory is disabled, do not
6
6
  initiate memory proposals, reviews, or updates.`
7
7
  : `Treat the \`<VCM-memory>\` block in this role definition as read-only. Only
8
- when VCM explicitly requests a proposal during Task Harness Review, use
9
- \`vcm-propose-memory\` and write the exact assigned draft path.`;
8
+ when VCM explicitly assigns a memory proposal or candidate path, use
9
+ \`vcm-propose-memory\` and write that exact path.`;
10
10
  return `### Role Memory
11
11
 
12
12
  The \`<VCM-memory>\` block in this role definition is accumulated project context,
@@ -1,33 +1,81 @@
1
1
  export function renderVcmProposeMemorySkillRules() {
2
- return `Use this skill only when VCM explicitly requests a memory proposal during Task
3
- Harness Review and provides an exact draft path.
2
+ return `Use this skill only when VCM explicitly requests a memory proposal or
3
+ planning-session memory candidate and provides an exact path.
4
4
 
5
5
  ## Rules
6
6
 
7
7
  - Treat every \`<VCM-memory>\` block as read-only. This skill creates a proposal;
8
8
  it never edits active memory.
9
- - Write only to the exact draft path assigned by VCM. The path must be under
10
- \`.ai/vcm/memory-review/runs/<run-id>/drafts/\` in the active task worktree.
11
- - If VCM did not provide a draft path, do not create a proposal.
9
+ - Write only to the exact path assigned by VCM. It must be either a role draft
10
+ under \`.ai/vcm/memory-review/runs/<run-id>/drafts/\` or a planning candidate
11
+ under \`.ai/vcm/memory-review/candidates/\` in the active task worktree.
12
+ - If VCM did not provide a path, do not create a proposal.
12
13
  - Propose only verified, durable, reusable project knowledge supported by task
13
14
  evidence.
15
+ - Target shared project knowledge to \`shared\`. Target knowledge used only by
16
+ the current role to \`current-role\`.
17
+ - For every add or update, explain why the memory is necessary, what future
18
+ impact its absence would have, and whether the knowledge belongs in memory,
19
+ a durable document, or a short memory reference to a durable document.
20
+ Use exactly \`memory\`, \`durable-doc\`, or \`memory-reference\`. Use
21
+ \`Durable doc path: none\` with \`memory\` and an actual path with either
22
+ durable-document disposition.
14
23
  - Do not record task narrative, temporary state, unverified conclusions, or
15
24
  Harness rules.
16
25
  - Do not edit handoff artifacts or route messages from this skill.
17
26
 
18
27
  ## Draft Format
19
28
 
29
+ Use this exact format for \`Decision: no-change\`:
30
+
20
31
  \`\`\`markdown
21
32
  # Memory Proposal
22
- Decision: update | no-change
33
+ Decision: no-change
23
34
 
24
35
  ## Add
36
+ none
25
37
 
26
38
  ## Update
39
+ none
27
40
 
28
41
  ## Remove
42
+ none
43
+ \`\`\`
44
+
45
+ For \`Decision: update\`, use one or more numbered items and write every field
46
+ on one line. Use \`none\` as the complete body of an operation section that has
47
+ no item:
48
+
49
+ \`\`\`markdown
50
+ # Memory Proposal
51
+ Decision: update
29
52
 
30
- ## Evidence
53
+ ## Add
54
+ ### Item 1
55
+ Target: shared
56
+ Content: <new memory entry>
57
+ Reason: <why this must remain available across tasks>
58
+ Impact if absent: <specific future role or task failure>
59
+ Durable doc disposition: memory
60
+ Durable doc path: none
61
+ Evidence: <task artifact, code, or durable documentation>
62
+
63
+ ## Update
64
+ ### Item 1
65
+ Target: current-role
66
+ Existing: <exact existing memory entry>
67
+ Content: <replacement memory entry>
68
+ Reason: <why the replacement must remain available across tasks>
69
+ Impact if absent: <specific future role or task failure>
70
+ Durable doc disposition: memory-reference
71
+ Durable doc path: docs/ARCHITECTURE.md
72
+ Evidence: <task artifact, code, or durable documentation>
73
+
74
+ ## Remove
75
+ ### Item 1
76
+ Target: current-role
77
+ Existing: <exact existing memory entry>
78
+ Evidence: <task artifact, code, or durable documentation>
31
79
  \`\`\`
32
80
 
33
81
  Use \`Decision: no-change\` when the completed task produced no qualifying
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-coding-master",
3
- "version": "0.7.28",
3
+ "version": "0.7.29",
4
4
  "description": "Local GUI session cockpit for Claude Code role sessions.",
5
5
  "type": "module",
6
6
  "files": [