vibe-coding-master 0.6.22 → 0.7.0

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 (33) hide show
  1. package/README.md +37 -3
  2. package/dist/backend/adapters/filesystem.js +8 -0
  3. package/dist/backend/api/harness-routes.js +54 -0
  4. package/dist/backend/api/runtime-state-routes.js +6 -2
  5. package/dist/backend/api/task-routes.js +2 -32
  6. package/dist/backend/cli/install-vcm-harness.js +1 -1
  7. package/dist/backend/gateway/gateway-service.js +4 -37
  8. package/dist/backend/server.js +50 -15
  9. package/dist/backend/services/app-settings-service.js +1 -0
  10. package/dist/backend/services/auto-memory-service.js +760 -0
  11. package/dist/backend/services/claude-hook-service.js +108 -2
  12. package/dist/backend/services/claude-transcript-reply.js +81 -1
  13. package/dist/backend/services/harness-service.js +1 -1
  14. package/dist/backend/services/runtime-coordinator-service.js +69 -1
  15. package/dist/backend/services/runtime-recovery-service.js +6 -0
  16. package/dist/backend/services/session-service.js +3 -0
  17. package/dist/backend/services/task-close-service.js +88 -0
  18. package/dist/backend/services/task-service.js +152 -35
  19. package/dist/backend/services/turn-reconciler-service.js +122 -0
  20. package/dist/backend/templates/harness/architect-agent.js +3 -0
  21. package/dist/backend/templates/harness/claude-root.js +3 -1
  22. package/dist/backend/templates/harness/coder-agent.js +3 -0
  23. package/dist/backend/templates/harness/gate-review.js +3 -0
  24. package/dist/backend/templates/harness/harness-engineer-agent.js +32 -8
  25. package/dist/backend/templates/harness/project-manager-agent.js +3 -0
  26. package/dist/backend/templates/harness/role-memory.js +9 -0
  27. package/dist/backend/templates/harness/tester-agent.js +3 -0
  28. package/dist/shared/types/memory.js +8 -0
  29. package/dist-frontend/assets/{index-DmSHDyiQ.css → index-C2QzumXk.css} +1 -1
  30. package/dist-frontend/assets/index-e8Tqa8Qh.js +97 -0
  31. package/dist-frontend/index.html +2 -2
  32. package/package.json +1 -1
  33. package/dist-frontend/assets/index-DYBg_qYS.js +0 -96
package/README.md CHANGED
@@ -333,7 +333,7 @@ Gateway can:
333
333
  - create a task
334
334
  - close a task with confirmation
335
335
  - send plain text to Project Manager
336
- - push PM replies back to the active chat
336
+ - push the PM Round-final reply and Round status back to the active chat
337
337
  - translate mobile messages when Gateway translation is enabled
338
338
 
339
339
  Gateway does not expose the embedded terminal and does not send directly to
@@ -361,6 +361,12 @@ Common commands:
361
361
  Gateway credentials and audit logs are stored in app-local state, not connected
362
362
  repositories.
363
363
 
364
+ When Gateway starts, VCM enables conversation translation, auto-send, and the
365
+ `Round final reply` scope. On a normal Round end, Gateway sends the PM original
366
+ reply first, then reuses the matching translation already produced for the
367
+ translation panel. `/retry` explicitly creates a new translation only when the
368
+ previous Gateway translation failed or was unavailable.
369
+
364
370
  ## Harness Studio
365
371
 
366
372
  Harness Studio is the UI for VCM harness maintenance.
@@ -371,8 +377,10 @@ Use it to:
371
377
  - run bootstrap
372
378
  - open Harness Engineer
373
379
  - review harness files
380
+ - view and edit shared or role-specific VCM memory
381
+ - review and revert memory changes recorded in the active task worktree
374
382
  - copy file paths for discussion
375
- - review task harness after a task completes
383
+ - review task harness after post-task memory processing completes
376
384
  - inspect commit diffs for harness changes
377
385
  - merge task harness commits back to the connected repository branch when
378
386
  appropriate
@@ -380,6 +388,32 @@ Use it to:
380
388
  Harness Engineer is project-scoped and resumable. When it performs task work,
381
389
  VCM runs it from the active task worktree.
382
390
 
391
+ ### Auto Memory
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.
398
+
399
+ Canonical memory is stored under the base repository's `.ai/vcm/memory/`.
400
+ Harness Studio shows current memory and task-local applied history. Memory is
401
+ applied before user review; while the task worktree remains available, the user
402
+ can edit current memory or revert a recorded change.
403
+
404
+ Post-task processing is ordered by the backend:
405
+
406
+ ```text
407
+ Final Acceptance
408
+ -> Auto Memory, when enabled
409
+ -> Task Harness Retrospective
410
+ ```
411
+
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.
416
+
383
417
  ## Closing a Task
384
418
 
385
419
  `Close Task` is destructive.
@@ -480,5 +514,5 @@ npm start
480
514
  - `docs/ARCHITECTURE.md`: repository architecture
481
515
  - `docs/TESTING.md`: validation strategy
482
516
  - `docs/vcm-cc-best-practices.md`: current VCM Claude Code harness practice
483
- - `docs/v0.5-custom-workflow-plan.md`: future custom workflow plan
517
+ - `docs/v0.5-custom-workflow-plan.md`: deferred custom workflow proposal
484
518
  - `docs/cc-best-practices.md`: archived generic Claude Code harness notes
@@ -47,6 +47,14 @@ export function createNodeFileSystemAdapter() {
47
47
  await fs.writeFile(targetPath, content, "utf8");
48
48
  });
49
49
  },
50
+ async writeTextAtomic(targetPath, content) {
51
+ await runFileOperation(async () => {
52
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
53
+ const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
54
+ await fs.writeFile(tempPath, content, "utf8");
55
+ await fs.rename(tempPath, targetPath);
56
+ });
57
+ },
50
58
  async appendText(targetPath, content) {
51
59
  await runFileOperation(async () => {
52
60
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
@@ -66,11 +66,13 @@ export function registerHarnessRoutes(app, deps) {
66
66
  });
67
67
  app.post("/api/projects/harness/bootstrap/start", async (request) => {
68
68
  const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
69
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
69
70
  await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
70
71
  return deps.harnessService.startHarnessBootstrap(project.repoRoot, task.worktreePath, request.body ?? {});
71
72
  });
72
73
  app.post("/api/projects/harness/bootstrap/restart", async (request) => {
73
74
  const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
75
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
74
76
  await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
75
77
  return deps.harnessService.restartHarnessBootstrap(project.repoRoot, task.worktreePath, request.body ?? {});
76
78
  });
@@ -80,6 +82,7 @@ export function registerHarnessRoutes(app, deps) {
80
82
  });
81
83
  app.post("/api/projects/harness/bootstrap/run", async (request) => {
82
84
  const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
85
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
83
86
  await deps.harnessFeedbackService.assertHarnessEngineerAvailable(project.repoRoot);
84
87
  return deps.harnessService.runHarnessBootstrap(project.repoRoot, task.worktreePath);
85
88
  });
@@ -93,14 +96,26 @@ export function registerHarnessRoutes(app, deps) {
93
96
  });
94
97
  app.post("/api/projects/harness/engineer/session/start", async (request) => {
95
98
  const project = await requireCurrentProject(deps.projectService);
99
+ if (request.body?.taskSlug) {
100
+ const task = await deps.taskService.loadTask(project.repoRoot, request.body.taskSlug);
101
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
102
+ }
96
103
  return deps.sessionService.startProjectHarnessEngineerSession(project.repoRoot, request.body);
97
104
  });
98
105
  app.post("/api/projects/harness/engineer/session/resume", async (request) => {
99
106
  const project = await requireCurrentProject(deps.projectService);
107
+ if (request.body?.taskSlug) {
108
+ const task = await deps.taskService.loadTask(project.repoRoot, request.body.taskSlug);
109
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
110
+ }
100
111
  return deps.sessionService.resumeProjectHarnessEngineerSession(project.repoRoot, request.body);
101
112
  });
102
113
  app.post("/api/projects/harness/engineer/session/restart", async (request) => {
103
114
  const project = await requireCurrentProject(deps.projectService);
115
+ if (request.body?.taskSlug) {
116
+ const task = await deps.taskService.loadTask(project.repoRoot, request.body.taskSlug);
117
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
118
+ }
104
119
  return deps.sessionService.restartProjectHarnessEngineerSession(project.repoRoot, request.body);
105
120
  });
106
121
  app.post("/api/projects/harness/engineer/session/stop", async () => {
@@ -127,6 +142,10 @@ export function registerHarnessRoutes(app, deps) {
127
142
  });
128
143
  }
129
144
  const taskSlug = await normalizeOptionalTaskSlug(deps, project.repoRoot, request.body?.taskSlug);
145
+ if (taskSlug && (action === "approve" || action === "comment")) {
146
+ const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
147
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
148
+ }
130
149
  return deps.harnessFeedbackService.decide(project.repoRoot, {
131
150
  action,
132
151
  taskSlug,
@@ -135,6 +154,14 @@ export function registerHarnessRoutes(app, deps) {
135
154
  });
136
155
  app.post("/api/projects/harness/task-retrospective", async (request) => {
137
156
  const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
157
+ await deps.autoMemoryService.assertTaskRetrospectiveReady({
158
+ baseRepoRoot: project.repoRoot,
159
+ taskRepoRoot: task.worktreePath,
160
+ taskSlug: task.taskSlug,
161
+ handoffDir: task.handoffDir,
162
+ roundReady: true
163
+ });
164
+ await deps.autoMemoryService.assertHarnessEngineerAvailable(task.worktreePath);
138
165
  const trigger = request.body?.trigger === "auto" ? "auto" : "manual";
139
166
  return deps.harnessFeedbackService.startTaskRetrospective(project.repoRoot, {
140
167
  taskSlug: task.taskSlug,
@@ -143,6 +170,33 @@ export function registerHarnessRoutes(app, deps) {
143
170
  trigger
144
171
  });
145
172
  });
173
+ app.get("/api/projects/harness/memory", async (request) => {
174
+ const { project, task } = await requireHarnessTaskContext(deps, request.query.taskSlug);
175
+ return deps.autoMemoryService.getState(project.repoRoot, task.worktreePath);
176
+ });
177
+ app.get("/api/projects/harness/memory/file", async (request) => {
178
+ const { project, task } = await requireHarnessTaskContext(deps, request.query.taskSlug);
179
+ return deps.autoMemoryService.getFile(project.repoRoot, task.worktreePath, request.query.path ?? "");
180
+ });
181
+ app.put("/api/projects/harness/memory/file", async (request) => {
182
+ const { project, task } = await requireHarnessTaskContext(deps, request.query.taskSlug ?? request.body?.taskSlug);
183
+ if (typeof request.body?.content !== "string") {
184
+ throw new VcmError({
185
+ code: "MEMORY_FILE_CONTENT_INVALID",
186
+ message: "Memory file content must be a string.",
187
+ statusCode: 400
188
+ });
189
+ }
190
+ return deps.autoMemoryService.updateFile(project.repoRoot, task.worktreePath, task.taskSlug, request.query.path ?? "", request.body.content);
191
+ });
192
+ app.post("/api/projects/harness/memory/revert", async (request) => {
193
+ const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
194
+ return deps.autoMemoryService.revertRun(project.repoRoot, task.worktreePath, request.body?.runId ?? "");
195
+ });
196
+ app.post("/api/projects/harness/memory/retry", async (request) => {
197
+ const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
198
+ return deps.autoMemoryService.retryFailedReview(project.repoRoot, task.worktreePath);
199
+ });
146
200
  }
147
201
  function degradedHarnessStatus(error) {
148
202
  return {
@@ -18,14 +18,16 @@ export function registerRuntimeStateRoutes(app, deps) {
18
18
  harnessStatus: null,
19
19
  harnessBootstrapStatus: null,
20
20
  harnessFeedbackState,
21
+ autoMemoryState: null,
21
22
  gatewayStatus: coordinated.gatewayStatus
22
23
  };
23
24
  }
24
25
  try {
25
26
  const task = await deps.taskService.loadTask(project.repoRoot, taskSlug);
26
- const [harnessStatus, harnessBootstrapStatus] = await Promise.all([
27
+ const [harnessStatus, harnessBootstrapStatus, autoMemoryState] = await Promise.all([
27
28
  withOpenFileLimitFallback(() => deps.harnessService.getHarnessStatus(task.worktreePath), (error) => degradedHarnessStatus(error)),
28
- withOpenFileLimitFallback(() => deps.harnessService.getBootstrapStatus(project.repoRoot, task.worktreePath), (error) => degradedBootstrapStatus(error))
29
+ withOpenFileLimitFallback(() => deps.harnessService.getBootstrapStatus(project.repoRoot, task.worktreePath), (error) => degradedBootstrapStatus(error)),
30
+ deps.autoMemoryService.getState(project.repoRoot, task.worktreePath)
29
31
  ]);
30
32
  return {
31
33
  translatorSession,
@@ -34,6 +36,7 @@ export function registerRuntimeStateRoutes(app, deps) {
34
36
  harnessStatus,
35
37
  harnessBootstrapStatus,
36
38
  harnessFeedbackState,
39
+ autoMemoryState,
37
40
  gatewayStatus: coordinated.gatewayStatus
38
41
  };
39
42
  }
@@ -46,6 +49,7 @@ export function registerRuntimeStateRoutes(app, deps) {
46
49
  harnessStatus: degradedHarnessStatus(error),
47
50
  harnessBootstrapStatus: degradedBootstrapStatus(error),
48
51
  harnessFeedbackState,
52
+ autoMemoryState: null,
49
53
  gatewayStatus: coordinated.gatewayStatus
50
54
  };
51
55
  }
@@ -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) {
@@ -36,7 +36,7 @@ const LEGACY_CODEX_HARNESS_PATHS = [
36
36
  ".ai/tools/request-codex-review"
37
37
  ];
38
38
  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'`;
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 --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
40
  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
41
  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
42
  const VCM_BASH_DEFAULT_TIMEOUT_MS = "600000";
@@ -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"}.`;
@@ -9,6 +9,7 @@ import { createCommandDispatcher } from "./services/command-dispatcher.js";
9
9
  import { createClaudeHookService } from "./services/claude-hook-service.js";
10
10
  import { createGitAdapter } from "./adapters/git-adapter.js";
11
11
  import { createAppSettingsService } from "./services/app-settings-service.js";
12
+ import { createAutoMemoryService } from "./services/auto-memory-service.js";
12
13
  import { createClaudeTranscriptService } from "./services/claude-transcript-service.js";
13
14
  import { createGateReviewService } from "./services/gate-review-service.js";
14
15
  import { createHarnessFeedbackService } from "./services/harness-feedback-service.js";
@@ -34,9 +35,11 @@ import { createRuntimeCoordinatorService } from "./services/runtime-coordinator-
34
35
  import { createRuntimeRecoveryService } from "./services/runtime-recovery-service.js";
35
36
  import { createStatusService } from "./services/status-service.js";
36
37
  import { createTaskService } from "./services/task-service.js";
38
+ import { createTaskCloseService } from "./services/task-close-service.js";
37
39
  import { createTaskLaunchService } from "./services/task-launch-service.js";
38
40
  import { createTerminalInterruptService } from "./services/terminal-interrupt-service.js";
39
41
  import { createTranslationService } from "./services/translation-service.js";
42
+ import { createTurnReconcilerService } from "./services/turn-reconciler-service.js";
40
43
  import { createDiagnosticsService } from "./services/diagnostics-service.js";
41
44
  import { registerAppSettingsRoutes } from "./api/app-settings-routes.js";
42
45
  import { registerArtifactRoutes } from "./api/artifact-routes.js";
@@ -94,6 +97,7 @@ export async function createServer(deps, options = {}) {
94
97
  projectService: deps.projectService,
95
98
  harnessService: deps.harnessService,
96
99
  harnessFeedbackService: deps.harnessFeedbackService,
100
+ autoMemoryService: deps.autoMemoryService,
97
101
  sessionService: deps.sessionService,
98
102
  taskService: deps.taskService
99
103
  });
@@ -104,16 +108,16 @@ export async function createServer(deps, options = {}) {
104
108
  translationWorkerService: deps.translationWorkerService,
105
109
  harnessService: deps.harnessService,
106
110
  harnessFeedbackService: deps.harnessFeedbackService,
111
+ autoMemoryService: deps.autoMemoryService,
107
112
  runtimeCoordinator: deps.runtimeCoordinator
108
113
  });
109
114
  registerTaskRoutes(app, {
110
115
  projectService: deps.projectService,
111
116
  taskService: deps.taskService,
112
- sessionService: deps.sessionService,
117
+ taskCloseService: deps.taskCloseService,
113
118
  statusService: deps.statusService,
114
119
  messageService: deps.messageService,
115
120
  taskLaunchService: deps.taskLaunchService,
116
- translationService: deps.translationService,
117
121
  roundService: deps.roundService
118
122
  });
119
123
  registerSessionRoutes(app, {
@@ -151,9 +155,11 @@ export async function createServer(deps, options = {}) {
151
155
  });
152
156
  app.addHook("onReady", async () => {
153
157
  await cleanupRecentTranslationRuntime(deps);
158
+ deps.runtimeCoordinator.start();
154
159
  await deps.gatewayService.start();
155
160
  });
156
161
  app.addHook("onClose", async () => {
162
+ deps.runtimeCoordinator.stop();
157
163
  await deps.gatewayService.stop();
158
164
  });
159
165
  if (options.staticDir) {
@@ -222,6 +228,16 @@ export function createDefaultServerDeps(options = {}) {
222
228
  runtime,
223
229
  sessionService
224
230
  });
231
+ const autoMemoryService = createAutoMemoryService({
232
+ fs,
233
+ runtime,
234
+ sessionService,
235
+ appSettings,
236
+ async isHarnessEngineerAvailable(repoRoot) {
237
+ const state = await harnessFeedbackService.getState(repoRoot);
238
+ return state.status === "idle";
239
+ }
240
+ });
225
241
  const commandDispatcher = createCommandDispatcher({
226
242
  runtime,
227
243
  sessionService,
@@ -293,6 +309,12 @@ export function createDefaultServerDeps(options = {}) {
293
309
  fs,
294
310
  auditPath: gatewaySettings.getAuditPath()
295
311
  });
312
+ const taskCloseService = createTaskCloseService({
313
+ taskService,
314
+ sessionService,
315
+ translationService,
316
+ roundService
317
+ });
296
318
  const gatewayService = createGatewayService({
297
319
  fs,
298
320
  settings: gatewaySettings,
@@ -300,6 +322,7 @@ export function createDefaultServerDeps(options = {}) {
300
322
  channels: gatewayChannels,
301
323
  projectService,
302
324
  taskService,
325
+ taskCloseService,
303
326
  sessionService,
304
327
  taskLaunchService,
305
328
  translationService,
@@ -307,19 +330,6 @@ export function createDefaultServerDeps(options = {}) {
307
330
  runtime,
308
331
  appSettings
309
332
  });
310
- const runtimeCoordinator = createRuntimeCoordinatorService({
311
- appSettings,
312
- taskService,
313
- sessionService,
314
- translationService,
315
- harnessService,
316
- harnessFeedbackService,
317
- roundService,
318
- gatewayService,
319
- async getStateRoot(repoRoot) {
320
- return (await projectService.loadConfig(repoRoot)).stateRoot;
321
- }
322
- });
323
333
  const runtimeRecoveryService = createRuntimeRecoveryService({
324
334
  fs,
325
335
  runtime,
@@ -338,10 +348,33 @@ export function createDefaultServerDeps(options = {}) {
338
348
  runtime,
339
349
  harnessService,
340
350
  harnessFeedbackService,
351
+ autoMemoryService,
341
352
  gatewayService,
342
353
  jobGuard: createJobGuardService(),
343
354
  translationWorkerService
344
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
+ });
345
378
  const terminalInterruptService = createTerminalInterruptService({
346
379
  runtime,
347
380
  projectService,
@@ -359,10 +392,12 @@ export function createDefaultServerDeps(options = {}) {
359
392
  appSettings,
360
393
  projectService,
361
394
  taskService,
395
+ taskCloseService,
362
396
  sessionService,
363
397
  artifactService,
364
398
  harnessService,
365
399
  harnessFeedbackService,
400
+ autoMemoryService,
366
401
  commandDispatcher,
367
402
  claudeHookService,
368
403
  messageService,
@@ -242,6 +242,7 @@ function normalizePreferences(input) {
242
242
  roleRetryEnabled: candidate.roleRetryEnabled !== false,
243
243
  permissionRequestMode: normalizePermissionRequestMode(candidate.permissionRequestMode),
244
244
  autoTaskHarnessReviewEnabled: candidate.autoTaskHarnessReviewEnabled === true,
245
+ autoMemoryEnabled: candidate.autoMemoryEnabled === true,
245
246
  translationEnabled: candidate.translationEnabled === true,
246
247
  translationAutoSendEnabled: candidate.translationAutoSendEnabled === true,
247
248
  translationTargetLanguage: normalizeTranslationTargetLanguage(candidate.translationTargetLanguage),