vibe-coding-master 0.7.2 → 0.7.4
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/dist/backend/api/harness-routes.js +0 -21
- package/dist/backend/server.js +2 -4
- package/dist/backend/services/claude-hook-service.js +0 -1
- package/dist/backend/services/harness-feedback-service.js +49 -404
- package/dist/backend/services/runtime-recovery-service.js +4 -31
- package/dist/backend/templates/harness/coder-agent.js +8 -2
- package/dist/backend/templates/harness/project-manager-agent.js +6 -4
- package/dist-frontend/assets/{index-C2QzumXk.css → index-DCb-S6Ls.css} +1 -1
- package/dist-frontend/assets/{index-e8Tqa8Qh.js → index-NTlycxx9.js} +42 -42
- package/dist-frontend/index.html +2 -2
- package/package.json +2 -1
|
@@ -131,27 +131,6 @@ export function registerHarnessRoutes(app, deps) {
|
|
|
131
131
|
const taskSlug = await normalizeOptionalTaskSlug(deps, project.repoRoot, request.query.taskSlug);
|
|
132
132
|
return deps.harnessFeedbackService.getState(project.repoRoot, taskSlug);
|
|
133
133
|
});
|
|
134
|
-
app.post("/api/projects/harness/feedback/decision", async (request) => {
|
|
135
|
-
const project = await requireCurrentProject(deps.projectService);
|
|
136
|
-
const action = request.body?.action;
|
|
137
|
-
if (action !== "approve" && action !== "reject" && action !== "comment" && action !== "cancel") {
|
|
138
|
-
throw new VcmError({
|
|
139
|
-
code: "HARNESS_FEEDBACK_DECISION_INVALID",
|
|
140
|
-
message: "Harness feedback decision action is invalid.",
|
|
141
|
-
statusCode: 400
|
|
142
|
-
});
|
|
143
|
-
}
|
|
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
|
-
}
|
|
149
|
-
return deps.harnessFeedbackService.decide(project.repoRoot, {
|
|
150
|
-
action,
|
|
151
|
-
taskSlug,
|
|
152
|
-
comment: typeof request.body?.comment === "string" ? request.body.comment : undefined
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
134
|
app.post("/api/projects/harness/task-retrospective", async (request) => {
|
|
156
135
|
const { project, task } = await requireHarnessTaskContext(deps, request.body?.taskSlug);
|
|
157
136
|
const trigger = request.body?.trigger === "auto" ? "auto" : "manual";
|
package/dist/backend/server.js
CHANGED
|
@@ -233,9 +233,8 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
233
233
|
runtime,
|
|
234
234
|
sessionService,
|
|
235
235
|
appSettings,
|
|
236
|
-
async isHarnessEngineerAvailable(
|
|
237
|
-
|
|
238
|
-
return state.status === "idle";
|
|
236
|
+
async isHarnessEngineerAvailable() {
|
|
237
|
+
return true;
|
|
239
238
|
}
|
|
240
239
|
});
|
|
241
240
|
const commandDispatcher = createCommandDispatcher({
|
|
@@ -347,7 +346,6 @@ export function createDefaultServerDeps(options = {}) {
|
|
|
347
346
|
appSettings,
|
|
348
347
|
runtime,
|
|
349
348
|
harnessService,
|
|
350
|
-
harnessFeedbackService,
|
|
351
349
|
autoMemoryService,
|
|
352
350
|
gatewayService,
|
|
353
351
|
jobGuard: createJobGuardService(),
|
|
@@ -126,7 +126,6 @@ export function createClaudeHookService(deps) {
|
|
|
126
126
|
sessionId: session?.id,
|
|
127
127
|
claudeSessionId: stringOrUndefined(input.event.session_id)
|
|
128
128
|
});
|
|
129
|
-
await deps.harnessFeedbackService?.recordHarnessEngineerHook(context.project.repoRoot, eventName);
|
|
130
129
|
return {
|
|
131
130
|
ok: true,
|
|
132
131
|
eventName,
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { checkMarkdownArtifact, readArtifactSectionValue } from "../../shared/validation/artifact-check.js";
|
|
4
|
-
import { resolveRepoPath
|
|
4
|
+
import { resolveRepoPath } from "../adapters/filesystem.js";
|
|
5
5
|
import { VcmError } from "../errors.js";
|
|
6
6
|
import { submitTerminalInput } from "../runtime/terminal-submit.js";
|
|
7
7
|
const FEEDBACK_ROOT = ".ai/vcm/harness-feedback";
|
|
8
8
|
const PENDING_DIR = `${FEEDBACK_ROOT}/pending`;
|
|
9
|
-
const ACTIVE_DIR = `${FEEDBACK_ROOT}/active`;
|
|
10
|
-
const COMPLETED_DIR = `${FEEDBACK_ROOT}/completed`;
|
|
11
9
|
const TASK_RETROSPECTIVE_DIR = `${FEEDBACK_ROOT}/task-retrospectives`;
|
|
12
|
-
const
|
|
10
|
+
const LEGACY_STATE_PATH = `${FEEDBACK_ROOT}/state.json`;
|
|
13
11
|
export function createHarnessFeedbackService(deps) {
|
|
14
12
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
15
|
-
async function getState(repoRoot,
|
|
16
|
-
await
|
|
17
|
-
|
|
13
|
+
async function getState(repoRoot, _activeTaskSlug) {
|
|
14
|
+
await cleanupLegacyState(repoRoot);
|
|
15
|
+
const pending = await listPendingFeedback(repoRoot);
|
|
16
|
+
return {
|
|
17
|
+
version: 1,
|
|
18
|
+
status: pending.length > 0 ? "queued" : "idle",
|
|
19
|
+
queuedCount: pending.length,
|
|
20
|
+
pending,
|
|
21
|
+
warnings: []
|
|
22
|
+
};
|
|
18
23
|
}
|
|
19
24
|
async function startTaskRetrospective(repoRoot, input) {
|
|
25
|
+
await cleanupLegacyState(repoRoot);
|
|
20
26
|
const taskSlug = input.taskSlug.trim();
|
|
21
27
|
if (!taskSlug) {
|
|
22
28
|
throw new VcmError({
|
|
@@ -25,22 +31,13 @@ export function createHarnessFeedbackService(deps) {
|
|
|
25
31
|
statusCode: 409
|
|
26
32
|
});
|
|
27
33
|
}
|
|
28
|
-
const state = await loadStoredState(repoRoot);
|
|
29
|
-
if (state) {
|
|
30
|
-
throw new VcmError({
|
|
31
|
-
code: "HARNESS_FEEDBACK_ACTIVE",
|
|
32
|
-
message: "Harness feedback is already active.",
|
|
33
|
-
statusCode: 409,
|
|
34
|
-
hint: "Review, approve, comment, or reject the current Harness feedback before starting Task Harness Retrospective."
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
34
|
const existingMarker = await loadTaskRetrospectiveMarker(repoRoot, taskSlug);
|
|
38
35
|
if (existingMarker) {
|
|
39
36
|
throw new VcmError({
|
|
40
37
|
code: "TASK_HARNESS_RETROSPECTIVE_EXISTS",
|
|
41
38
|
message: `Task Harness Retrospective has already been triggered for task: ${taskSlug}`,
|
|
42
39
|
statusCode: 409,
|
|
43
|
-
hint: "Review the existing
|
|
40
|
+
hint: "Review the existing retrospective result instead of starting another retrospective for the same task."
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
43
|
const finalAcceptancePath = path.posix.join(input.handoffDir, "final-acceptance.md");
|
|
@@ -62,204 +59,39 @@ export function createHarnessFeedbackService(deps) {
|
|
|
62
59
|
}
|
|
63
60
|
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
64
61
|
const timestamp = now();
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const analysisPath = `${ACTIVE_DIR}/${id}/analysis.md`;
|
|
69
|
-
const applyReportPath = `${ACTIVE_DIR}/${id}/apply-report.md`;
|
|
70
|
-
const active = {
|
|
71
|
-
id,
|
|
72
|
-
title: `Task Harness Retrospective: ${taskSlug}`,
|
|
73
|
-
path: feedbackPath,
|
|
74
|
-
source: "task-retrospective",
|
|
62
|
+
const analysisPath = `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.md`;
|
|
63
|
+
await persistTaskRetrospectiveMarker(repoRoot, {
|
|
64
|
+
version: 1,
|
|
75
65
|
taskSlug,
|
|
76
|
-
summary: "Review the completed task workflow for reusable harness problems.",
|
|
77
66
|
trigger: input.trigger,
|
|
78
|
-
|
|
79
|
-
feedbackPath,
|
|
67
|
+
status: "triggered",
|
|
80
68
|
analysisPath,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
updatedAt: timestamp
|
|
84
|
-
lastPromptAt: timestamp
|
|
85
|
-
};
|
|
86
|
-
const nextState = {
|
|
87
|
-
version: 1,
|
|
88
|
-
status: "analyzing",
|
|
89
|
-
active
|
|
90
|
-
};
|
|
91
|
-
await deps.fs.ensureDir(resolveRepoPath(repoRoot, path.posix.dirname(feedbackPath)));
|
|
92
|
-
await deps.fs.writeText(resolveRepoPath(repoRoot, feedbackPath), renderTaskRetrospectiveFeedback(active, finalAcceptancePath));
|
|
93
|
-
await persistStoredState(repoRoot, nextState);
|
|
94
|
-
await persistTaskRetrospectiveMarker(repoRoot, active, "analyzing");
|
|
95
|
-
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, active));
|
|
96
|
-
return buildStateReport(repoRoot);
|
|
97
|
-
}
|
|
98
|
-
async function decide(repoRoot, input) {
|
|
99
|
-
const state = await loadStoredState(repoRoot);
|
|
100
|
-
if (state && input.action === "cancel") {
|
|
101
|
-
await completeActive(repoRoot, state, "canceled", input.comment);
|
|
102
|
-
await clearStoredState(repoRoot);
|
|
103
|
-
return getState(repoRoot, input.taskSlug);
|
|
104
|
-
}
|
|
105
|
-
if (!state || state.status !== "awaiting_user_approval") {
|
|
106
|
-
throw new VcmError({
|
|
107
|
-
code: "HARNESS_FEEDBACK_NOT_AWAITING_APPROVAL",
|
|
108
|
-
message: "There is no Harness feedback waiting for user approval.",
|
|
109
|
-
statusCode: 409
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
if (input.action === "reject") {
|
|
113
|
-
await completeActive(repoRoot, state, "rejected", input.comment);
|
|
114
|
-
await clearStoredState(repoRoot);
|
|
115
|
-
return getState(repoRoot, input.taskSlug);
|
|
116
|
-
}
|
|
117
|
-
const taskSlug = input.taskSlug?.trim();
|
|
118
|
-
if (!taskSlug) {
|
|
119
|
-
throw new VcmError({
|
|
120
|
-
code: "HARNESS_FEEDBACK_TASK_REQUIRED",
|
|
121
|
-
message: "Select an active task before asking Harness Engineer to continue feedback work.",
|
|
122
|
-
statusCode: 409
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
if (input.action === "comment") {
|
|
126
|
-
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
127
|
-
const timestamp = now();
|
|
128
|
-
const nextState = {
|
|
129
|
-
...state,
|
|
130
|
-
status: "analyzing",
|
|
131
|
-
active: {
|
|
132
|
-
...state.active,
|
|
133
|
-
updatedAt: timestamp,
|
|
134
|
-
lastPromptAt: timestamp
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
await persistStoredState(repoRoot, nextState);
|
|
138
|
-
await persistTaskRetrospectiveMarker(repoRoot, nextState.active, "analyzing");
|
|
139
|
-
await submitTerminalInput(deps.runtime, session.id, buildFeedbackCommentPrompt(repoRoot, nextState.active, input.comment ?? ""));
|
|
140
|
-
return buildStateReport(repoRoot);
|
|
141
|
-
}
|
|
142
|
-
const session = await ensureIdleHarnessEngineer(repoRoot, taskSlug);
|
|
143
|
-
const timestamp = now();
|
|
144
|
-
const nextState = {
|
|
145
|
-
...state,
|
|
146
|
-
status: "applying",
|
|
147
|
-
active: {
|
|
148
|
-
...state.active,
|
|
149
|
-
updatedAt: timestamp,
|
|
150
|
-
lastPromptAt: timestamp
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
await persistStoredState(repoRoot, nextState);
|
|
154
|
-
await persistTaskRetrospectiveMarker(repoRoot, nextState.active, "applying");
|
|
155
|
-
await submitTerminalInput(deps.runtime, session.id, buildFeedbackApplyPrompt(repoRoot, nextState.active, input.comment ?? ""));
|
|
156
|
-
return buildStateReport(repoRoot);
|
|
157
|
-
}
|
|
158
|
-
async function recordHarnessEngineerHook(repoRoot, eventName) {
|
|
159
|
-
if (eventName !== "Stop" && eventName !== "StopFailure") {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const state = await loadStoredState(repoRoot);
|
|
163
|
-
if (!state) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const timestamp = now();
|
|
167
|
-
if (state.status === "analyzing") {
|
|
168
|
-
const nextState = {
|
|
169
|
-
...state,
|
|
170
|
-
status: "awaiting_user_approval",
|
|
171
|
-
active: {
|
|
172
|
-
...state.active,
|
|
173
|
-
updatedAt: timestamp
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
await persistStoredState(repoRoot, nextState);
|
|
177
|
-
await persistTaskRetrospectiveMarker(repoRoot, nextState.active, "awaiting_user_approval", timestamp);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (state.status === "applying") {
|
|
181
|
-
await completeActive(repoRoot, {
|
|
182
|
-
...state,
|
|
183
|
-
active: {
|
|
184
|
-
...state.active,
|
|
185
|
-
updatedAt: timestamp
|
|
186
|
-
}
|
|
187
|
-
}, "applied");
|
|
188
|
-
await clearStoredState(repoRoot);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
async function assertHarnessEngineerAvailable(repoRoot) {
|
|
192
|
-
const state = await loadStoredState(repoRoot);
|
|
193
|
-
if (!state) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
throw new VcmError({
|
|
197
|
-
code: "HARNESS_ENGINEER_FEEDBACK_ACTIVE",
|
|
198
|
-
message: "Harness Engineer is reserved for an active Harness feedback item.",
|
|
199
|
-
statusCode: 409,
|
|
200
|
-
hint: state.status === "awaiting_user_approval"
|
|
201
|
-
? "Review, approve, comment, or reject the current Harness feedback before starting another Harness Engineer task."
|
|
202
|
-
: "Wait for the current Harness feedback turn to finish before starting another Harness Engineer task."
|
|
69
|
+
finalAcceptanceHash: `sha256:${sha256(finalAcceptanceContent)}`,
|
|
70
|
+
createdAt: timestamp,
|
|
71
|
+
updatedAt: timestamp
|
|
203
72
|
});
|
|
73
|
+
await submitTerminalInput(deps.runtime, session.id, buildTaskRetrospectivePrompt(repoRoot, analysisPath));
|
|
74
|
+
return getState(repoRoot);
|
|
204
75
|
}
|
|
205
|
-
async function
|
|
206
|
-
|
|
207
|
-
if (state) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const pending = await listPendingFeedback(repoRoot);
|
|
211
|
-
const next = pending[0];
|
|
212
|
-
const taskSlug = activeTaskSlug?.trim();
|
|
213
|
-
if (!next || !taskSlug) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const session = await getIdleHarnessEngineer(repoRoot, taskSlug);
|
|
217
|
-
if (!session) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const timestamp = now();
|
|
221
|
-
const analysisPath = `${ACTIVE_DIR}/${next.id}/analysis.md`;
|
|
222
|
-
const applyReportPath = `${ACTIVE_DIR}/${next.id}/apply-report.md`;
|
|
223
|
-
const active = {
|
|
224
|
-
...next,
|
|
225
|
-
feedbackPath: next.path,
|
|
226
|
-
source: next.source ?? "role-feedback",
|
|
227
|
-
analysisPath,
|
|
228
|
-
applyReportPath,
|
|
229
|
-
startedAt: timestamp,
|
|
230
|
-
updatedAt: timestamp,
|
|
231
|
-
lastPromptAt: timestamp
|
|
232
|
-
};
|
|
233
|
-
const nextState = {
|
|
234
|
-
version: 1,
|
|
235
|
-
status: "analyzing",
|
|
236
|
-
active
|
|
237
|
-
};
|
|
238
|
-
await persistStoredState(repoRoot, nextState);
|
|
239
|
-
await deps.fs.ensureDir(resolveRepoPath(repoRoot, path.posix.dirname(analysisPath)));
|
|
240
|
-
await submitTerminalInput(deps.runtime, session.id, await buildFeedbackAnalysisPrompt(repoRoot, active));
|
|
76
|
+
async function assertHarnessEngineerAvailable(_repoRoot) {
|
|
77
|
+
return undefined;
|
|
241
78
|
}
|
|
242
|
-
async function
|
|
79
|
+
async function ensureIdleHarnessEngineer(repoRoot, taskSlug) {
|
|
243
80
|
const existing = await deps.sessionService.getProjectHarnessEngineerSession(repoRoot);
|
|
244
81
|
if (existing?.status === "running" && existing.activityStatus === "running") {
|
|
245
|
-
|
|
82
|
+
throw new VcmError({
|
|
83
|
+
code: "HARNESS_ENGINEER_BUSY",
|
|
84
|
+
message: "Harness Engineer is busy or unavailable.",
|
|
85
|
+
statusCode: 409,
|
|
86
|
+
hint: "Wait for the current Harness Engineer turn to finish, then retry."
|
|
87
|
+
});
|
|
246
88
|
}
|
|
247
89
|
const session = await deps.sessionService.ensureProjectHarnessEngineerSession(repoRoot, {
|
|
248
90
|
taskSlug,
|
|
249
91
|
cols: 120,
|
|
250
92
|
rows: 32
|
|
251
93
|
});
|
|
252
|
-
if (session.status !== "running" || session.activityStatus === "running") {
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
if (!deps.runtime.getSession(session.id)) {
|
|
256
|
-
return undefined;
|
|
257
|
-
}
|
|
258
|
-
return session;
|
|
259
|
-
}
|
|
260
|
-
async function ensureIdleHarnessEngineer(repoRoot, taskSlug) {
|
|
261
|
-
const session = await getIdleHarnessEngineer(repoRoot, taskSlug);
|
|
262
|
-
if (!session) {
|
|
94
|
+
if (session.status !== "running" || session.activityStatus === "running" || !deps.runtime.getSession(session.id)) {
|
|
263
95
|
throw new VcmError({
|
|
264
96
|
code: "HARNESS_ENGINEER_BUSY",
|
|
265
97
|
message: "Harness Engineer is busy or unavailable.",
|
|
@@ -269,54 +101,6 @@ export function createHarnessFeedbackService(deps) {
|
|
|
269
101
|
}
|
|
270
102
|
return session;
|
|
271
103
|
}
|
|
272
|
-
async function buildStateReport(repoRoot) {
|
|
273
|
-
const [state, pending] = await Promise.all([
|
|
274
|
-
loadStoredState(repoRoot),
|
|
275
|
-
listPendingFeedback(repoRoot)
|
|
276
|
-
]);
|
|
277
|
-
if (!state) {
|
|
278
|
-
return {
|
|
279
|
-
version: 1,
|
|
280
|
-
status: pending.length > 0 ? "queued" : "idle",
|
|
281
|
-
queuedCount: pending.length,
|
|
282
|
-
pending,
|
|
283
|
-
warnings: []
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
const active = await readActiveItem(repoRoot, state);
|
|
287
|
-
return {
|
|
288
|
-
version: 1,
|
|
289
|
-
status: state.status,
|
|
290
|
-
queuedCount: Math.max(0, pending.length - (pending.some((item) => item.id === state.active.id) ? 1 : 0)),
|
|
291
|
-
pending: pending.filter((item) => item.id !== state.active.id),
|
|
292
|
-
active,
|
|
293
|
-
warnings: []
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
async function readActiveItem(repoRoot, state) {
|
|
297
|
-
const feedbackContent = await readOptionalText(repoRoot, state.active.feedbackPath) ?? "";
|
|
298
|
-
const analysisContent = await readOptionalText(repoRoot, state.active.analysisPath);
|
|
299
|
-
const applyReportContent = await readOptionalText(repoRoot, state.active.applyReportPath);
|
|
300
|
-
return {
|
|
301
|
-
id: state.active.id,
|
|
302
|
-
title: state.active.title,
|
|
303
|
-
path: state.active.path,
|
|
304
|
-
source: state.active.source,
|
|
305
|
-
reporterRole: state.active.reporterRole,
|
|
306
|
-
taskSlug: state.active.taskSlug,
|
|
307
|
-
summary: state.active.summary,
|
|
308
|
-
status: state.status,
|
|
309
|
-
startedAt: state.active.startedAt,
|
|
310
|
-
updatedAt: state.active.updatedAt,
|
|
311
|
-
trigger: state.active.trigger,
|
|
312
|
-
finalAcceptanceHash: state.active.finalAcceptanceHash,
|
|
313
|
-
feedbackContent,
|
|
314
|
-
analysisPath: state.active.analysisPath,
|
|
315
|
-
analysisContent,
|
|
316
|
-
applyReportPath: state.active.applyReportPath,
|
|
317
|
-
applyReportContent
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
104
|
async function listPendingFeedback(repoRoot) {
|
|
321
105
|
const pendingDir = resolveRepoPath(repoRoot, PENDING_DIR);
|
|
322
106
|
if (!(await deps.fs.pathExists(pendingDir))) {
|
|
@@ -350,135 +134,37 @@ export function createHarnessFeedbackService(deps) {
|
|
|
350
134
|
summary: metadata.summary
|
|
351
135
|
};
|
|
352
136
|
}
|
|
353
|
-
|
|
354
|
-
const feedback = await readOptionalText(repoRoot, active.feedbackPath) ?? "";
|
|
355
|
-
return [
|
|
356
|
-
"[VCM Harness Feedback Analysis]",
|
|
357
|
-
"",
|
|
358
|
-
"Analyze this harness feedback. Do not edit files yet.",
|
|
359
|
-
"",
|
|
360
|
-
`Base repository root: ${repoRoot}`,
|
|
361
|
-
`Feedback file: ${resolveRepoPath(repoRoot, active.feedbackPath)}`,
|
|
362
|
-
`Result path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
363
|
-
"",
|
|
364
|
-
"Rules:",
|
|
365
|
-
"- Decide whether the reported issue is a real reusable harness problem.",
|
|
366
|
-
"- Inspect relevant harness files before judging.",
|
|
367
|
-
"- If the issue is not real or does not need a harness change, say so in the result file.",
|
|
368
|
-
"- If it should be fixed, write a short proposal with affected files, proposed diff shape, risks, validation, and whether a VCM GitHub issue is needed.",
|
|
369
|
-
"- Do not edit harness files or product source during this analysis turn.",
|
|
370
|
-
"- End your turn after writing the result file.",
|
|
371
|
-
"",
|
|
372
|
-
"<HARNESS_FEEDBACK>",
|
|
373
|
-
feedback.trimEnd(),
|
|
374
|
-
"</HARNESS_FEEDBACK>"
|
|
375
|
-
].join("\n");
|
|
376
|
-
}
|
|
377
|
-
function buildTaskRetrospectivePrompt(repoRoot, active) {
|
|
137
|
+
function buildTaskRetrospectivePrompt(repoRoot, analysisPath) {
|
|
378
138
|
return [
|
|
379
139
|
"[VCM Task Harness Retrospective]",
|
|
380
140
|
"",
|
|
381
141
|
"Review the completed task from the current active task worktree.",
|
|
382
142
|
"",
|
|
383
|
-
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot,
|
|
143
|
+
`Write the analysis to Result Path: ${resolveRepoPath(repoRoot, analysisPath)}`,
|
|
384
144
|
"End your turn after writing the result."
|
|
385
145
|
].join("\n");
|
|
386
146
|
}
|
|
387
|
-
function
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
"",
|
|
391
|
-
"The user reviewed your harness feedback analysis and added comments.",
|
|
392
|
-
"",
|
|
393
|
-
`Feedback file: ${resolveRepoPath(repoRoot, active.feedbackPath)}`,
|
|
394
|
-
`Current analysis path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
395
|
-
`Rewrite the analysis result at: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
396
|
-
"",
|
|
397
|
-
"Rules:",
|
|
398
|
-
"- Do not edit harness files yet.",
|
|
399
|
-
"- Address the user's comments and keep the proposal concise.",
|
|
400
|
-
"- End your turn after updating the analysis result file.",
|
|
401
|
-
"",
|
|
402
|
-
"<USER_COMMENT>",
|
|
403
|
-
comment.trim(),
|
|
404
|
-
"</USER_COMMENT>"
|
|
405
|
-
].join("\n");
|
|
406
|
-
}
|
|
407
|
-
function buildFeedbackApplyPrompt(repoRoot, active, comment) {
|
|
408
|
-
return [
|
|
409
|
-
"[VCM Harness Feedback Approved]",
|
|
410
|
-
"",
|
|
411
|
-
"The user approved this harness improvement. Apply only the approved harness changes.",
|
|
412
|
-
"",
|
|
413
|
-
`Base repository root: ${repoRoot}`,
|
|
414
|
-
`Feedback file: ${resolveRepoPath(repoRoot, active.feedbackPath)}`,
|
|
415
|
-
`Approved analysis path: ${resolveRepoPath(repoRoot, active.analysisPath)}`,
|
|
416
|
-
`Write completion report to: ${resolveRepoPath(repoRoot, active.applyReportPath)}`,
|
|
417
|
-
"",
|
|
418
|
-
"Rules:",
|
|
419
|
-
"- Work in the active task worktree.",
|
|
420
|
-
"- Edit only harness files and project harness docs that are necessary for the approved change.",
|
|
421
|
-
"- Do not edit product source code.",
|
|
422
|
-
"- Do not overwrite VCM fixed managed blocks; draft an issue instead if a fixed template is wrong.",
|
|
423
|
-
"- Stage the harness changes and create a commit yourself.",
|
|
424
|
-
"- Write the completion report with files changed, commit id if available, validation run, and any follow-up.",
|
|
425
|
-
"- End your turn after the report is written.",
|
|
426
|
-
...(comment.trim()
|
|
427
|
-
? ["", "<USER_APPROVAL_COMMENT>", comment.trim(), "</USER_APPROVAL_COMMENT>"]
|
|
428
|
-
: [])
|
|
429
|
-
].join("\n");
|
|
430
|
-
}
|
|
431
|
-
async function completeActive(repoRoot, state, outcome, comment = "") {
|
|
432
|
-
const completedDir = `${COMPLETED_DIR}/${state.active.id}`;
|
|
433
|
-
await deps.fs.ensureDir(resolveRepoPath(repoRoot, completedDir));
|
|
434
|
-
const feedbackContent = await readOptionalText(repoRoot, state.active.feedbackPath);
|
|
435
|
-
const analysisContent = await readOptionalText(repoRoot, state.active.analysisPath);
|
|
436
|
-
const applyReportContent = await readOptionalText(repoRoot, state.active.applyReportPath);
|
|
437
|
-
if (feedbackContent !== undefined) {
|
|
438
|
-
await deps.fs.writeText(resolveRepoPath(repoRoot, `${completedDir}/feedback.md`), feedbackContent);
|
|
439
|
-
}
|
|
440
|
-
if (analysisContent !== undefined) {
|
|
441
|
-
await deps.fs.writeText(resolveRepoPath(repoRoot, `${completedDir}/analysis.md`), analysisContent);
|
|
442
|
-
}
|
|
443
|
-
if (applyReportContent !== undefined) {
|
|
444
|
-
await deps.fs.writeText(resolveRepoPath(repoRoot, `${completedDir}/apply-report.md`), applyReportContent);
|
|
445
|
-
}
|
|
446
|
-
await deps.fs.writeJsonAtomic(resolveRepoPath(repoRoot, `${completedDir}/decision.json`), {
|
|
447
|
-
version: 1,
|
|
448
|
-
id: state.active.id,
|
|
449
|
-
title: state.active.title,
|
|
450
|
-
outcome,
|
|
451
|
-
comment,
|
|
452
|
-
completedAt: now()
|
|
453
|
-
});
|
|
454
|
-
await persistTaskRetrospectiveMarker(repoRoot, state.active, outcome === "applied" ? "completed" : outcome);
|
|
455
|
-
await deps.fs.removePath?.(resolveRepoPath(repoRoot, state.active.feedbackPath), { force: true });
|
|
456
|
-
await deps.fs.removePath?.(resolveRepoPath(repoRoot, path.posix.dirname(state.active.analysisPath)), { recursive: true, force: true });
|
|
457
|
-
}
|
|
458
|
-
async function loadStoredState(repoRoot) {
|
|
459
|
-
const statePath = resolveRepoPath(repoRoot, STATE_PATH);
|
|
460
|
-
if (!(await deps.fs.pathExists(statePath))) {
|
|
461
|
-
return undefined;
|
|
462
|
-
}
|
|
463
|
-
const state = await deps.fs.readJson(statePath);
|
|
464
|
-
if (state?.version !== 1 || !state.active?.id || !state.status) {
|
|
147
|
+
async function loadTaskRetrospectiveMarker(repoRoot, taskSlug) {
|
|
148
|
+
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(taskSlug));
|
|
149
|
+
if (!(await deps.fs.pathExists(markerPath))) {
|
|
465
150
|
return undefined;
|
|
466
151
|
}
|
|
467
|
-
|
|
468
|
-
return state;
|
|
152
|
+
return deps.fs.readJson(markerPath);
|
|
469
153
|
}
|
|
470
|
-
async function
|
|
471
|
-
|
|
154
|
+
async function persistTaskRetrospectiveMarker(repoRoot, marker) {
|
|
155
|
+
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(String(marker.taskSlug ?? "")));
|
|
156
|
+
await deps.fs.ensureDir(path.dirname(markerPath));
|
|
157
|
+
await deps.fs.writeJsonAtomic(markerPath, marker);
|
|
472
158
|
}
|
|
473
|
-
async function
|
|
474
|
-
|
|
159
|
+
async function cleanupLegacyState(repoRoot) {
|
|
160
|
+
const statePath = resolveRepoPath(repoRoot, LEGACY_STATE_PATH);
|
|
161
|
+
if (await deps.fs.pathExists(statePath)) {
|
|
162
|
+
await deps.fs.removePath?.(statePath, { force: true });
|
|
163
|
+
}
|
|
475
164
|
}
|
|
476
165
|
async function readOptionalText(repoRoot, relativePath) {
|
|
477
166
|
const absolutePath = resolveRepoPath(repoRoot, relativePath);
|
|
478
|
-
|
|
479
|
-
return undefined;
|
|
480
|
-
}
|
|
481
|
-
return deps.fs.readText(absolutePath);
|
|
167
|
+
return readAbsoluteOptionalText(absolutePath);
|
|
482
168
|
}
|
|
483
169
|
async function readAbsoluteOptionalText(absolutePath) {
|
|
484
170
|
if (!(await deps.fs.pathExists(absolutePath))) {
|
|
@@ -486,34 +172,9 @@ export function createHarnessFeedbackService(deps) {
|
|
|
486
172
|
}
|
|
487
173
|
return deps.fs.readText(absolutePath);
|
|
488
174
|
}
|
|
489
|
-
async function loadTaskRetrospectiveMarker(repoRoot, taskSlug) {
|
|
490
|
-
const markerPath = resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(taskSlug));
|
|
491
|
-
if (!(await deps.fs.pathExists(markerPath))) {
|
|
492
|
-
return undefined;
|
|
493
|
-
}
|
|
494
|
-
return deps.fs.readJson(markerPath);
|
|
495
|
-
}
|
|
496
|
-
async function persistTaskRetrospectiveMarker(repoRoot, active, status, timestamp = now()) {
|
|
497
|
-
if (active.source !== "task-retrospective" || !active.taskSlug) {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
await deps.fs.writeJsonAtomic(resolveRepoPath(repoRoot, getTaskRetrospectiveMarkerPath(active.taskSlug)), {
|
|
501
|
-
version: 1,
|
|
502
|
-
taskSlug: active.taskSlug,
|
|
503
|
-
activeId: active.id,
|
|
504
|
-
trigger: active.trigger ?? "manual",
|
|
505
|
-
status,
|
|
506
|
-
finalAcceptanceHash: active.finalAcceptanceHash,
|
|
507
|
-
createdAt: active.startedAt,
|
|
508
|
-
updatedAt: timestamp,
|
|
509
|
-
...(status === "completed" || status === "rejected" ? { completedAt: timestamp } : {})
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
175
|
return {
|
|
513
176
|
getState,
|
|
514
177
|
startTaskRetrospective,
|
|
515
|
-
decide,
|
|
516
|
-
recordHarnessEngineerHook,
|
|
517
178
|
assertHarnessEngineerAvailable
|
|
518
179
|
};
|
|
519
180
|
}
|
|
@@ -535,19 +196,6 @@ function firstHeading(content) {
|
|
|
535
196
|
function compactLine(value) {
|
|
536
197
|
return value.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
537
198
|
}
|
|
538
|
-
function renderTaskRetrospectiveFeedback(active, finalAcceptancePath) {
|
|
539
|
-
return [
|
|
540
|
-
`# ${active.title}`,
|
|
541
|
-
"",
|
|
542
|
-
`Source: ${active.source}`,
|
|
543
|
-
`Task slug: ${active.taskSlug ?? ""}`,
|
|
544
|
-
`Trigger: ${active.trigger ?? "manual"}`,
|
|
545
|
-
`Final acceptance: ${finalAcceptancePath}`,
|
|
546
|
-
`Final acceptance hash: ${active.finalAcceptanceHash ?? ""}`,
|
|
547
|
-
"",
|
|
548
|
-
"Summary: Review the completed task workflow for reusable harness problems."
|
|
549
|
-
].join("\n");
|
|
550
|
-
}
|
|
551
199
|
function getTaskRetrospectiveMarkerPath(taskSlug) {
|
|
552
200
|
return `${TASK_RETROSPECTIVE_DIR}/${sanitizeFeedbackId(taskSlug)}.json`;
|
|
553
201
|
}
|
|
@@ -558,6 +206,3 @@ function sanitizeFeedbackId(value) {
|
|
|
558
206
|
function sha256(content) {
|
|
559
207
|
return createHash("sha256").update(content).digest("hex");
|
|
560
208
|
}
|
|
561
|
-
export function getHarnessFeedbackRelativePath(repoRoot, absolutePath) {
|
|
562
|
-
return toRepoRelativePath(repoRoot, absolutePath);
|
|
563
|
-
}
|
|
@@ -6,7 +6,6 @@ const HARNESS_ENGINEER_SESSION_PATH = ".ai/vcm/harness-engineer/session.json";
|
|
|
6
6
|
const BOOTSTRAP_SESSION_PATH = ".ai/vcm/bootstrap/session.json";
|
|
7
7
|
const HARNESS_FEEDBACK_STATE_PATH = ".ai/vcm/harness-feedback/state.json";
|
|
8
8
|
const CODER_WORKERS_RUNTIME_DIR = ".ai/vcm/coder-workers";
|
|
9
|
-
const RECOVERABLE_FEEDBACK_STATES = new Set(["analyzing", "applying"]);
|
|
10
9
|
export function createRuntimeRecoveryService(deps) {
|
|
11
10
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
12
11
|
return {
|
|
@@ -20,7 +19,7 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
20
19
|
const config = await deps.projectService.loadConfig(repoRoot);
|
|
21
20
|
await runStep(context, "recover project tool sessions", () => recoverProjectToolSessions(repoRoot, recoveredAt, context));
|
|
22
21
|
await runStep(context, "recover harness bootstrap", () => recoverHarnessBootstrap(repoRoot, recoveredAt, context));
|
|
23
|
-
await runStep(context, "
|
|
22
|
+
await runStep(context, "cleanup legacy harness feedback state", () => cleanupLegacyHarnessFeedback(repoRoot, context));
|
|
24
23
|
const tasks = await deps.taskService.listTasks(repoRoot);
|
|
25
24
|
for (const task of tasks.filter((candidate) => candidate.cleanupStatus === "cleaned")) {
|
|
26
25
|
await runStep(context, `retry cleaned task ${task.taskSlug}`, async () => {
|
|
@@ -258,39 +257,13 @@ export function createRuntimeRecoveryService(deps) {
|
|
|
258
257
|
await deps.fs.removePath?.(absolutePath, { force: true });
|
|
259
258
|
context.changedPaths.add(BOOTSTRAP_SESSION_PATH);
|
|
260
259
|
}
|
|
261
|
-
async function
|
|
260
|
+
async function cleanupLegacyHarnessFeedback(repoRoot, context) {
|
|
262
261
|
const absolutePath = path.join(repoRoot, HARNESS_FEEDBACK_STATE_PATH);
|
|
263
|
-
|
|
264
|
-
if (!state?.status || !RECOVERABLE_FEEDBACK_STATES.has(state.status)) {
|
|
262
|
+
if (!(await deps.fs.pathExists(absolutePath))) {
|
|
265
263
|
return;
|
|
266
264
|
}
|
|
267
|
-
|
|
268
|
-
const next = {
|
|
269
|
-
...state,
|
|
270
|
-
status: "awaiting_user_approval",
|
|
271
|
-
updatedAt: timestamp,
|
|
272
|
-
active: state.active
|
|
273
|
-
? {
|
|
274
|
-
...state.active,
|
|
275
|
-
updatedAt: timestamp
|
|
276
|
-
}
|
|
277
|
-
: state.active
|
|
278
|
-
};
|
|
279
|
-
await deps.fs.writeJsonAtomic(absolutePath, next);
|
|
265
|
+
await deps.fs.removePath?.(absolutePath, { force: true });
|
|
280
266
|
context.changedPaths.add(HARNESS_FEEDBACK_STATE_PATH);
|
|
281
|
-
const analysisPath = state.active?.analysisPath;
|
|
282
|
-
if (analysisPath) {
|
|
283
|
-
const notePath = path.join(repoRoot, analysisPath);
|
|
284
|
-
if (!(await deps.fs.pathExists(notePath))) {
|
|
285
|
-
await deps.fs.writeText(notePath, [
|
|
286
|
-
"# Harness Feedback Recovery",
|
|
287
|
-
"",
|
|
288
|
-
`VCM restarted while this harness feedback item was ${previousStatus}.`,
|
|
289
|
-
"Review the current repository diff and either comment, reject, cancel, or approve explicitly."
|
|
290
|
-
].join("\n"));
|
|
291
|
-
context.changedPaths.add(analysisPath);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
267
|
}
|
|
295
268
|
function hasLiveTaskSession(taskSlug) {
|
|
296
269
|
return deps.runtime.listSessions(taskSlug).some((session) => session.status === "running");
|