vibe-coding-master 0.6.23 → 0.7.1

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
@@ -390,11 +390,11 @@ VCM runs it from the active task worktree.
390
390
 
391
391
  ### Auto Memory
392
392
 
393
- Enable `Auto memory` in the `VCM Harness` sidebar group to collect durable role
394
- knowledge after a complete task passes Final Acceptance. Project Manager,
395
- Architect, Coder, Tester, and an enabled Gate Reviewer submit memory drafts in
396
- sequence. Harness Engineer verifies and consolidates them before VCM applies
397
- the result.
393
+ `Auto memory` is the switch for the entire automated memory workflow. During
394
+ Review Task Harness after Final Acceptance, Project Manager, Architect, Coder,
395
+ Tester, and an enabled Gate Reviewer submit proposals in sequence through
396
+ `vcm-propose-memory`. Harness Engineer verifies and consolidates them before VCM
397
+ applies the result. Roles cannot edit active memory directly.
398
398
 
399
399
  Canonical memory is stored under the base repository's `.ai/vcm/memory/`.
400
400
  Harness Studio shows current memory and task-local applied history. Memory is
@@ -405,14 +405,16 @@ Post-task processing is ordered by the backend:
405
405
 
406
406
  ```text
407
407
  Final Acceptance
408
- -> Auto Memory, when enabled
408
+ -> Review Task Harness
409
+ -> Memory proposals and Harness Engineer review, when Auto Memory is enabled
409
410
  -> Task Harness Retrospective
410
411
  ```
411
412
 
412
- Auto Memory adds no retrospective prerequisite when it is disabled. When it is
413
- enabled, both automatic and manual retrospective requests wait until memory has
414
- been applied for the current Final Acceptance. A failed memory review must be
415
- retried from Harness Studio before retrospective can run.
413
+ When Auto Memory is disabled, Review Task Harness does not collect proposals or
414
+ ask Harness Engineer to update memory. When enabled, both automatic and manual
415
+ review requests complete the memory phase before retrospective analysis. A
416
+ failed memory review must be retried from Harness Studio before retrospective
417
+ can continue.
416
418
 
417
419
  ## Closing a Task
418
420
 
@@ -154,15 +154,21 @@ export function registerHarnessRoutes(app, deps) {
154
154
  });
155
155
  app.post("/api/projects/harness/task-retrospective", async (request) => {
156
156
  const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
157
- await deps.autoMemoryService.assertTaskRetrospectiveReady({
157
+ const trigger = request.body?.trigger === "auto" ? "auto" : "manual";
158
+ const memoryInput = {
158
159
  baseRepoRoot: project.repoRoot,
159
160
  taskRepoRoot: task.worktreePath,
160
161
  taskSlug: task.taskSlug,
161
162
  handoffDir: task.handoffDir,
162
- roundReady: true
163
- });
163
+ roundReady: true,
164
+ requestTrigger: trigger
165
+ };
166
+ await deps.autoMemoryService.reconcileTask(memoryInput);
167
+ const memoryReadiness = await deps.autoMemoryService.getTaskRetrospectiveReadiness(memoryInput);
168
+ if (!memoryReadiness.ready) {
169
+ return deps.harnessFeedbackService.getState(project.repoRoot, task.taskSlug);
170
+ }
164
171
  await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
165
- const trigger = request.body?.trigger === "auto" ? "auto" : "manual";
166
172
  return deps.harnessFeedbackService.startTaskRetrospective(project.repoRoot, {
167
173
  taskSlug: task.taskSlug,
168
174
  taskRepoRoot: task.worktreePath,
@@ -1,4 +1,4 @@
1
- import { DISPATCHABLE_ROLES, VCM_ROLE_NAMES } from "../../shared/constants.js";
1
+ import { DISPATCHABLE_ROLES } from "../../shared/constants.js";
2
2
  import { isOpenFileLimitError, VcmError } from "../errors.js";
3
3
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
4
4
  export function registerTaskRoutes(app, deps) {
@@ -92,39 +92,9 @@ export function registerTaskRoutes(app, deps) {
92
92
  });
93
93
  app.post("/api/tasks/:taskSlug/cleanup", async (request) => {
94
94
  const project = await requireCurrentProject(deps.projectService);
95
- const task = await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
96
- await stopRunningRoleSessions(deps, project.repoRoot, request.params.taskSlug);
97
- await moveProjectToolSessionsToSafeCwd(deps, project.repoRoot);
98
- await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), request.params.taskSlug, { clearCache: true });
99
- deps.roundService.stopTask(request.params.taskSlug);
100
- return deps.taskService.cleanupTask(project.repoRoot, request.params.taskSlug, request.body ?? {});
95
+ return deps.taskCloseService.closeTask(project.repoRoot, request.params.taskSlug);
101
96
  });
102
97
  }
103
- async function stopRunningRoleSessions(deps, repoRoot, taskSlug) {
104
- const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
105
- for (const session of sessions) {
106
- if (session.status === "running" && VCM_ROLE_NAMES.some((role) => role === session.role)) {
107
- await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
108
- }
109
- }
110
- }
111
- async function moveProjectToolSessionsToSafeCwd(deps, repoRoot) {
112
- await Promise.all([
113
- ignoreMissingSession(deps.sessionService.moveProjectTranslatorSessionToSafeCwd(repoRoot)),
114
- ignoreMissingSession(deps.sessionService.moveProjectHarnessEngineerSessionToSafeCwd(repoRoot))
115
- ]);
116
- }
117
- async function ignoreMissingSession(operation) {
118
- try {
119
- await operation;
120
- }
121
- catch (error) {
122
- if (error instanceof VcmError && error.code === "SESSION_MISSING") {
123
- return;
124
- }
125
- throw error;
126
- }
127
- }
128
98
  async function requireCurrentProject(projectService) {
129
99
  const project = await projectService.getCurrentProject();
130
100
  if (!project) {
@@ -20,6 +20,7 @@ import { renderTesterHarnessRules } from "../templates/harness/tester-agent.js";
20
20
  import { renderVcmFinalAcceptanceSkillRules } from "../templates/harness/vcm-final-acceptance-skill.js";
21
21
  import { renderVcmHarnessBootstrapSkillRules } from "../templates/harness/vcm-harness-bootstrap-skill.js";
22
22
  import { renderVcmLongRunningValidationSkillRules } from "../templates/harness/vcm-long-running-validation-skill.js";
23
+ import { renderVcmProposeMemorySkillRules } from "../templates/harness/vcm-propose-memory-skill.js";
23
24
  import { renderVcmReportHarnessIssueSkillRules } from "../templates/harness/vcm-report-harness-issue-skill.js";
24
25
  import { renderVcmRouteMessageSkillRules } from "../templates/harness/vcm-route-message-skill.js";
25
26
  import { readVcmPackageVersion } from "../app-version.js";
@@ -36,7 +37,7 @@ const LEGACY_CODEX_HARNESS_PATHS = [
36
37
  ".ai/tools/request-codex-review"
37
38
  ];
38
39
  const VCM_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code" -H "content-type: application/json" --data-binary @- >/dev/null || true'`;
39
- const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
40
+ const VCM_STOP_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 1 --max-time 2 -X POST "\${VCM_API_URL}/api/hooks/claude-code/stop" -H "content-type: application/json" --data-binary @- || true'`;
40
41
  const VCM_PERMISSION_REQUEST_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ] || [ -z "\${VCM_API_URL:-}" ]; then exit 0; fi; node -e '"'"'let s="";process.stdin.setEncoding("utf8");process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{let event={};try{event=s.trim()?JSON.parse(s):{};}catch{event={raw:s};}process.stdout.write(JSON.stringify({taskSlug:process.env.VCM_TASK_SLUG,role:process.env.VCM_ROLE,event}));});'"'"' | curl -fsS --max-time 5 -X POST "\${VCM_API_URL}/api/hooks/claude-code/permission-request" -H "content-type: application/json" --data-binary @- || true'`;
41
42
  const VCM_BASH_GUARD_HOOK_COMMAND = `sh -c 'if [ -z "\${VCM_TASK_SLUG:-}" ] || [ -z "\${VCM_ROLE:-}" ]; then exit 0; fi; guard=""; repo="$(git rev-parse --show-toplevel 2>/dev/null || true)"; if [ -n "$repo" ] && [ -f "$repo/.ai/tools/vcm-bash-guard" ]; then guard="$repo/.ai/tools/vcm-bash-guard"; else cwd="$(pwd -P 2>/dev/null || pwd)"; dir="$cwd"; while [ -n "$dir" ] && [ "$dir" != "/" ]; do if [ -f "$dir/.ai/tools/vcm-bash-guard" ]; then guard="$dir/.ai/tools/vcm-bash-guard"; break; fi; dir="$(dirname "$dir")"; done; if [ -z "$guard" ] && [ -n "\${CLAUDE_PROJECT_DIR:-}" ] && [ -f "\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard" ]; then guard="\${CLAUDE_PROJECT_DIR}/.ai/tools/vcm-bash-guard"; fi; fi; [ -n "$guard" ] || exit 0; python3 "$guard" || exit 0'`;
42
43
  const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
@@ -248,6 +249,12 @@ const WHOLE_FILES = [
248
249
  mode: 0o644,
249
250
  content: renderSkillFile("VCM Report Harness Issue Skill", "vcm-report-harness-issue", "Use when a VCM role notices a reusable harness problem and needs to record feedback for Harness Engineer review.", renderVcmReportHarnessIssueSkillRules())
250
251
  },
252
+ {
253
+ path: ".claude/skills/vcm-propose-memory/SKILL.md",
254
+ category: "skill",
255
+ mode: 0o644,
256
+ content: renderSkillFile("VCM Propose Memory Skill", "vcm-propose-memory", "Use only when VCM requests a role memory proposal during Task Harness Review.", renderVcmProposeMemorySkillRules())
257
+ },
251
258
  {
252
259
  path: ".ai/tools/request-gate-review",
253
260
  category: "runtime-tool",
@@ -476,6 +483,7 @@ function fixedDirectories() {
476
483
  ".claude/skills/vcm-route-message/",
477
484
  ".claude/skills/vcm-gate-review/",
478
485
  ".claude/skills/vcm-report-harness-issue/",
486
+ ".claude/skills/vcm-propose-memory/",
479
487
  ".ai/vcm/translations/",
480
488
  ".ai/vcm/gate-reviews/",
481
489
  ".ai/tools/",
@@ -1,4 +1,3 @@
1
- import { VCM_ROLE_NAMES } from "../../shared/constants.js";
2
1
  import { VcmError } from "../errors.js";
3
2
  import { submitTerminalInput } from "../runtime/terminal-submit.js";
4
3
  import { getTaskRuntimeRepoRoot } from "../services/task-service.js";
@@ -605,15 +604,7 @@ export function createGatewayService(deps) {
605
604
  hint: settings.currentTaskSlug ? `Use /close-task confirm ${settings.currentTaskSlug}` : undefined
606
605
  });
607
606
  }
608
- const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
609
- await stopRunningRoleSessions(project.repoRoot, taskSlug);
610
- await moveProjectToolSessionsToSafeCwd(project.repoRoot);
611
- await deps.translationService.stopTask(getTaskRuntimeRepoRoot(task), taskSlug, { clearCache: true });
612
- deps.roundService.stopTask(taskSlug);
613
- const result = await deps.taskService.cleanupTask(project.repoRoot, taskSlug, {
614
- force: true,
615
- forceDeleteBranch: true
616
- });
607
+ const result = await deps.taskCloseService.closeTask(project.repoRoot, taskSlug);
617
608
  clearFailedTranslation(project.repoRoot, taskSlug);
618
609
  const latestPmReplies = { ...settings.latestPmReplies };
619
610
  delete latestPmReplies[latestPmReplyKey(project.repoRoot, taskSlug)];
@@ -629,8 +620,9 @@ export function createGatewayService(deps) {
629
620
  });
630
621
  const lines = [
631
622
  `Closed task: ${result.taskSlug}`,
632
- result.removedWorktreePath ? `removed worktree: ${result.removedWorktreePath}` : "removed worktree: none",
633
- result.deletedBranch ? `deleted branch: ${result.deletedBranch}` : "deleted branch: none",
623
+ `worktree removed: ${result.worktreeRemoved ? "yes" : "no"}`,
624
+ `branch deleted: ${result.branchDeleted ? "yes" : "no"}`,
625
+ `task state removed: ${result.stateRemoved ? "yes" : "no"}`,
634
626
  `removed state paths: ${result.removedStatePaths.length}`
635
627
  ];
636
628
  if (result.warnings?.length) {
@@ -638,31 +630,6 @@ export function createGatewayService(deps) {
638
630
  }
639
631
  return lines.join("\n");
640
632
  }
641
- async function stopRunningRoleSessions(repoRoot, taskSlug) {
642
- const sessions = await deps.sessionService.listRoleSessions(repoRoot, taskSlug);
643
- for (const session of sessions) {
644
- if (session.status === "running" && VCM_ROLE_NAMES.some((role) => role === session.role)) {
645
- await deps.sessionService.stopRoleSession(repoRoot, taskSlug, session.role);
646
- }
647
- }
648
- }
649
- async function moveProjectToolSessionsToSafeCwd(repoRoot) {
650
- await Promise.all([
651
- ignoreMissingSession(deps.sessionService.moveProjectTranslatorSessionToSafeCwd(repoRoot)),
652
- ignoreMissingSession(deps.sessionService.moveProjectHarnessEngineerSessionToSafeCwd(repoRoot))
653
- ]);
654
- }
655
- async function ignoreMissingSession(operation) {
656
- try {
657
- await operation;
658
- }
659
- catch (error) {
660
- if (error instanceof VcmError && error.code === "SESSION_MISSING") {
661
- return;
662
- }
663
- throw error;
664
- }
665
- }
666
633
  async function setGatewayTranslation(enabled) {
667
634
  const settings = await deps.settings.updateSettings({ translationEnabled: enabled });
668
635
  return `Gateway translation ${settings.translationEnabled ? "on" : "off"}.`;
@@ -35,9 +35,11 @@ import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-
35
35
  import { createRuntimeRecoveryService } from "./services/runtime-recovery-service.js";
36
36
  import { createStatusService } from "./services/status-service.js";
37
37
  import { createTaskService } from "./services/task-service.js";
38
+ import { createTaskCloseService } from "./services/task-close-service.js";
38
39
  import { createTaskLaunchService } from "./services/task-launch-service.js";
39
40
  import { createTerminalInterruptService } from "./services/terminal-interrupt-service.js";
40
41
  import { createTranslationService } from "./services/translation-service.js";
42
+ import { createTurnReconcilerService } from "./services/turn-reconciler-service.js";
41
43
  import { createDiagnosticsService } from "./services/diagnostics-service.js";
42
44
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
43
45
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
@@ -112,11 +114,10 @@ export async function createServer(deps, options = {}) {
112
114
  registerTaskRoutes(app, {
113
115
  projectService: deps.projectService,
114
116
  taskService: deps.taskService,
115
- sessionService: deps.sessionService,
117
+ taskCloseService: deps.taskCloseService,
116
118
  statusService: deps.statusService,
117
119
  messageService: deps.messageService,
118
120
  taskLaunchService: deps.taskLaunchService,
119
- translationService: deps.translationService,
120
121
  roundService: deps.roundService
121
122
  });
122
123
  registerSessionRoutes(app, {
@@ -154,9 +155,11 @@ export async function createServer(deps, options = {}) {
154
155
  });
155
156
  app.addHook("onReady", async () => {
156
157
  await cleanupRecentTranslationRuntime(deps);
158
+ deps.runtimeCoordinator.start();
157
159
  await deps.gatewayService.start();
158
160
  });
159
161
  app.addHook("onClose", async () => {
162
+ deps.runtimeCoordinator.stop();
160
163
  await deps.gatewayService.stop();
161
164
  });
162
165
  if (options.staticDir) {
@@ -306,6 +309,12 @@ export function createDefaultServerDeps(options = {}) {
306
309
  fs,
307
310
  auditPath: gatewaySettings.getAuditPath()
308
311
  });
312
+ const taskCloseService = createTaskCloseService({
313
+ taskService,
314
+ sessionService,
315
+ translationService,
316
+ roundService
317
+ });
309
318
  const gatewayService = createGatewayService({
310
319
  fs,
311
320
  settings: gatewaySettings,
@@ -313,6 +322,7 @@ export function createDefaultServerDeps(options = {}) {
313
322
  channels: gatewayChannels,
314
323
  projectService,
315
324
  taskService,
325
+ taskCloseService,
316
326
  sessionService,
317
327
  taskLaunchService,
318
328
  translationService,
@@ -320,20 +330,6 @@ export function createDefaultServerDeps(options = {}) {
320
330
  runtime,
321
331
  appSettings
322
332
  });
323
- const runtimeCoordinator = createRuntimeCoordinatorService({
324
- appSettings,
325
- taskService,
326
- sessionService,
327
- translationService,
328
- harnessService,
329
- harnessFeedbackService,
330
- autoMemoryService,
331
- roundService,
332
- gatewayService,
333
- async getStateRoot(repoRoot) {
334
- return (await projectService.loadConfig(repoRoot)).stateRoot;
335
- }
336
- });
337
333
  const runtimeRecoveryService = createRuntimeRecoveryService({
338
334
  fs,
339
335
  runtime,
@@ -357,6 +353,28 @@ export function createDefaultServerDeps(options = {}) {
357
353
  jobGuard: createJobGuardService(),
358
354
  translationWorkerService
359
355
  });
356
+ const turnReconciler = createTurnReconcilerService({
357
+ sessionService,
358
+ roundService,
359
+ claudeHookService,
360
+ runtime
361
+ });
362
+ const runtimeCoordinator = createRuntimeCoordinatorService({
363
+ appSettings,
364
+ projectService,
365
+ taskService,
366
+ sessionService,
367
+ translationService,
368
+ harnessService,
369
+ harnessFeedbackService,
370
+ autoMemoryService,
371
+ roundService,
372
+ gatewayService,
373
+ turnReconciler,
374
+ async getStateRoot(repoRoot) {
375
+ return (await projectService.loadConfig(repoRoot)).stateRoot;
376
+ }
377
+ });
360
378
  const terminalInterruptService = createTerminalInterruptService({
361
379
  runtime,
362
380
  projectService,
@@ -374,6 +392,7 @@ export function createDefaultServerDeps(options = {}) {
374
392
  appSettings,
375
393
  projectService,
376
394
  taskService,
395
+ taskCloseService,
377
396
  sessionService,
378
397
  artifactService,
379
398
  harnessService,
@@ -62,11 +62,16 @@ export function createAutoMemoryService(deps) {
62
62
  }
63
63
  async function getState(baseRepoRoot, taskRepoRoot) {
64
64
  await ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
65
- const [active, runs, files] = await Promise.all([
65
+ let [active, runs, files] = await Promise.all([
66
66
  loadActiveState(taskRepoRoot),
67
67
  listRuns(taskRepoRoot),
68
68
  listMemoryFiles(taskRepoRoot)
69
69
  ]);
70
+ if (active && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
71
+ await discardActiveReview(taskRepoRoot, active);
72
+ active = undefined;
73
+ runs = await listRuns(taskRepoRoot);
74
+ }
70
75
  return {
71
76
  version: 1,
72
77
  status: active?.status ?? "idle",
@@ -79,6 +84,13 @@ export function createAutoMemoryService(deps) {
79
84
  async function reconcileTask(input) {
80
85
  await ensureTaskMemorySnapshot(deps.fs, input.baseRepoRoot, input.taskRepoRoot);
81
86
  const active = await loadActiveState(input.taskRepoRoot);
87
+ const preferences = await deps.appSettings.getPreferences();
88
+ if (!preferences.autoMemoryEnabled) {
89
+ if (active) {
90
+ await discardActiveReview(input.taskRepoRoot, active);
91
+ }
92
+ return getState(input.baseRepoRoot, input.taskRepoRoot);
93
+ }
82
94
  if (active?.status === "collecting") {
83
95
  await dispatchCurrentDraft(input.baseRepoRoot, input.taskRepoRoot, active);
84
96
  return getState(input.baseRepoRoot, input.taskRepoRoot);
@@ -87,11 +99,7 @@ export function createAutoMemoryService(deps) {
87
99
  await dispatchHarnessReview(input.baseRepoRoot, input.taskRepoRoot, active);
88
100
  return getState(input.baseRepoRoot, input.taskRepoRoot);
89
101
  }
90
- if (active || !input.roundReady) {
91
- return getState(input.baseRepoRoot, input.taskRepoRoot);
92
- }
93
- const preferences = await deps.appSettings.getPreferences();
94
- if (!preferences.autoMemoryEnabled) {
102
+ if (active || !input.roundReady || !input.requestTrigger) {
95
103
  return getState(input.baseRepoRoot, input.taskRepoRoot);
96
104
  }
97
105
  if (deps.isHarnessEngineerAvailable && !(await deps.isHarnessEngineerAvailable(input.baseRepoRoot))) {
@@ -122,6 +130,7 @@ export function createAutoMemoryService(deps) {
122
130
  taskSlug: input.taskSlug,
123
131
  status: "collecting",
124
132
  finalAcceptanceHash,
133
+ trigger: input.requestTrigger,
125
134
  createdAt: timestamp,
126
135
  updatedAt: timestamp,
127
136
  drafts
@@ -138,6 +147,7 @@ export function createAutoMemoryService(deps) {
138
147
  createdAt: timestamp,
139
148
  updatedAt: timestamp,
140
149
  finalAcceptanceHash,
150
+ trigger: input.requestTrigger,
141
151
  beforeHashes: hashMemorySet(before)
142
152
  });
143
153
  await persistActiveState(input.taskRepoRoot, state);
@@ -159,13 +169,15 @@ export function createAutoMemoryService(deps) {
159
169
  return {
160
170
  ready: false,
161
171
  disposition,
172
+ trigger: active.trigger,
162
173
  reason: disposition === "failed"
163
174
  ? "Auto Memory failed for this task. Retry it before Task Harness Retrospective."
164
175
  : `Auto Memory is ${disposition} for this task.`
165
176
  };
166
177
  }
167
- if (await hasCompletedRunForFinalAcceptance(input.taskRepoRoot, finalAcceptanceHash)) {
168
- return { ready: true, disposition: "completed" };
178
+ const completedRun = await findCompletedRunForFinalAcceptance(input.taskRepoRoot, finalAcceptanceHash);
179
+ if (completedRun) {
180
+ return { ready: true, disposition: "completed", trigger: completedRun.trigger };
169
181
  }
170
182
  return {
171
183
  ready: false,
@@ -173,18 +185,6 @@ export function createAutoMemoryService(deps) {
173
185
  reason: "Auto Memory must complete for this Final Acceptance before Task Harness Retrospective."
174
186
  };
175
187
  }
176
- async function assertTaskRetrospectiveReady(input) {
177
- const readiness = await getTaskRetrospectiveReadiness(input);
178
- if (readiness.ready) {
179
- return;
180
- }
181
- throw new VcmError({
182
- code: "TASK_MEMORY_REVIEW_NOT_READY",
183
- message: "Task Harness Retrospective must run after Auto Memory.",
184
- statusCode: 409,
185
- hint: readiness.reason
186
- });
187
- }
188
188
  async function isRoleMemoryTurn(taskRepoRoot, role) {
189
189
  const state = await loadActiveState(taskRepoRoot);
190
190
  const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
@@ -192,6 +192,13 @@ export function createAutoMemoryService(deps) {
192
192
  }
193
193
  async function handleRoleHook(input) {
194
194
  const state = await loadActiveState(input.taskRepoRoot);
195
+ if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
196
+ if (state) {
197
+ await discardActiveReview(input.taskRepoRoot, state);
198
+ return true;
199
+ }
200
+ return false;
201
+ }
195
202
  const draft = state?.status === "collecting" ? currentDraft(state) : undefined;
196
203
  if (!state || !draft || draft.role !== input.role) {
197
204
  return false;
@@ -237,13 +244,17 @@ export function createAutoMemoryService(deps) {
237
244
  }
238
245
  async function handleHarnessEngineerHook(input) {
239
246
  const state = await loadActiveState(input.taskRepoRoot);
247
+ if (!(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
248
+ if (state) {
249
+ await discardActiveReview(input.taskRepoRoot, state);
250
+ return true;
251
+ }
252
+ return false;
253
+ }
240
254
  if (state?.status === "reviewing" && !state.reviewPromptDispatchedAt) {
241
255
  return false;
242
256
  }
243
257
  if (!state || state.status !== "reviewing") {
244
- if (input.eventName === "Stop") {
245
- await captureHarnessEngineerChanges(input.baseRepoRoot, input.taskRepoRoot, input.taskSlug);
246
- }
247
258
  return false;
248
259
  }
249
260
  if (input.eventName === "UserPromptSubmit" || input.eventName === "PostCompact") {
@@ -329,6 +340,10 @@ export function createAutoMemoryService(deps) {
329
340
  }
330
341
  async function assertHarnessEngineerAvailable(taskRepoRoot) {
331
342
  const state = await loadActiveState(taskRepoRoot);
343
+ if (state && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
344
+ await discardActiveReview(taskRepoRoot, state);
345
+ return;
346
+ }
332
347
  if (!state || state.status === "failed") {
333
348
  return;
334
349
  }
@@ -341,6 +356,10 @@ export function createAutoMemoryService(deps) {
341
356
  }
342
357
  async function assertNoActiveReview(taskRepoRoot) {
343
358
  const state = await loadActiveState(taskRepoRoot);
359
+ if (state && !(await deps.appSettings.getPreferences()).autoMemoryEnabled) {
360
+ await discardActiveReview(taskRepoRoot, state);
361
+ return;
362
+ }
344
363
  if (!state) {
345
364
  return;
346
365
  }
@@ -437,17 +456,6 @@ export function createAutoMemoryService(deps) {
437
456
  await failReview(taskRepoRoot, state, `Harness Engineer memory result could not be applied: ${errorMessage(error)}`);
438
457
  }
439
458
  }
440
- async function captureHarnessEngineerChanges(baseRepoRoot, taskRepoRoot, taskSlug) {
441
- await ensureTaskMemorySnapshot(deps.fs, baseRepoRoot, taskRepoRoot);
442
- const [before, after] = await Promise.all([
443
- readMemorySet(baseRepoRoot),
444
- readMemorySet(taskRepoRoot)
445
- ]);
446
- if (sameHashes(hashMemorySet(before), hashMemorySet(after))) {
447
- return;
448
- }
449
- await createAppliedRun(baseRepoRoot, taskRepoRoot, taskSlug, "harness-engineer", before, after);
450
- }
451
459
  async function createAppliedRun(baseRepoRoot, taskRepoRoot, taskSlug, source, before, after) {
452
460
  assertCompleteMemorySet(after);
453
461
  const timestamp = now();
@@ -513,6 +521,7 @@ export function createAutoMemoryService(deps) {
513
521
  failedAt: run.failedAt,
514
522
  revertedAt: run.revertedAt,
515
523
  finalAcceptanceHash: run.finalAcceptanceHash,
524
+ trigger: run.trigger,
516
525
  diff: run.diff ?? "",
517
526
  canRevert: run.status === "applied" && !run.revertedAt && Boolean(run.afterHashes),
518
527
  error: run.error
@@ -534,8 +543,11 @@ export function createAutoMemoryService(deps) {
534
543
  return files;
535
544
  }
536
545
  async function hasCompletedRunForFinalAcceptance(taskRepoRoot, finalAcceptanceHash) {
546
+ return Boolean(await findCompletedRunForFinalAcceptance(taskRepoRoot, finalAcceptanceHash));
547
+ }
548
+ async function findCompletedRunForFinalAcceptance(taskRepoRoot, finalAcceptanceHash) {
537
549
  const runs = await listRuns(taskRepoRoot);
538
- return runs.some((run) => run.finalAcceptanceHash === finalAcceptanceHash && (run.status === "applied" || run.status === "reverted"));
550
+ return runs.find((run) => run.finalAcceptanceHash === finalAcceptanceHash && (run.status === "applied" || run.status === "reverted"));
539
551
  }
540
552
  async function loadActiveState(taskRepoRoot) {
541
553
  const statePath = resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_STATE_PATH);
@@ -551,6 +563,10 @@ export function createAutoMemoryService(deps) {
551
563
  async function clearActiveState(taskRepoRoot) {
552
564
  await deps.fs.removePath?.(resolveRepoPath(taskRepoRoot, MEMORY_REVIEW_STATE_PATH), { force: true });
553
565
  }
566
+ async function discardActiveReview(taskRepoRoot, state) {
567
+ await clearActiveState(taskRepoRoot);
568
+ await deps.fs.removePath?.(resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`), { recursive: true, force: true });
569
+ }
554
570
  async function readRun(taskRepoRoot, runId) {
555
571
  const runPath = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${requireSafeRunId(runId)}/run.json`);
556
572
  if (!(await deps.fs.pathExists(runPath))) {
@@ -590,7 +606,6 @@ export function createAutoMemoryService(deps) {
590
606
  reconcileTask,
591
607
  getState,
592
608
  getTaskRetrospectiveReadiness,
593
- assertTaskRetrospectiveReady,
594
609
  getFile,
595
610
  updateFile,
596
611
  revertRun,
@@ -629,44 +644,36 @@ function toActiveReview(state) {
629
644
  updatedAt: state.updatedAt,
630
645
  currentRole: state.status === "collecting" ? currentDraft(state)?.role : undefined,
631
646
  drafts: state.drafts,
647
+ trigger: state.trigger,
632
648
  error: state.error
633
649
  };
634
650
  }
635
651
  function buildRoleDraftPrompt(taskRepoRoot, state, draft) {
636
652
  return [
637
- "[VCM Auto Memory Draft]",
653
+ "[VCM Task Harness Review: Memory Proposal]",
638
654
  "",
639
- "The task passed Final Acceptance. Propose only durable, reusable memory supported by this task's evidence.",
655
+ "Use the vcm-propose-memory skill to submit the assigned proposal.",
640
656
  `Task worktree: ${taskRepoRoot}`,
641
657
  `Current shared memory: ${resolveRepoPath(taskRepoRoot, `${MEMORY_ROOT}/shared.md`)}`,
642
658
  `Current role memory: ${resolveRepoPath(taskRepoRoot, `${MEMORY_ROOT}/roles/${draft.role}.md`)}`,
643
659
  `Write the draft to: ${resolveRepoPath(taskRepoRoot, draft.path)}`,
644
660
  "",
645
- "Use this structure:",
646
- "# Memory Draft",
647
- "Decision: update | no-change",
648
- "## Add",
649
- "## Update",
650
- "## Remove",
651
- "## Evidence",
652
- "",
653
- "Do not edit memory files, route messages, or handoff artifacts. Do not record task narrative, temporary state, unverified conclusions, or rules that belong in the harness.",
654
661
  "End the turn after writing the draft."
655
662
  ].join("\n");
656
663
  }
657
664
  function buildHarnessReviewPrompt(taskRepoRoot, state) {
658
665
  const runRoot = resolveRepoPath(taskRepoRoot, `${MEMORY_REVIEW_RUNS_ROOT}/${state.runId}`);
659
666
  return [
660
- "[VCM Auto Memory Review]",
667
+ "[VCM Task Harness Review: Memory Review]",
661
668
  "",
662
- "Review the role drafts and task evidence, then produce the complete next memory set.",
669
+ "Auto Memory is enabled. Review the role proposals and task evidence, then produce the complete next memory set.",
663
670
  `Task worktree: ${taskRepoRoot}`,
664
671
  `Role drafts: ${path.join(runRoot, "drafts")}`,
665
672
  `Current memory snapshot: ${path.join(runRoot, "before")}`,
666
673
  `Write the complete reviewed memory set to: ${path.join(runRoot, "after")}`,
667
674
  "",
668
675
  "Keep only verified, durable, reusable project knowledge. Merge duplicates, remove stale entries, and keep role-specific knowledge in the matching role file.",
669
- "Memory is context, not authority. Keep harness rules and task-specific narrative out of memory.",
676
+ "Do not record task narrative, temporary state, unverified conclusions, or Harness rules.",
670
677
  "Do not edit product code, harness files, the canonical base-repository memory, or review metadata.",
671
678
  "All existing files already exist in the after directory. Edit those files in place and end the turn when review is complete."
672
679
  ].join("\n");