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 +12 -10
- package/dist/backend/api/harness-routes.js +10 -4
- package/dist/backend/api/task-routes.js +2 -32
- package/dist/backend/cli/install-vcm-harness.js +9 -1
- package/dist/backend/gateway/gateway-service.js +4 -37
- package/dist/backend/server.js +35 -16
- package/dist/backend/services/auto-memory-service.js +57 -50
- package/dist/backend/services/claude-hook-service.js +42 -2
- package/dist/backend/services/claude-transcript-reply.js +81 -1
- package/dist/backend/services/harness-service.js +10 -1
- package/dist/backend/services/runtime-coordinator-service.js +43 -9
- package/dist/backend/services/runtime-recovery-service.js +6 -0
- package/dist/backend/services/task-close-service.js +88 -0
- package/dist/backend/services/task-service.js +152 -35
- package/dist/backend/services/turn-reconciler-service.js +122 -0
- package/dist/backend/templates/harness/claude-root.js +1 -0
- package/dist/backend/templates/harness/harness-engineer-agent.js +15 -17
- package/dist/backend/templates/harness/role-memory.js +11 -1
- package/dist/backend/templates/harness/vcm-propose-memory-skill.js +35 -0
- package/dist-frontend/assets/{index-9V9COJZy.js → index-e8Tqa8Qh.js} +26 -25
- package/dist-frontend/index.html +1 -1
- package/package.json +1 -1
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
->
|
|
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
|
|
413
|
-
enabled, both automatic and manual
|
|
414
|
-
|
|
415
|
-
retried from Harness Studio before retrospective
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
633
|
-
|
|
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"}.`;
|
package/dist/backend/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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.
|
|
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
|
|
653
|
+
"[VCM Task Harness Review: Memory Proposal]",
|
|
638
654
|
"",
|
|
639
|
-
"
|
|
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
|
|
667
|
+
"[VCM Task Harness Review: Memory Review]",
|
|
661
668
|
"",
|
|
662
|
-
"Review the role
|
|
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
|
-
"
|
|
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");
|