vibe-coding-master 0.7.46 → 0.7.48
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 +2 -2
- package/dist/backend/cli/install-vcm-harness.js +30 -0
- package/dist/backend/server.js +3 -1
- package/dist/backend/services/architect-restart-service.js +251 -78
- package/dist/backend/services/artifact-service.js +60 -131
- package/dist/backend/services/claude-hook-service.js +3 -0
- package/dist/backend/services/gate-review-service.js +42 -217
- package/dist/backend/services/harness-feedback-service.js +13 -24
- package/dist/backend/services/harness-service.js +16 -0
- package/dist/backend/services/managed-artifact-validation.js +403 -0
- package/dist/backend/services/message-service.js +13 -64
- package/dist/backend/services/runtime-recovery-service.js +1 -0
- package/dist/backend/services/task-close-service.js +3 -1
- package/dist/backend/templates/harness/architect-agent.js +18 -3
- package/dist/backend/templates/harness/architect-evidence-worker-agent.js +44 -0
- package/dist/backend/templates/harness/architect-scaffold-worker-agent.js +3 -1
- package/dist/backend/templates/harness/architect-validation-worker-agent.js +36 -0
- package/dist/backend/templates/harness/gate-review.js +13 -98
- package/dist/backend/templates/harness/vcm-report-harness-issue-skill.js +1 -1
- package/dist/backend/templates/harness/vcm-route-message-skill.js +2 -2
- package/dist/shared/validation/artifact-registry.js +68 -0
- package/package.json +1 -1
- package/scripts/harness-tools/vcm-artifact +1 -34
- package/scripts/harness-tools/vcm-subagent-guard +9 -4
package/README.md
CHANGED
|
@@ -285,8 +285,8 @@ Codex Bridge child processes use a hard context limit and auto-compaction window
|
|
|
285
285
|
`258400` tokens. VCM sets Claude Code's proactive compaction threshold to `90%`
|
|
286
286
|
(about `232560` tokens), leaving headroom before the Codex API limit. Native
|
|
287
287
|
Claude sessions receive no VCM-owned context override. Bridge-backed subagents
|
|
288
|
-
inherit the selected GPT model
|
|
289
|
-
use `opus
|
|
288
|
+
inherit the selected GPT model. Native Architect evidence, scaffold, and
|
|
289
|
+
validation workers use `opus` with `xhigh` effort.
|
|
290
290
|
|
|
291
291
|
Resume keeps the provider recorded by the existing Session. Use Restart when
|
|
292
292
|
switching between native Claude and Codex Bridge. If the Bridge is disabled,
|
|
@@ -8,7 +8,9 @@ import { renderArchitectHarnessRules } from "../templates/harness/architect-agen
|
|
|
8
8
|
import { CODE_ROLE_DISALLOWED_TOOLS, REVIEWER_DISALLOWED_TOOLS } from "../role-tool-policy.js";
|
|
9
9
|
import { renderCoderHarnessRules } from "../templates/harness/coder-agent.js";
|
|
10
10
|
import { renderCoderWorkerHarnessRules } from "../templates/harness/coder-worker-agent.js";
|
|
11
|
+
import { renderArchitectEvidenceWorkerHarnessRules } from "../templates/harness/architect-evidence-worker-agent.js";
|
|
11
12
|
import { renderArchitectScaffoldWorkerHarnessRules } from "../templates/harness/architect-scaffold-worker-agent.js";
|
|
13
|
+
import { renderArchitectValidationWorkerHarnessRules } from "../templates/harness/architect-validation-worker-agent.js";
|
|
12
14
|
import { renderReviewerAgentRules, renderRequestGateReviewTool, renderTranslatorAgentRules, renderVcmGateReviewSkillRules } from "../templates/harness/gate-review.js";
|
|
13
15
|
import { renderHarnessEngineerHarnessRules } from "../templates/harness/harness-engineer-agent.js";
|
|
14
16
|
import { renderRootClaudeHarnessRules } from "../templates/harness/claude-root.js";
|
|
@@ -106,10 +108,22 @@ const AGENT_FRONTMATTER = {
|
|
|
106
108
|
description: "Bounded VCM implementation worker for assigned modules, files, and VCM:CODE markers from Coder.",
|
|
107
109
|
model: "inherit"
|
|
108
110
|
},
|
|
111
|
+
"vcm-architect-evidence-worker": {
|
|
112
|
+
description: "Foreground Architect worker for bounded project evidence collection.",
|
|
113
|
+
tools: "Read, Grep, Glob, Write",
|
|
114
|
+
model: "opus",
|
|
115
|
+
effort: "xhigh"
|
|
116
|
+
},
|
|
109
117
|
"vcm-architect-scaffold-worker": {
|
|
110
118
|
description: "Foreground Architect worker for exact scaffold execution and scaffold validation.",
|
|
111
119
|
model: "opus",
|
|
112
120
|
effort: "xhigh"
|
|
121
|
+
},
|
|
122
|
+
"vcm-architect-validation-worker": {
|
|
123
|
+
description: "Foreground Architect worker for exact assigned command execution and evidence capture.",
|
|
124
|
+
tools: "Read, Grep, Glob, Bash, Write",
|
|
125
|
+
model: "opus",
|
|
126
|
+
effort: "xhigh"
|
|
113
127
|
}
|
|
114
128
|
};
|
|
115
129
|
const REQUIRED_AGENT_DISALLOWED_TOOLS = {
|
|
@@ -242,6 +256,14 @@ const MANAGED_FILES = [
|
|
|
242
256
|
category: "agent-coder-worker",
|
|
243
257
|
content: renderCoderWorkerHarnessRules()
|
|
244
258
|
},
|
|
259
|
+
{
|
|
260
|
+
path: ".claude/agents/vcm-architect-evidence-worker.md",
|
|
261
|
+
title: "VCM Architect Evidence Worker Agent",
|
|
262
|
+
agentName: "vcm-architect-evidence-worker",
|
|
263
|
+
commentStyle: "html",
|
|
264
|
+
category: "agent-architect-evidence-worker",
|
|
265
|
+
content: renderArchitectEvidenceWorkerHarnessRules()
|
|
266
|
+
},
|
|
245
267
|
{
|
|
246
268
|
path: ".claude/agents/vcm-architect-scaffold-worker.md",
|
|
247
269
|
title: "VCM Architect Scaffold Worker Agent",
|
|
@@ -249,6 +271,14 @@ const MANAGED_FILES = [
|
|
|
249
271
|
commentStyle: "html",
|
|
250
272
|
category: "agent-architect-scaffold-worker",
|
|
251
273
|
content: renderArchitectScaffoldWorkerHarnessRules()
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
path: ".claude/agents/vcm-architect-validation-worker.md",
|
|
277
|
+
title: "VCM Architect Validation Worker Agent",
|
|
278
|
+
agentName: "vcm-architect-validation-worker",
|
|
279
|
+
commentStyle: "html",
|
|
280
|
+
category: "agent-architect-validation-worker",
|
|
281
|
+
content: renderArchitectValidationWorkerHarnessRules()
|
|
252
282
|
}
|
|
253
283
|
];
|
|
254
284
|
const DURABLE_DOC_TEMPLATES = [
|
package/dist/backend/server.js
CHANGED
|
@@ -297,6 +297,7 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
297
297
|
const architectRestartService = createArchitectRestartService({
|
|
298
298
|
fs,
|
|
299
299
|
taskService,
|
|
300
|
+
projectService,
|
|
300
301
|
sessionService,
|
|
301
302
|
appSettings
|
|
302
303
|
});
|
|
@@ -399,7 +400,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
399
400
|
runtime,
|
|
400
401
|
projectService,
|
|
401
402
|
taskService,
|
|
402
|
-
translationWorkerService
|
|
403
|
+
translationWorkerService,
|
|
404
|
+
architectRestartService
|
|
403
405
|
});
|
|
404
406
|
const claudeHookService = createClaudeHookService({
|
|
405
407
|
projectService,
|
|
@@ -17,55 +17,61 @@ Before performing any assigned work, read:
|
|
|
17
17
|
- the latest Gate Review report when present
|
|
18
18
|
|
|
19
19
|
Treat the current artifacts and worktree as the source of truth. The architecture-plan Gate has accepted the current planning artifacts or VCM recorded an explicit Gate exception. Do not repeat the completed interview or planning work unless a later route explicitly reopens it.`;
|
|
20
|
+
const ARCHITECT_RESTART_STATE_FILE = "architect-restart.json";
|
|
20
21
|
export function createArchitectRestartService(deps) {
|
|
21
22
|
const pendingByTask = new Map();
|
|
23
|
+
const operationsByTask = new Map();
|
|
22
24
|
return {
|
|
23
25
|
async schedule(repoRoot, taskSlug) {
|
|
24
|
-
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
25
|
-
await requireCompletePlan(repoRoot, taskSlug);
|
|
26
26
|
const key = taskKey(repoRoot, taskSlug);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await tryRestart(existing);
|
|
27
|
+
return withTaskLock(key, async () => {
|
|
28
|
+
const session = await requireRunningArchitect(repoRoot, taskSlug);
|
|
29
|
+
await requireCompletePlan(repoRoot, taskSlug);
|
|
30
|
+
const existing = pendingByTask.get(key);
|
|
31
|
+
if (existing?.status === "executing") {
|
|
33
32
|
return {
|
|
34
33
|
taskSlug,
|
|
35
|
-
sessionId:
|
|
36
|
-
status: "
|
|
34
|
+
sessionId: existing.replacementSessionId ?? existing.sourceSessionId,
|
|
35
|
+
status: "already_scheduled",
|
|
37
36
|
...(existing.memoryCandidatePath
|
|
38
37
|
? { memoryCandidatePath: existing.memoryCandidatePath }
|
|
39
38
|
: {})
|
|
40
39
|
};
|
|
41
40
|
}
|
|
42
|
-
|
|
41
|
+
if (existing && isSourceSession(existing, session)) {
|
|
42
|
+
if (existing.status === "blocked") {
|
|
43
|
+
existing.status = "pending";
|
|
44
|
+
existing.blocker = undefined;
|
|
45
|
+
existing.sourceSessionId = session.id;
|
|
46
|
+
existing.updatedAt = timestamp();
|
|
47
|
+
await persistPending(existing);
|
|
48
|
+
await tryRestart(existing);
|
|
49
|
+
return scheduleResult(existing, "scheduled");
|
|
50
|
+
}
|
|
51
|
+
return scheduleResult(existing, "already_scheduled");
|
|
52
|
+
}
|
|
53
|
+
const memoryCandidatePath = (await deps.appSettings.getPreferences()).autoMemoryEnabled
|
|
54
|
+
? ARCHITECT_PLANNING_MEMORY_CANDIDATE_PATH
|
|
55
|
+
: undefined;
|
|
56
|
+
const pending = {
|
|
57
|
+
version: 1,
|
|
58
|
+
repoRoot,
|
|
43
59
|
taskSlug,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
sourceSessionId: session.id,
|
|
61
|
+
...(session.claudeSessionId ? { sourceClaudeSessionId: session.claudeSessionId } : {}),
|
|
62
|
+
stopped: false,
|
|
63
|
+
gateAccepted: false,
|
|
64
|
+
status: "pending",
|
|
65
|
+
permissionMode: session.permissionMode,
|
|
66
|
+
model: session.model,
|
|
67
|
+
effort: session.effort,
|
|
68
|
+
memoryCandidatePath: existing?.memoryCandidatePath ?? memoryCandidatePath,
|
|
69
|
+
updatedAt: timestamp()
|
|
49
70
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
: undefined;
|
|
54
|
-
pendingByTask.set(key, {
|
|
55
|
-
repoRoot,
|
|
56
|
-
taskSlug,
|
|
57
|
-
sessionId: session.id,
|
|
58
|
-
stopped: false,
|
|
59
|
-
gateAccepted: false,
|
|
60
|
-
status: "pending",
|
|
61
|
-
memoryCandidatePath
|
|
71
|
+
pendingByTask.set(key, pending);
|
|
72
|
+
await persistPending(pending);
|
|
73
|
+
return scheduleResult(pending, "scheduled");
|
|
62
74
|
});
|
|
63
|
-
return {
|
|
64
|
-
taskSlug,
|
|
65
|
-
sessionId: session.id,
|
|
66
|
-
status: "scheduled",
|
|
67
|
-
...(memoryCandidatePath ? { memoryCandidatePath } : {})
|
|
68
|
-
};
|
|
69
75
|
},
|
|
70
76
|
getState(repoRoot, taskSlug) {
|
|
71
77
|
const pending = pendingByTask.get(taskKey(repoRoot, taskSlug));
|
|
@@ -74,7 +80,7 @@ export function createArchitectRestartService(deps) {
|
|
|
74
80
|
}
|
|
75
81
|
return {
|
|
76
82
|
taskSlug: pending.taskSlug,
|
|
77
|
-
sessionId: pending.
|
|
83
|
+
sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
|
|
78
84
|
status: pending.status,
|
|
79
85
|
...(pending.memoryCandidatePath
|
|
80
86
|
? { memoryCandidatePath: pending.memoryCandidatePath }
|
|
@@ -82,46 +88,128 @@ export function createArchitectRestartService(deps) {
|
|
|
82
88
|
...(pending.blocker ? { blocker: { ...pending.blocker } } : {})
|
|
83
89
|
};
|
|
84
90
|
},
|
|
91
|
+
async recoverTask(repoRoot, taskSlug) {
|
|
92
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
93
|
+
await withTaskLock(key, async () => {
|
|
94
|
+
const stored = await loadPending(repoRoot, taskSlug);
|
|
95
|
+
if (!stored) {
|
|
96
|
+
pendingByTask.delete(key);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const pending = { ...stored, repoRoot };
|
|
100
|
+
pendingByTask.set(key, pending);
|
|
101
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
102
|
+
if (pending.status === "executing") {
|
|
103
|
+
if (isConfirmedReplacement(pending, session)) {
|
|
104
|
+
await removePending(pending);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (session?.status === "running"
|
|
108
|
+
&& pending.replacementSessionId === session.id) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await launchReplacement(pending);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (pending.status === "pending" && restartPrerequisitesMet(pending)) {
|
|
115
|
+
if (session?.status === "running") {
|
|
116
|
+
await tryRestart(pending);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
await launchReplacement(pending);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
},
|
|
85
124
|
async recordArchitectStop(repoRoot, taskSlug, sessionId) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
126
|
+
await withTaskLock(key, async () => {
|
|
127
|
+
const pending = pendingByTask.get(key);
|
|
128
|
+
if (!pending || pending.status !== "pending") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
132
|
+
if (!session || session.id !== sessionId || !isSourceSession(pending, session)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
pending.sourceSessionId = session.id;
|
|
136
|
+
pending.stopped = true;
|
|
137
|
+
pending.updatedAt = timestamp();
|
|
138
|
+
await persistPending(pending);
|
|
139
|
+
await tryRestart(pending);
|
|
140
|
+
});
|
|
92
141
|
},
|
|
93
142
|
async recordRouteDelivered(repoRoot, taskSlug, message) {
|
|
94
143
|
if (!isArchitectToPm(message)) {
|
|
95
144
|
return;
|
|
96
145
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
147
|
+
await withTaskLock(key, async () => {
|
|
148
|
+
const pending = pendingByTask.get(key);
|
|
149
|
+
if (!pending || pending.status !== "pending") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
pending.deliveredMessageId = message.id;
|
|
153
|
+
pending.updatedAt = timestamp();
|
|
154
|
+
await persistPending(pending);
|
|
155
|
+
await tryRestart(pending);
|
|
156
|
+
});
|
|
103
157
|
},
|
|
104
158
|
async recordRouteAccepted(repoRoot, taskSlug, message) {
|
|
105
159
|
if (!isArchitectToPm(message)) {
|
|
106
160
|
return;
|
|
107
161
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
162
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
163
|
+
await withTaskLock(key, async () => {
|
|
164
|
+
const pending = pendingByTask.get(key);
|
|
165
|
+
if (!pending || pending.status !== "pending") {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
pending.acceptedMessageId = message.id;
|
|
169
|
+
pending.updatedAt = timestamp();
|
|
170
|
+
await persistPending(pending);
|
|
171
|
+
await tryRestart(pending);
|
|
172
|
+
});
|
|
114
173
|
},
|
|
115
174
|
async recordArchitectureGateDisposition(repoRoot, taskSlug, accepted) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
175
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
176
|
+
await withTaskLock(key, async () => {
|
|
177
|
+
const pending = pendingByTask.get(key);
|
|
178
|
+
if (!pending || pending.status !== "pending") {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
pending.gateAccepted = accepted;
|
|
182
|
+
pending.updatedAt = timestamp();
|
|
183
|
+
await persistPending(pending);
|
|
184
|
+
await tryRestart(pending);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
async recordReplacementPromptSubmitted(repoRoot, taskSlug, sessionId) {
|
|
188
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
189
|
+
await withTaskLock(key, async () => {
|
|
190
|
+
const pending = pendingByTask.get(key);
|
|
191
|
+
if (!pending || pending.status !== "executing") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const session = await deps.sessionService.getRoleSession(repoRoot, taskSlug, ARCHITECT_ROLE);
|
|
195
|
+
if (!session
|
|
196
|
+
|| session.id !== sessionId
|
|
197
|
+
|| !session.claudeSessionId
|
|
198
|
+
|| (pending.replacementSessionId && pending.replacementSessionId !== session.id)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await removePending(pending);
|
|
202
|
+
});
|
|
122
203
|
},
|
|
123
|
-
clear(repoRoot, taskSlug) {
|
|
124
|
-
|
|
204
|
+
async clear(repoRoot, taskSlug) {
|
|
205
|
+
const key = taskKey(repoRoot, taskSlug);
|
|
206
|
+
await withTaskLock(key, async () => {
|
|
207
|
+
const pending = pendingByTask.get(key) ?? await loadPending(repoRoot, taskSlug);
|
|
208
|
+
pendingByTask.delete(key);
|
|
209
|
+
if (pending) {
|
|
210
|
+
await removePending({ ...pending, repoRoot });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
125
213
|
}
|
|
126
214
|
};
|
|
127
215
|
async function requireRunningArchitect(repoRoot, taskSlug) {
|
|
@@ -147,17 +235,12 @@ export function createArchitectRestartService(deps) {
|
|
|
147
235
|
}
|
|
148
236
|
}
|
|
149
237
|
async function tryRestart(pending) {
|
|
150
|
-
if (pending.status !== "pending"
|
|
151
|
-
|| !pending.stopped
|
|
152
|
-
|| !pending.deliveredMessageId
|
|
153
|
-
|| pending.deliveredMessageId !== pending.acceptedMessageId
|
|
154
|
-
|| !pending.gateAccepted) {
|
|
238
|
+
if (pending.status !== "pending" || !restartPrerequisitesMet(pending)) {
|
|
155
239
|
return;
|
|
156
240
|
}
|
|
157
241
|
const session = await deps.sessionService.getRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE);
|
|
158
|
-
if (!session
|
|
159
|
-
|
|
160
|
-
blockPending(pending, new VcmError({
|
|
242
|
+
if (!session || !isSourceSession(pending, session)) {
|
|
243
|
+
await blockPending(pending, new VcmError({
|
|
161
244
|
code: "ARCHITECT_RESTART_SESSION_UNAVAILABLE",
|
|
162
245
|
message: "Architect restart is blocked because the scheduled Architect session no longer exists.",
|
|
163
246
|
statusCode: 409
|
|
@@ -165,7 +248,7 @@ export function createArchitectRestartService(deps) {
|
|
|
165
248
|
return;
|
|
166
249
|
}
|
|
167
250
|
if (session.status !== "running") {
|
|
168
|
-
blockPending(pending, new VcmError({
|
|
251
|
+
await blockPending(pending, new VcmError({
|
|
169
252
|
code: "ARCHITECT_RESTART_SESSION_NOT_RUNNING",
|
|
170
253
|
message: "Architect restart is blocked because the scheduled Architect session is not running.",
|
|
171
254
|
statusCode: 409
|
|
@@ -175,31 +258,121 @@ export function createArchitectRestartService(deps) {
|
|
|
175
258
|
if (session.activityStatus !== "idle") {
|
|
176
259
|
return;
|
|
177
260
|
}
|
|
261
|
+
pending.sourceSessionId = session.id;
|
|
262
|
+
await launchReplacement(pending);
|
|
263
|
+
}
|
|
264
|
+
async function launchReplacement(pending) {
|
|
178
265
|
pending.status = "executing";
|
|
266
|
+
pending.blocker = undefined;
|
|
267
|
+
pending.updatedAt = timestamp();
|
|
268
|
+
await persistPending(pending);
|
|
179
269
|
try {
|
|
180
270
|
await requireCompletePlan(pending.repoRoot, pending.taskSlug);
|
|
181
271
|
await requirePlanningMemoryCandidate(pending);
|
|
182
|
-
await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
183
|
-
permissionMode:
|
|
184
|
-
model:
|
|
185
|
-
effort:
|
|
272
|
+
const replacement = await deps.sessionService.restartRoleSession(pending.repoRoot, pending.taskSlug, ARCHITECT_ROLE, {
|
|
273
|
+
permissionMode: pending.permissionMode,
|
|
274
|
+
model: pending.model,
|
|
275
|
+
effort: pending.effort,
|
|
186
276
|
appendSystemPrompt: ARCHITECT_RESTORE_PROMPT
|
|
187
277
|
});
|
|
188
|
-
|
|
278
|
+
pending.replacementSessionId = replacement.id;
|
|
279
|
+
pending.updatedAt = timestamp();
|
|
280
|
+
await persistPending(pending);
|
|
189
281
|
}
|
|
190
282
|
catch (error) {
|
|
191
|
-
blockPending(pending, error);
|
|
283
|
+
await blockPending(pending, error);
|
|
192
284
|
}
|
|
193
285
|
}
|
|
194
|
-
function blockPending(pending, error) {
|
|
286
|
+
async function blockPending(pending, error) {
|
|
195
287
|
const normalized = toVcmError(error);
|
|
196
288
|
pending.status = "blocked";
|
|
197
289
|
pending.blocker = {
|
|
198
290
|
code: normalized.code,
|
|
199
291
|
message: normalized.message,
|
|
200
|
-
blockedAt:
|
|
292
|
+
blockedAt: timestamp()
|
|
293
|
+
};
|
|
294
|
+
pending.updatedAt = timestamp();
|
|
295
|
+
await persistPending(pending);
|
|
296
|
+
}
|
|
297
|
+
function restartPrerequisitesMet(pending) {
|
|
298
|
+
return Boolean(pending.stopped
|
|
299
|
+
&& pending.deliveredMessageId
|
|
300
|
+
&& pending.deliveredMessageId === pending.acceptedMessageId
|
|
301
|
+
&& pending.gateAccepted);
|
|
302
|
+
}
|
|
303
|
+
function isSourceSession(pending, session) {
|
|
304
|
+
return session.id === pending.sourceSessionId
|
|
305
|
+
|| Boolean(pending.sourceClaudeSessionId
|
|
306
|
+
&& session.claudeSessionId
|
|
307
|
+
&& pending.sourceClaudeSessionId === session.claudeSessionId);
|
|
308
|
+
}
|
|
309
|
+
function isConfirmedReplacement(pending, session) {
|
|
310
|
+
return Boolean(session?.claudeSessionId
|
|
311
|
+
&& ((pending.replacementSessionId && session.id === pending.replacementSessionId)
|
|
312
|
+
|| !pending.sourceClaudeSessionId
|
|
313
|
+
|| session.claudeSessionId !== pending.sourceClaudeSessionId));
|
|
314
|
+
}
|
|
315
|
+
function scheduleResult(pending, status) {
|
|
316
|
+
return {
|
|
317
|
+
taskSlug: pending.taskSlug,
|
|
318
|
+
sessionId: pending.replacementSessionId ?? pending.sourceSessionId,
|
|
319
|
+
status,
|
|
320
|
+
...(pending.memoryCandidatePath ? { memoryCandidatePath: pending.memoryCandidatePath } : {})
|
|
201
321
|
};
|
|
202
322
|
}
|
|
323
|
+
async function persistPending(pending) {
|
|
324
|
+
const { repoRoot: _repoRoot, ...stored } = pending;
|
|
325
|
+
await deps.fs.writeJsonAtomic(await statePath(pending.repoRoot, pending.taskSlug), stored);
|
|
326
|
+
pendingByTask.set(taskKey(pending.repoRoot, pending.taskSlug), pending);
|
|
327
|
+
}
|
|
328
|
+
async function loadPending(repoRoot, taskSlug) {
|
|
329
|
+
const target = await statePath(repoRoot, taskSlug);
|
|
330
|
+
if (!(await deps.fs.pathExists(target))) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
const stored = await deps.fs.readJson(target);
|
|
334
|
+
if (stored.version !== 1
|
|
335
|
+
|| stored.taskSlug !== taskSlug
|
|
336
|
+
|| !stored.sourceSessionId
|
|
337
|
+
|| !["pending", "executing", "blocked"].includes(stored.status)) {
|
|
338
|
+
throw new VcmError({
|
|
339
|
+
code: "ARCHITECT_RESTART_STATE_INVALID",
|
|
340
|
+
message: `Architect restart state is invalid for task ${taskSlug}.`,
|
|
341
|
+
statusCode: 500
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return stored;
|
|
345
|
+
}
|
|
346
|
+
async function removePending(pending) {
|
|
347
|
+
pendingByTask.delete(taskKey(pending.repoRoot, pending.taskSlug));
|
|
348
|
+
const target = await statePath(pending.repoRoot, pending.taskSlug);
|
|
349
|
+
if (await deps.fs.pathExists(target)) {
|
|
350
|
+
await deps.fs.removePath?.(target, { force: true });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function statePath(repoRoot, taskSlug) {
|
|
354
|
+
const [config, task] = await Promise.all([
|
|
355
|
+
deps.projectService.loadConfig(repoRoot),
|
|
356
|
+
deps.taskService.loadTask(repoRoot, taskSlug)
|
|
357
|
+
]);
|
|
358
|
+
return resolveRepoPath(getTaskRuntimeRepoRoot(task), path.posix.join(config.stateRoot, ARCHITECT_RESTART_STATE_FILE));
|
|
359
|
+
}
|
|
360
|
+
async function withTaskLock(key, run) {
|
|
361
|
+
const previous = operationsByTask.get(key) ?? Promise.resolve();
|
|
362
|
+
const current = previous.catch(() => undefined).then(run);
|
|
363
|
+
operationsByTask.set(key, current);
|
|
364
|
+
try {
|
|
365
|
+
return await current;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (operationsByTask.get(key) === current) {
|
|
369
|
+
operationsByTask.delete(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function timestamp() {
|
|
374
|
+
return new Date().toISOString();
|
|
375
|
+
}
|
|
203
376
|
async function requirePlanningMemoryCandidate(pending) {
|
|
204
377
|
if (!pending.memoryCandidatePath) {
|
|
205
378
|
return;
|