vibe-coding-master 0.7.27 → 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.
Files changed (29) hide show
  1. package/README.md +31 -10
  2. package/dist/backend/api/harness-routes.js +0 -1
  3. package/dist/backend/cli/install-vcm-harness.js +37 -7
  4. package/dist/backend/server.js +9 -6
  5. package/dist/backend/services/architect-restart-service.js +51 -3
  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 +35 -18
  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/handoff.js +20 -16
  14. package/dist/backend/templates/harness/architect-agent.js +3 -2
  15. package/dist/backend/templates/harness/claude-root.js +1 -1
  16. package/dist/backend/templates/harness/coder-agent.js +12 -1
  17. package/dist/backend/templates/harness/coder-worker-agent.js +12 -1
  18. package/dist/backend/templates/harness/harness-engineer-agent.js +53 -12
  19. package/dist/backend/templates/harness/restart-architect-skill.js +12 -2
  20. package/dist/backend/templates/harness/role-memory.js +2 -2
  21. package/dist/backend/templates/harness/tester-agent.js +3 -3
  22. package/dist/backend/templates/harness/vcm-architecture-interview-skill.js +2 -2
  23. package/dist/backend/templates/harness/vcm-final-acceptance-skill.js +1 -1
  24. package/dist/backend/templates/harness/vcm-propose-memory-skill.js +55 -7
  25. package/dist/backend/templates/harness/vcm-route-message-skill.js +2 -5
  26. package/dist/shared/validation/artifact-check.js +78 -64
  27. package/dist/shared/validation/artifact-contract.js +22 -0
  28. package/package.json +1 -1
  29. package/scripts/uninstall-vcm-harness.mjs +5 -0
package/README.md CHANGED
@@ -148,12 +148,15 @@ the active task worktree:
148
148
  - `.claude/agents/**`
149
149
  - `.claude/skills/**`
150
150
  - `.claude/settings.json` hooks
151
- - `.ai/tools/**`
151
+ - VCM protocol and runtime tools under `.ai/tools/**`
152
152
  - `.gitignore` entries for VCM runtime state and task worktrees
153
- - generated-context tooling
153
+ - initial project-owned generated-context tooling
154
154
  - pull request template
155
155
 
156
156
  VCM preserves user-authored content outside VCM managed blocks.
157
+ VCM seeds `.ai/tools/generate-module-index` and
158
+ `.ai/tools/generate-public-surface` only when missing. After installation these
159
+ generators belong to the project and later harness updates do not replace them.
157
160
 
158
161
  The fixed harness install is deterministic and creates a commit in the active
159
162
  task worktree. Bootstrap is AI-assisted and is run through the Harness Engineer
@@ -448,7 +451,7 @@ Use it to:
448
451
  - view and edit shared or role-specific VCM memory
449
452
  - review and revert memory changes recorded in the active task worktree
450
453
  - copy file paths for discussion
451
- - review task harness after post-task memory processing completes
454
+ - review task harness and consolidate optional Auto Memory in one retrospective
452
455
  - inspect commit diffs for harness changes
453
456
  - merge task harness commits back to the connected repository branch when
454
457
  appropriate
@@ -464,7 +467,14 @@ files.
464
467
  Review Task Harness after Final Acceptance, Project Manager, Architect, Coder,
465
468
  Tester, and an enabled Reviewer submit proposals in sequence through
466
469
  `vcm-propose-memory`. Harness Engineer verifies and consolidates them before VCM
467
- 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.
468
478
 
469
479
  Shared memory is stored in the root `CLAUDE.md` `<VCM-memory>` block. Role memory
470
480
  is stored in the matching `.claude/agents/*.md` block. VCM changes only block
@@ -478,21 +488,32 @@ Post-task processing is ordered by the backend:
478
488
  ```text
479
489
  Final Acceptance
480
490
  -> Review Task Harness
491
+ -> Snapshot Architect planning-session memory candidate, when present
481
492
  -> Workflow-role memory proposals, when Auto Memory is enabled
482
- -> Harness Engineer memory review, when Auto Memory is enabled
483
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
484
497
  ```
485
498
 
486
499
  Memory proposal prompts sent to Project Manager, Architect, Coder, Tester, and
487
500
  an enabled Reviewer use their normal task sessions and participate in
488
- Round/Turn tracking. Harness Engineer review and retrospective work remain tool
489
- 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.
490
511
 
491
512
  When Auto Memory is disabled, Review Task Harness does not collect proposals or
492
513
  ask Harness Engineer to update memory. When enabled, both automatic and manual
493
- review requests complete the memory phase before retrospective analysis. A
494
- failed memory review must be retried from Harness Studio before retrospective
495
- 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.
496
517
 
497
518
  ## Closing a Task
498
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,
@@ -229,13 +229,7 @@ const DURABLE_DOC_TEMPLATES = [
229
229
  content: "# Testing\n"
230
230
  },
231
231
  ];
232
- const WHOLE_FILES = [
233
- {
234
- path: ".ai/tools/check-durable-docs",
235
- category: "durable-docs-tool",
236
- mode: 0o755,
237
- templatePath: "scripts/harness-tools/check-durable-docs"
238
- },
232
+ const PROJECT_OWNED_FILES = [
239
233
  {
240
234
  path: ".ai/tools/generate-module-index",
241
235
  category: "generated-context-tool",
@@ -247,6 +241,14 @@ const WHOLE_FILES = [
247
241
  category: "generated-context-tool",
248
242
  mode: 0o755,
249
243
  templatePath: "scripts/harness-tools/generate-public-surface"
244
+ }
245
+ ];
246
+ const WHOLE_FILES = [
247
+ {
248
+ path: ".ai/tools/check-durable-docs",
249
+ category: "durable-docs-tool",
250
+ mode: 0o755,
251
+ templatePath: "scripts/harness-tools/check-durable-docs"
250
252
  },
251
253
  {
252
254
  path: ".claude/skills/vcm-architecture-interview/SKILL.md",
@@ -397,6 +399,9 @@ async function main() {
397
399
  for (const file of WHOLE_FILES) {
398
400
  await installWholeFile({ projectRoot, file, dryRun, operations });
399
401
  }
402
+ for (const file of PROJECT_OWNED_FILES) {
403
+ await installProjectOwnedFile({ projectRoot, file, dryRun, operations });
404
+ }
400
405
  await removeLegacyFlatSkillFiles({ projectRoot, dryRun, operations });
401
406
  await removeLegacyCodexHarnessPaths({ projectRoot, dryRun, operations });
402
407
  await installManifest({
@@ -511,6 +516,14 @@ async function buildManifest(projectRoot) {
511
516
  ...fixedDirectories().map((directory) => manifestEntry(directory, "directory", directoryCategory(directory), "vcm-created")),
512
517
  manifestEntry("docs/GLOSSARY.md", "file", "project-glossary", "project-owned"),
513
518
  manifestEntry("docs/CODING_STANDARDS.md", "file", "project-coding-standards", "project-owned"),
519
+ ...PROJECT_OWNED_FILES.map((file) => ({
520
+ path: file.path,
521
+ entryType: "file",
522
+ category: file.category,
523
+ ownership: "project-owned",
524
+ source: "vcm-template",
525
+ lifecycle: "long-term"
526
+ })),
514
527
  ...WHOLE_FILES.map((file) => ({
515
528
  path: file.path,
516
529
  entryType: "file",
@@ -820,6 +833,23 @@ async function installWholeFile({ projectRoot, file, dryRun, operations }) {
820
833
  action: "write fixed VCM file"
821
834
  });
822
835
  }
836
+ async function installProjectOwnedFile({ projectRoot, file, dryRun, operations }) {
837
+ const targetPath = resolveInside(projectRoot, file.path);
838
+ if (await pathExists(targetPath)) {
839
+ operations.push(skip(file.path, "exists; project-owned"));
840
+ return;
841
+ }
842
+ const content = await wholeFileContent(file);
843
+ await writeIfChanged({
844
+ targetPath,
845
+ relativePath: file.path,
846
+ content: ensureTrailingNewline(content),
847
+ mode: file.mode,
848
+ dryRun,
849
+ operations,
850
+ action: "seed project-owned VCM file"
851
+ });
852
+ }
823
853
  async function removeLegacyFlatSkillFiles({ projectRoot, dryRun, operations }) {
824
854
  const wholeFilesByPath = new Map(WHOLE_FILES.map((file) => [file.path, file]));
825
855
  for (const legacy of LEGACY_FLAT_SKILL_FILES) {
@@ -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,