patchwarden 1.5.0 → 1.5.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.en.md +43 -2
- package/README.md +43 -2
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +3 -3
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +2 -2
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +7 -0
- package/dist/config.js +34 -0
- package/dist/control/middleware/auth.d.ts +10 -0
- package/dist/control/middleware/auth.js +8 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +65 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +277 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +5 -0
- package/dist/control/routes/process.js +200 -0
- package/dist/control/routes/sessions.d.ts +22 -0
- package/dist/control/routes/sessions.js +224 -0
- package/dist/control/routes/status.d.ts +6 -0
- package/dist/control/routes/status.js +250 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +233 -0
- package/dist/control/routes/tasks.d.ts +20 -0
- package/dist/control/routes/tasks.js +310 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +193 -0
- package/dist/control/runtime.d.ts +91 -0
- package/dist/control/runtime.js +392 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +479 -0
- package/dist/control/shared.d.ts +35 -0
- package/dist/control/shared.js +288 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directGuards.js +30 -8
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +579 -348
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +204 -0
- package/dist/goal/goalStatus.d.ts +6 -0
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +220 -0
- package/dist/goal/subgoalSync.js +2 -1
- package/dist/httpServer.js +15 -12
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +8 -0
- package/dist/runner/changeCapture.d.ts +3 -3
- package/dist/runner/changeCapture.js +63 -39
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +26 -2
- package/dist/runner/runTask.js +245 -221
- package/dist/runner/simpleProcess.js +4 -4
- package/dist/runner/watch.js +17 -14
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +22 -0
- package/dist/smoke-test.js +257 -251
- package/dist/test/unit/apply-patch.test.d.ts +1 -0
- package/dist/test/unit/apply-patch.test.js +225 -0
- package/dist/test/unit/create-task.test.d.ts +1 -0
- package/dist/test/unit/create-task.test.js +197 -0
- package/dist/test/unit/direct-guards.test.js +124 -9
- package/dist/test/unit/evidence-pack.test.js +95 -1
- package/dist/test/unit/get-task-status.test.d.ts +1 -0
- package/dist/test/unit/get-task-status.test.js +174 -0
- package/dist/test/unit/get-task-summary.test.d.ts +1 -0
- package/dist/test/unit/get-task-summary.test.js +146 -0
- package/dist/test/unit/goal-report.test.d.ts +1 -0
- package/dist/test/unit/goal-report.test.js +159 -0
- package/dist/test/unit/goal-subgoal-task.test.js +6 -6
- package/dist/test/unit/goal-tools-registry.test.js +6 -4
- package/dist/test/unit/path-guard.test.js +24 -0
- package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
- package/dist/test/unit/spec-kit-import.test.js +341 -0
- package/dist/test/unit/wait-for-task.test.d.ts +1 -0
- package/dist/test/unit/wait-for-task.test.js +144 -0
- package/dist/tools/auditTask.d.ts +8 -63
- package/dist/tools/auditTask.js +12 -8
- package/dist/tools/createDirectSession.d.ts +1 -1
- package/dist/tools/createDirectSession.js +2 -2
- package/dist/tools/createTask.d.ts +2 -2
- package/dist/tools/createTask.js +4 -4
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +282 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +115 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +91 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +45 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/evidencePack.d.ts +6 -0
- package/dist/tools/evidencePack.js +213 -6
- package/dist/tools/finalizeDirectSession.d.ts +1 -1
- package/dist/tools/finalizeDirectSession.js +3 -3
- package/dist/tools/goalSubgoalTask.d.ts +1 -1
- package/dist/tools/goalSubgoalTask.js +2 -2
- package/dist/tools/healthCheck.js +3 -3
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +60 -503
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runTaskLoop.js +4 -4
- package/dist/tools/safeViews.d.ts +2 -2
- package/dist/tools/safeViews.js +2 -2
- package/dist/tools/toolRegistry.js +22 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/CODE_WIKI.md +983 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +63 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/execution-plan-2026-07-09.md +315 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +162 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-evidence.md +72 -0
- package/docs/roadmap-execution-and-acceptance.md +365 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +79 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/package.json +2 -2
- package/scripts/checks/control-center-smoke.js +356 -0
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-smoke.js +3 -1
- package/scripts/generate-demo-gif.py +320 -0
- package/src/assessments/agentAssessor.ts +3 -3
- package/src/assessments/assessmentStore.ts +2 -2
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +37 -0
- package/src/control/middleware/auth.ts +17 -0
- package/src/control/middleware/static.ts +71 -0
- package/src/control/routes/audit.ts +321 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +204 -0
- package/src/control/routes/sessions.ts +251 -0
- package/src/control/routes/status.ts +325 -0
- package/src/control/routes/taskActions.ts +248 -0
- package/src/control/routes/tasks.ts +323 -0
- package/src/control/routes/workspace.ts +203 -0
- package/src/control/runtime.ts +472 -0
- package/src/control/server.ts +471 -0
- package/src/control/shared.ts +294 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directGuards.ts +28 -7
- package/src/doctor.ts +741 -481
- package/src/goal/goalReport.ts +271 -0
- package/src/goal/goalStatus.ts +6 -0
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +4 -2
- package/src/httpServer.ts +17 -14
- package/src/index.ts +7 -4
- package/src/logging.ts +10 -1
- package/src/runner/changeCapture.ts +70 -42
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +26 -2
- package/src/runner/runTask.ts +325 -223
- package/src/runner/simpleProcess.ts +4 -4
- package/src/runner/watch.ts +17 -14
- package/src/security/contentRedaction.ts +29 -0
- package/src/smoke-test.ts +252 -250
- package/src/test/unit/apply-patch.test.ts +293 -0
- package/src/test/unit/create-task.test.ts +255 -0
- package/src/test/unit/direct-guards.test.ts +178 -8
- package/src/test/unit/evidence-pack.test.ts +110 -1
- package/src/test/unit/get-task-status.test.ts +203 -0
- package/src/test/unit/get-task-summary.test.ts +173 -0
- package/src/test/unit/goal-report.test.ts +189 -0
- package/src/test/unit/goal-subgoal-task.test.ts +6 -6
- package/src/test/unit/goal-tools-registry.test.ts +7 -5
- package/src/test/unit/path-guard.test.ts +30 -0
- package/src/test/unit/spec-kit-import.test.ts +429 -0
- package/src/test/unit/wait-for-task.test.ts +176 -0
- package/src/tools/auditTask.ts +99 -59
- package/src/tools/createDirectSession.ts +3 -3
- package/src/tools/createTask.ts +7 -7
- package/src/tools/dispatch/coreDispatch.ts +374 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
- package/src/tools/dispatch/directDispatch.ts +167 -0
- package/src/tools/dispatch/goalDispatch.ts +127 -0
- package/src/tools/dispatch/releaseDispatch.ts +65 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/evidencePack.ts +291 -6
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/registry.ts +68 -628
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runTaskLoop.ts +4 -4
- package/src/tools/safeViews.ts +2 -2
- package/src/tools/toolRegistry.ts +22 -0
- package/src/version.ts +1 -1
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +959 -44
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
package/dist/runner/runTask.js
CHANGED
|
@@ -17,6 +17,16 @@ const GRACEFUL_KILL_MS = 2000;
|
|
|
17
17
|
const MAX_CAPTURE_CHARS = 100_000;
|
|
18
18
|
const ARTIFACT_COLLECTION_TIMEOUT_MS = 60_000;
|
|
19
19
|
export async function runTask(taskId) {
|
|
20
|
+
const prepared = await prepareTask(taskId);
|
|
21
|
+
if (!("taskDir" in prepared))
|
|
22
|
+
return prepared;
|
|
23
|
+
const ctx = prepared;
|
|
24
|
+
const state = await executeAgent(ctx);
|
|
25
|
+
await runVerification(ctx, state);
|
|
26
|
+
const evidence = await collectArtifacts(ctx, state);
|
|
27
|
+
return finalizeTask(ctx, state, evidence);
|
|
28
|
+
}
|
|
29
|
+
async function prepareTask(taskId) {
|
|
20
30
|
const config = getConfig();
|
|
21
31
|
const tasksDir = getTasksDir(config);
|
|
22
32
|
const plansDir = getPlansDir(config);
|
|
@@ -57,11 +67,10 @@ export async function runTask(taskId) {
|
|
|
57
67
|
const message = `verification metadata validation failed: ${errorMessage(error)}`;
|
|
58
68
|
return failBeforeExecution(taskId, taskDir, message, error);
|
|
59
69
|
}
|
|
60
|
-
// ── Assessment freshness revalidation before execution ──
|
|
61
70
|
const assessmentId = String(initialStatus.assessment_id || "");
|
|
62
71
|
if (assessmentId) {
|
|
63
72
|
try {
|
|
64
|
-
const preExecSnapshot = captureRepoSnapshot(repoPath);
|
|
73
|
+
const preExecSnapshot = await captureRepoSnapshot(repoPath);
|
|
65
74
|
const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
|
|
66
75
|
if (!validation.valid) {
|
|
67
76
|
const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
|
|
@@ -82,10 +91,6 @@ export async function runTask(taskId) {
|
|
|
82
91
|
current_command: null,
|
|
83
92
|
error: null,
|
|
84
93
|
});
|
|
85
|
-
// v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
|
|
86
|
-
// can detect PID reuse and orphaned tasks. watcher_instance_id comes from
|
|
87
|
-
// the watcher process env; when run_task MCP tool is used directly, this
|
|
88
|
-
// field is intentionally left undefined so ownership cannot be falsely claimed.
|
|
89
94
|
writeTaskRuntime(taskDir, {
|
|
90
95
|
task_started_at: new Date(startedAtMs).toISOString(),
|
|
91
96
|
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
|
|
@@ -95,136 +100,151 @@ export async function runTask(taskId) {
|
|
|
95
100
|
let beforeWorkspaceSnapshot;
|
|
96
101
|
let externalDirtyBaseline = [];
|
|
97
102
|
try {
|
|
98
|
-
beforeSnapshot = captureRepoSnapshot(repoPath);
|
|
99
|
-
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
|
|
103
|
+
beforeSnapshot = await captureRepoSnapshot(repoPath);
|
|
104
|
+
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : await captureRepoSnapshot(wsRoot);
|
|
100
105
|
writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
|
|
101
106
|
writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
|
|
102
|
-
// Phase 4: Record external dirty files as baseline before task execution
|
|
103
107
|
externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
|
|
104
108
|
}
|
|
105
109
|
catch (error) {
|
|
106
110
|
const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
|
|
107
111
|
return failBeforeExecution(taskId, taskDir, message, error);
|
|
108
112
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
return {
|
|
114
|
+
taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
|
|
115
|
+
planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
|
|
116
|
+
startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function executeAgent(ctx) {
|
|
120
|
+
const state = {
|
|
121
|
+
agentResult: null,
|
|
122
|
+
testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
|
|
123
|
+
verifyResults: [],
|
|
124
|
+
finalStatus: "failed",
|
|
125
|
+
finalError: null,
|
|
126
|
+
lastCaughtError: null,
|
|
127
|
+
};
|
|
115
128
|
try {
|
|
116
|
-
const planFile = resolve(plansDir, planId, "plan.md");
|
|
129
|
+
const planFile = resolve(ctx.plansDir, ctx.planId, "plan.md");
|
|
117
130
|
if (!existsSync(planFile))
|
|
118
|
-
throw new Error(`Plan not found: "${planId}". Save the plan first.`);
|
|
131
|
+
throw new Error(`Plan not found: "${ctx.planId}". Save the plan first.`);
|
|
119
132
|
const planContent = readFileSync(planFile, "utf-8");
|
|
120
|
-
const prompt = buildExecutionPrompt(planContent, repoPath, testCommand);
|
|
121
|
-
const invocation = buildAgentInvocation(agentName, repoPath, prompt, config);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
agentResult = await runManagedProcess({
|
|
133
|
+
const prompt = buildExecutionPrompt(planContent, ctx.repoPath, ctx.testCommand);
|
|
134
|
+
const invocation = buildAgentInvocation(ctx.agentName, ctx.repoPath, prompt, ctx.config);
|
|
135
|
+
setTaskPhase(ctx.taskDir, "executing_agent", invocation.commandLabel);
|
|
136
|
+
state.agentResult = await runManagedProcess({
|
|
125
137
|
command: invocation.command,
|
|
126
138
|
args: invocation.args,
|
|
127
|
-
cwd: repoPath,
|
|
128
|
-
taskDir,
|
|
129
|
-
statusFile,
|
|
139
|
+
cwd: ctx.repoPath,
|
|
140
|
+
taskDir: ctx.taskDir,
|
|
141
|
+
statusFile: ctx.statusFile,
|
|
130
142
|
phase: "executing_agent",
|
|
131
|
-
currentCommand:
|
|
132
|
-
deadlineMs,
|
|
133
|
-
stdoutPath: join(taskDir, "stdout.log"),
|
|
134
|
-
stderrPath: join(taskDir, "stderr.log"),
|
|
143
|
+
currentCommand: invocation.commandLabel,
|
|
144
|
+
deadlineMs: ctx.deadlineMs,
|
|
145
|
+
stdoutPath: join(ctx.taskDir, "stdout.log"),
|
|
146
|
+
stderrPath: join(ctx.taskDir, "stderr.log"),
|
|
135
147
|
});
|
|
148
|
+
const agentResult = state.agentResult;
|
|
136
149
|
if (agentResult.terminationReason === "canceled" || agentResult.terminationReason === "killed") {
|
|
137
|
-
finalStatus = "canceled";
|
|
138
|
-
finalError = agentResult.terminationReason === "killed"
|
|
150
|
+
state.finalStatus = "canceled";
|
|
151
|
+
state.finalError = agentResult.terminationReason === "killed"
|
|
139
152
|
? "Task was terminated by kill_task."
|
|
140
153
|
: "Task was canceled by user request.";
|
|
141
154
|
}
|
|
142
155
|
else if (agentResult.terminationReason === "timeout") {
|
|
143
|
-
finalError = `Task timed out after ${timeoutSeconds} seconds during agent execution.`;
|
|
156
|
+
state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
|
|
144
157
|
}
|
|
145
158
|
else if (agentResult.spawnError) {
|
|
146
|
-
finalError = `Agent spawn failed: ${agentResult.spawnError}`;
|
|
159
|
+
state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
|
|
147
160
|
}
|
|
148
161
|
else if (agentResult.exitCode !== 0) {
|
|
149
|
-
finalError = `Agent exited with code ${agentResult.exitCode}.`;
|
|
162
|
+
state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
|
|
150
163
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
state.lastCaughtError = error;
|
|
167
|
+
state.finalError = errorMessage(error);
|
|
168
|
+
}
|
|
169
|
+
return state;
|
|
170
|
+
}
|
|
171
|
+
async function runVerification(ctx, state) {
|
|
172
|
+
if (state.finalError !== null)
|
|
173
|
+
return state;
|
|
174
|
+
try {
|
|
175
|
+
if (ctx.verifyCommands.length > 0) {
|
|
176
|
+
for (const command of ctx.verifyCommands) {
|
|
177
|
+
setTaskPhase(ctx.taskDir, "running_tests", command);
|
|
178
|
+
const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
|
|
179
|
+
state.verifyResults.push(verification);
|
|
180
|
+
if (verification.terminationReason || Date.now() >= ctx.deadlineMs)
|
|
157
181
|
break;
|
|
158
182
|
}
|
|
159
|
-
testResult = verifyResults[0] || skippedTest(testCommand, repoPath, "No verification command ran.");
|
|
160
|
-
const interrupted = verifyResults.find((result) => result.terminationReason);
|
|
161
|
-
const failedVerification = verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
|
|
183
|
+
state.testResult = state.verifyResults[0] || skippedTest(ctx.testCommand, ctx.repoPath, "No verification command ran.");
|
|
184
|
+
const interrupted = state.verifyResults.find((result) => result.terminationReason);
|
|
185
|
+
const failedVerification = state.verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
|
|
162
186
|
if (interrupted?.terminationReason === "canceled" || interrupted?.terminationReason === "killed") {
|
|
163
|
-
finalStatus = "canceled";
|
|
164
|
-
finalError = interrupted.terminationReason === "killed"
|
|
187
|
+
state.finalStatus = "canceled";
|
|
188
|
+
state.finalError = interrupted.terminationReason === "killed"
|
|
165
189
|
? "Task was terminated by kill_task during verification."
|
|
166
190
|
: "Task was canceled during verification.";
|
|
167
191
|
}
|
|
168
|
-
else if (interrupted?.terminationReason === "timeout" || Date.now() >= deadlineMs) {
|
|
169
|
-
finalError = `Task timed out after ${timeoutSeconds} seconds during verification.`;
|
|
192
|
+
else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
|
|
193
|
+
state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
|
|
170
194
|
}
|
|
171
195
|
else if (failedVerification) {
|
|
172
|
-
finalStatus = "failed_verification";
|
|
173
|
-
finalError = failedVerification.spawnError
|
|
174
|
-
? `Verification command
|
|
175
|
-
: `Verification command
|
|
196
|
+
state.finalStatus = "failed_verification";
|
|
197
|
+
state.finalError = failedVerification.spawnError
|
|
198
|
+
? `Verification command "${failedVerification.command}" could not start: ${failedVerification.spawnError}`
|
|
199
|
+
: `Verification command "${failedVerification.command}" exited with code ${failedVerification.exitCode}.`;
|
|
176
200
|
}
|
|
177
|
-
else if (verifyResults.length === verifyCommands.length) {
|
|
178
|
-
finalStatus = "done_by_agent";
|
|
201
|
+
else if (state.verifyResults.length === ctx.verifyCommands.length) {
|
|
202
|
+
state.finalStatus = "done_by_agent";
|
|
179
203
|
}
|
|
180
204
|
else {
|
|
181
|
-
finalError = "Verification did not complete all configured commands.";
|
|
205
|
+
state.finalError = "Verification did not complete all configured commands.";
|
|
182
206
|
}
|
|
183
207
|
}
|
|
184
208
|
else {
|
|
185
|
-
testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
|
|
186
|
-
finalStatus = "done_by_agent";
|
|
209
|
+
state.testResult = skippedTest(ctx.testCommand, ctx.repoPath, "No verification command configured.");
|
|
210
|
+
state.finalStatus = "done_by_agent";
|
|
187
211
|
}
|
|
188
212
|
}
|
|
189
213
|
catch (error) {
|
|
190
|
-
lastCaughtError = error;
|
|
191
|
-
finalError = errorMessage(error);
|
|
214
|
+
state.lastCaughtError = error;
|
|
215
|
+
state.finalError = errorMessage(error);
|
|
192
216
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
};
|
|
199
|
-
if (finalStatus !== "canceled") {
|
|
217
|
+
return state;
|
|
218
|
+
}
|
|
219
|
+
async function collectArtifacts(ctx, state) {
|
|
220
|
+
let cleanupReport = { enabled: true, removed: [], skipped: [], source_files_touched: 0 };
|
|
221
|
+
if (state.finalStatus !== "canceled") {
|
|
200
222
|
try {
|
|
201
|
-
cleanupReport = runPostTaskCleanup(repoPath, taskDir);
|
|
202
|
-
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
223
|
+
cleanupReport = runPostTaskCleanup(ctx.repoPath, ctx.taskDir);
|
|
224
|
+
updateStatus(ctx.taskDir, { cleanup: cleanupReport });
|
|
203
225
|
}
|
|
204
226
|
catch (error) {
|
|
205
227
|
cleanupReport = {
|
|
206
|
-
enabled: true,
|
|
207
|
-
removed: [],
|
|
228
|
+
enabled: true, removed: [],
|
|
208
229
|
skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
|
|
209
230
|
source_files_touched: 0,
|
|
210
231
|
};
|
|
211
|
-
writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
|
|
212
|
-
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
232
|
+
writeFileSync(join(ctx.taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
|
|
233
|
+
updateStatus(ctx.taskDir, { cleanup: cleanupReport });
|
|
213
234
|
}
|
|
214
235
|
}
|
|
215
|
-
setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
236
|
+
setTaskPhase(ctx.taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
216
237
|
const artifactCollectionStartedAt = new Date().toISOString();
|
|
217
238
|
let changes;
|
|
218
239
|
let artifactStatus = "collected";
|
|
219
240
|
let artifactCollectionError = null;
|
|
220
241
|
let timeoutHandle;
|
|
221
242
|
try {
|
|
222
|
-
// Phase 5: Wrap artifact collection with a timeout
|
|
223
243
|
const collectionResult = await Promise.race([
|
|
224
|
-
Promise.resolve().then(() => {
|
|
225
|
-
const afterSnapshot = captureRepoSnapshot(repoPath);
|
|
226
|
-
writeSnapshot(taskDir, "git-after.json", afterSnapshot);
|
|
227
|
-
return buildChangeArtifacts(repoPath, beforeSnapshot, afterSnapshot);
|
|
244
|
+
Promise.resolve().then(async () => {
|
|
245
|
+
const afterSnapshot = await captureRepoSnapshot(ctx.repoPath);
|
|
246
|
+
writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
|
|
247
|
+
return await buildChangeArtifacts(ctx.repoPath, ctx.beforeSnapshot, afterSnapshot);
|
|
228
248
|
}),
|
|
229
249
|
new Promise((_, reject) => {
|
|
230
250
|
timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
|
|
@@ -237,14 +257,9 @@ export async function runTask(taskId) {
|
|
|
237
257
|
catch (error) {
|
|
238
258
|
if (timeoutHandle)
|
|
239
259
|
clearTimeout(timeoutHandle);
|
|
240
|
-
lastCaughtError = error;
|
|
260
|
+
state.lastCaughtError = error;
|
|
241
261
|
artifactCollectionError = errorMessage(error);
|
|
242
|
-
|
|
243
|
-
artifactStatus = "timeout";
|
|
244
|
-
}
|
|
245
|
-
else {
|
|
246
|
-
artifactStatus = "failed";
|
|
247
|
-
}
|
|
262
|
+
artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
|
|
248
263
|
changes = {
|
|
249
264
|
changed_files: [],
|
|
250
265
|
diff: `(change capture failed: ${artifactCollectionError})\n`,
|
|
@@ -254,17 +269,16 @@ export async function runTask(taskId) {
|
|
|
254
269
|
additions: 0,
|
|
255
270
|
deletions: 0,
|
|
256
271
|
file_stats: [],
|
|
257
|
-
workspace_dirty_before: beforeSnapshot.workspace_dirty,
|
|
258
|
-
workspace_dirty_after: beforeSnapshot.workspace_dirty,
|
|
272
|
+
workspace_dirty_before: ctx.beforeSnapshot.workspace_dirty,
|
|
273
|
+
workspace_dirty_after: ctx.beforeSnapshot.workspace_dirty,
|
|
259
274
|
patch_mode: "hash_only",
|
|
260
275
|
unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
|
|
261
276
|
artifact_hygiene: emptyArtifactHygiene(),
|
|
262
277
|
};
|
|
263
|
-
|
|
264
|
-
writeFileSync(join(taskDir, "partial_result.md"), [
|
|
278
|
+
writeFileSync(join(ctx.taskDir, "partial_result.md"), [
|
|
265
279
|
"# PatchWarden Partial Result",
|
|
266
280
|
"",
|
|
267
|
-
`Task: ${taskId}`,
|
|
281
|
+
`Task: ${ctx.taskId}`,
|
|
268
282
|
"",
|
|
269
283
|
"## Artifact Collection Status",
|
|
270
284
|
artifactStatus,
|
|
@@ -273,62 +287,79 @@ export async function runTask(taskId) {
|
|
|
273
287
|
artifactCollectionError,
|
|
274
288
|
"",
|
|
275
289
|
"## Agent Status",
|
|
276
|
-
agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
|
|
290
|
+
state.agentResult ? `Exit code: ${state.agentResult.exitCode}` : "Agent did not run",
|
|
277
291
|
"",
|
|
278
292
|
"## Verification Status",
|
|
279
|
-
verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
293
|
+
state.verifyResults.length > 0 ? state.verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
280
294
|
"",
|
|
281
295
|
"## Note",
|
|
282
296
|
"Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
|
|
283
297
|
"",
|
|
284
298
|
].join("\n"), "utf-8");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
finalStatus = "failed";
|
|
299
|
+
if (state.finalStatus === "done_by_agent") {
|
|
300
|
+
state.finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
301
|
+
state.finalStatus = "failed";
|
|
289
302
|
}
|
|
290
303
|
else {
|
|
291
|
-
finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
304
|
+
state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
292
305
|
}
|
|
293
306
|
}
|
|
294
307
|
const artifactCollectionFinishedAt = new Date().toISOString();
|
|
295
308
|
let outOfScopeChanges = [];
|
|
296
|
-
|
|
309
|
+
const preexistingExternalDirty = ctx.externalDirtyBaseline;
|
|
297
310
|
let newOutOfScopeChanges = [];
|
|
298
311
|
try {
|
|
299
|
-
const afterWorkspaceSnapshot = repoPath === wsRoot ? captureRepoSnapshot(repoPath) : captureRepoSnapshot(wsRoot);
|
|
300
|
-
writeSnapshot(taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
301
|
-
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, repoPath, wsRoot);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
Boolean(file.old_path && !isPathInside(resolve(wsRoot, file.old_path), repoPath)));
|
|
312
|
+
const afterWorkspaceSnapshot = ctx.repoPath === ctx.wsRoot ? await captureRepoSnapshot(ctx.repoPath) : await captureRepoSnapshot(ctx.wsRoot);
|
|
313
|
+
writeSnapshot(ctx.taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
314
|
+
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, ctx.repoPath, ctx.wsRoot);
|
|
315
|
+
newOutOfScopeChanges = findNewExternalDirtyFiles(ctx.externalDirtyBaseline, allExternalDirty);
|
|
316
|
+
outOfScopeChanges = compareSnapshots(ctx.beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
|
|
317
|
+
.filter((file) => !isPathInside(resolve(ctx.wsRoot, file.path), ctx.repoPath) ||
|
|
318
|
+
Boolean(file.old_path && !isPathInside(resolve(ctx.wsRoot, file.old_path), ctx.repoPath)));
|
|
307
319
|
}
|
|
308
320
|
catch (error) {
|
|
309
|
-
finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
310
|
-
if (finalStatus === "done_by_agent")
|
|
311
|
-
finalStatus = "failed";
|
|
321
|
+
state.finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
322
|
+
if (state.finalStatus === "done_by_agent")
|
|
323
|
+
state.finalStatus = "failed";
|
|
312
324
|
}
|
|
313
|
-
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
314
325
|
const preexistingWarnings = preexistingExternalDirty.length > 0
|
|
315
326
|
? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
|
|
316
327
|
: [];
|
|
328
|
+
applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
|
|
329
|
+
writeFileSync(join(ctx.taskDir, "git.diff"), changes.diff, "utf-8");
|
|
330
|
+
writeFileSync(join(ctx.taskDir, "diff.patch"), changes.diff, "utf-8");
|
|
331
|
+
writeFileSync(join(ctx.taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
|
|
332
|
+
const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
|
|
333
|
+
writeFileSync(join(ctx.taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
|
|
334
|
+
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
335
|
+
writeFileSync(join(ctx.taskDir, "file-stats.json"), JSON.stringify({
|
|
336
|
+
task_id: ctx.taskId,
|
|
337
|
+
additions: changes.additions,
|
|
338
|
+
deletions: changes.deletions,
|
|
339
|
+
files: changes.file_stats,
|
|
340
|
+
}, null, 2), "utf-8");
|
|
341
|
+
return {
|
|
342
|
+
changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
|
|
343
|
+
outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
|
|
344
|
+
artifactManifest, changedFileGroups,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty) {
|
|
317
348
|
if (newOutOfScopeChanges.length > 0) {
|
|
318
|
-
finalStatus = "failed_scope_violation";
|
|
319
|
-
finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
320
|
-
writeFileSync(join(taskDir, "rollback-plan.json"), JSON.stringify({
|
|
321
|
-
task_id: taskId,
|
|
349
|
+
state.finalStatus = "failed_scope_violation";
|
|
350
|
+
state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
351
|
+
writeFileSync(join(ctx.taskDir, "rollback-plan.json"), JSON.stringify({
|
|
352
|
+
task_id: ctx.taskId,
|
|
322
353
|
status: "review_required",
|
|
323
354
|
automatic_rollback_performed: false,
|
|
324
355
|
warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
|
|
325
356
|
out_of_scope_changes: newOutOfScopeChanges,
|
|
326
357
|
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
327
358
|
}, null, 2), "utf-8");
|
|
328
|
-
writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
|
|
359
|
+
writeFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
|
|
329
360
|
"# Scope Violation Rollback Plan",
|
|
330
361
|
"",
|
|
331
|
-
`Task: ${taskId}`,
|
|
362
|
+
`Task: ${ctx.taskId}`,
|
|
332
363
|
"",
|
|
333
364
|
"PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
|
|
334
365
|
"",
|
|
@@ -337,44 +368,30 @@ export async function runTask(taskId) {
|
|
|
337
368
|
"",
|
|
338
369
|
].join("\n"), "utf-8");
|
|
339
370
|
}
|
|
340
|
-
else if (changePolicy === "no_changes" && changes.changed_files.length > 0) {
|
|
341
|
-
finalStatus = "failed_policy_violation";
|
|
342
|
-
finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
|
|
371
|
+
else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
|
|
372
|
+
state.finalStatus = "failed_policy_violation";
|
|
373
|
+
state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
|
|
343
374
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
writeFileSync(join(taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
|
|
350
|
-
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
351
|
-
writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
|
|
352
|
-
task_id: taskId,
|
|
353
|
-
additions: changes.additions,
|
|
354
|
-
deletions: changes.deletions,
|
|
355
|
-
files: changes.file_stats,
|
|
356
|
-
}, null, 2), "utf-8");
|
|
357
|
-
writeFileSync(join(taskDir, "test.log"), buildTestLog(testResult), "utf-8");
|
|
358
|
-
const verifyJson = buildVerifyJson(verifyCommands, verifyResults, repoPath);
|
|
359
|
-
// Phase 4: Use newOutOfScopeChanges (not outOfScopeChanges) for verify_status
|
|
360
|
-
// to stay consistent with finalStatus. Pre-existing external dirty files
|
|
361
|
-
// that didn't change during the task should NOT cause verify_status to fail.
|
|
375
|
+
}
|
|
376
|
+
function finalizeTask(ctx, state, evidence) {
|
|
377
|
+
const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport, artifactManifest, changedFileGroups } = evidence;
|
|
378
|
+
writeFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult), "utf-8");
|
|
379
|
+
const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
|
|
362
380
|
if (newOutOfScopeChanges.length > 0) {
|
|
363
381
|
verifyJson.status = "failed";
|
|
364
382
|
verifyJson.failure_reason = "scope_violation";
|
|
365
383
|
}
|
|
366
|
-
else if (finalStatus === "failed_policy_violation") {
|
|
384
|
+
else if (state.finalStatus === "failed_policy_violation") {
|
|
367
385
|
verifyJson.status = "failed";
|
|
368
386
|
verifyJson.failure_reason = "change_policy_violation";
|
|
369
387
|
}
|
|
370
|
-
writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
|
|
371
|
-
writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
|
|
372
|
-
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus))
|
|
373
|
-
finalStatus = "failed";
|
|
374
|
-
const finalPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
|
|
375
|
-
const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
|
|
376
|
-
|
|
377
|
-
const androidDiagnostic = diagnoseAndroidBuild(repoPath);
|
|
388
|
+
writeFileSync(join(ctx.taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
|
|
389
|
+
writeFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
|
|
390
|
+
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus))
|
|
391
|
+
state.finalStatus = "failed";
|
|
392
|
+
const finalPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : state.finalStatus;
|
|
393
|
+
const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
|
|
394
|
+
const androidDiagnostic = diagnoseAndroidBuild(ctx.repoPath);
|
|
378
395
|
let androidWarning = null;
|
|
379
396
|
if (androidDiagnostic.status !== "skip") {
|
|
380
397
|
if (androidDiagnostic.status === "fail") {
|
|
@@ -388,13 +405,13 @@ export async function runTask(taskId) {
|
|
|
388
405
|
}
|
|
389
406
|
}
|
|
390
407
|
const resultMd = buildResultMarkdown({
|
|
391
|
-
taskId,
|
|
392
|
-
planId,
|
|
393
|
-
agent: agentName,
|
|
394
|
-
status: finalStatus,
|
|
395
|
-
error: finalError,
|
|
396
|
-
agentResult,
|
|
397
|
-
testResult,
|
|
408
|
+
taskId: ctx.taskId,
|
|
409
|
+
planId: ctx.planId,
|
|
410
|
+
agent: ctx.agentName,
|
|
411
|
+
status: state.finalStatus,
|
|
412
|
+
error: state.finalError,
|
|
413
|
+
agentResult: state.agentResult,
|
|
414
|
+
testResult: state.testResult,
|
|
398
415
|
verify: verifyJson,
|
|
399
416
|
changes,
|
|
400
417
|
outOfScopeChanges,
|
|
@@ -405,18 +422,72 @@ export async function runTask(taskId) {
|
|
|
405
422
|
androidWarning,
|
|
406
423
|
preexistingWarnings,
|
|
407
424
|
});
|
|
408
|
-
writeFileSync(join(taskDir, "result.md"), resultMd, "utf-8");
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
425
|
+
writeFileSync(join(ctx.taskDir, "result.md"), resultMd, "utf-8");
|
|
426
|
+
const resultJson = buildResultJson({
|
|
427
|
+
ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
|
|
428
|
+
});
|
|
429
|
+
writeFileSync(join(ctx.taskDir, "result.json"), JSON.stringify(resultJson, null, 2), "utf-8");
|
|
430
|
+
if (state.finalError) {
|
|
431
|
+
const structuredError = errorPayload(state.lastCaughtError);
|
|
432
|
+
writeFileSync(join(ctx.taskDir, "error.log"), JSON.stringify({
|
|
433
|
+
summary: state.finalError,
|
|
434
|
+
...structuredError,
|
|
435
|
+
}, null, 2), "utf-8");
|
|
436
|
+
}
|
|
437
|
+
const finishedAt = new Date().toISOString();
|
|
438
|
+
updateStatus(ctx.taskDir, {
|
|
439
|
+
status: state.finalStatus,
|
|
440
|
+
phase: finalPhase,
|
|
441
|
+
current_command: null,
|
|
442
|
+
last_heartbeat_at: finishedAt,
|
|
443
|
+
finished_at: finishedAt,
|
|
444
|
+
error: state.finalError,
|
|
445
|
+
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
446
|
+
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
447
|
+
artifact_status: artifactStatus,
|
|
448
|
+
artifact_collection_error: artifactCollectionError,
|
|
449
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
450
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
451
|
+
cleanup: cleanupReport,
|
|
452
|
+
out_of_scope_changes: outOfScopeChanges,
|
|
453
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
454
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
455
|
+
verify_status: verifyJson.status,
|
|
456
|
+
verify_commands: ctx.verifyCommands,
|
|
457
|
+
diff_available: changes.diff_available,
|
|
458
|
+
diff_truncated: changes.diff_truncated,
|
|
459
|
+
workspace_dirty_before: changes.workspace_dirty_before,
|
|
460
|
+
workspace_dirty_after: changes.workspace_dirty_after,
|
|
461
|
+
workspace_dirty: changes.workspace_dirty_after,
|
|
462
|
+
acceptance_status: state.finalStatus === "done_by_agent" ? "pending" : null,
|
|
463
|
+
});
|
|
464
|
+
writeTaskRuntime(ctx.taskDir, {
|
|
465
|
+
phase: finalPhase,
|
|
466
|
+
current_command: null,
|
|
467
|
+
last_heartbeat_at: finishedAt,
|
|
468
|
+
runner_pid: process.pid,
|
|
469
|
+
child_pid: undefined,
|
|
470
|
+
});
|
|
471
|
+
writeTaskProgress(ctx.taskDir, finalPhase, {
|
|
472
|
+
heartbeatAt: finishedAt,
|
|
473
|
+
note: state.finalError || `Task finished with status ${state.finalStatus}.`,
|
|
474
|
+
});
|
|
475
|
+
return { task_id: ctx.taskId, status: state.finalStatus, error: state.finalError };
|
|
476
|
+
}
|
|
477
|
+
function buildResultJson(input) {
|
|
478
|
+
const { ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning } = input;
|
|
479
|
+
const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
|
|
480
|
+
return {
|
|
481
|
+
task_id: ctx.taskId,
|
|
482
|
+
status: state.finalStatus,
|
|
483
|
+
agent: ctx.agentName,
|
|
484
|
+
workspace_root: ctx.wsRoot,
|
|
485
|
+
repo_path: ctx.initialStatus.repo_path,
|
|
486
|
+
resolved_repo_path: ctx.repoPath,
|
|
487
|
+
plan_source: ctx.initialStatus.plan_source || "saved",
|
|
488
|
+
template: ctx.initialStatus.template || null,
|
|
489
|
+
change_policy: ctx.changePolicy,
|
|
490
|
+
summary: state.finalError || "Agent execution and configured verification completed successfully.",
|
|
420
491
|
changed_files: changes.changed_files,
|
|
421
492
|
changed_file_groups: {
|
|
422
493
|
source_changes: changedFileGroups.source_changes.length,
|
|
@@ -454,68 +525,21 @@ export async function runTask(taskId) {
|
|
|
454
525
|
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
455
526
|
],
|
|
456
527
|
warnings: [
|
|
457
|
-
...beforeSnapshot.warnings,
|
|
528
|
+
...ctx.beforeSnapshot.warnings,
|
|
458
529
|
...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
|
|
459
|
-
...preexistingWarnings,
|
|
530
|
+
...evidence.preexistingWarnings,
|
|
460
531
|
...(androidWarning ? [androidWarning] : []),
|
|
461
532
|
],
|
|
462
|
-
errors: finalError ? [finalError] : [],
|
|
463
|
-
known_issues: finalError ? [finalError] : [],
|
|
533
|
+
errors: state.finalError ? [state.finalError] : [],
|
|
534
|
+
known_issues: state.finalError ? [state.finalError] : [],
|
|
464
535
|
failure_reason: followup.failure_reason,
|
|
465
536
|
failed_command: followup.failed_command,
|
|
466
537
|
suggested_next_action: followup.suggested_next_action,
|
|
467
538
|
safe_followup_prompt: followup.safe_followup_prompt,
|
|
468
|
-
next_steps: finalStatus === "done_by_agent"
|
|
539
|
+
next_steps: state.finalStatus === "done_by_agent"
|
|
469
540
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
470
541
|
: ["Resolve the reported failure before accepting the work."],
|
|
471
|
-
}
|
|
472
|
-
if (finalError) {
|
|
473
|
-
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
474
|
-
const structuredError = errorPayload(lastCaughtError);
|
|
475
|
-
writeFileSync(join(taskDir, "error.log"), JSON.stringify({
|
|
476
|
-
summary: finalError,
|
|
477
|
-
...structuredError,
|
|
478
|
-
}, null, 2), "utf-8");
|
|
479
|
-
}
|
|
480
|
-
const finishedAt = new Date().toISOString();
|
|
481
|
-
updateStatus(taskDir, {
|
|
482
|
-
status: finalStatus,
|
|
483
|
-
phase: finalPhase,
|
|
484
|
-
current_command: null,
|
|
485
|
-
last_heartbeat_at: finishedAt,
|
|
486
|
-
finished_at: finishedAt,
|
|
487
|
-
error: finalError,
|
|
488
|
-
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
489
|
-
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
490
|
-
artifact_status: artifactStatus,
|
|
491
|
-
artifact_collection_error: artifactCollectionError,
|
|
492
|
-
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
493
|
-
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
494
|
-
cleanup: cleanupReport,
|
|
495
|
-
out_of_scope_changes: outOfScopeChanges,
|
|
496
|
-
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
497
|
-
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
498
|
-
verify_status: verifyJson.status,
|
|
499
|
-
verify_commands: verifyCommands,
|
|
500
|
-
diff_available: changes.diff_available,
|
|
501
|
-
diff_truncated: changes.diff_truncated,
|
|
502
|
-
workspace_dirty_before: changes.workspace_dirty_before,
|
|
503
|
-
workspace_dirty_after: changes.workspace_dirty_after,
|
|
504
|
-
workspace_dirty: changes.workspace_dirty_after,
|
|
505
|
-
acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
|
|
506
|
-
});
|
|
507
|
-
writeTaskRuntime(taskDir, {
|
|
508
|
-
phase: finalPhase,
|
|
509
|
-
current_command: null,
|
|
510
|
-
last_heartbeat_at: finishedAt,
|
|
511
|
-
runner_pid: process.pid,
|
|
512
|
-
child_pid: undefined,
|
|
513
|
-
});
|
|
514
|
-
writeTaskProgress(taskDir, finalPhase, {
|
|
515
|
-
heartbeatAt: finishedAt,
|
|
516
|
-
note: finalError || `Task finished with status ${finalStatus}.`,
|
|
517
|
-
});
|
|
518
|
-
return { task_id: taskId, status: finalStatus, error: finalError };
|
|
542
|
+
};
|
|
519
543
|
}
|
|
520
544
|
function buildFailureFollowup(status, error, commands) {
|
|
521
545
|
const failed = commands.find((command) => command.status !== "passed");
|
|
@@ -643,11 +667,11 @@ async function runManagedProcess(options) {
|
|
|
643
667
|
try {
|
|
644
668
|
forceKill(child);
|
|
645
669
|
}
|
|
646
|
-
catch { }
|
|
670
|
+
catch { } // cleanup failure is safe to ignore
|
|
647
671
|
try {
|
|
648
672
|
child.kill("SIGKILL");
|
|
649
673
|
}
|
|
650
|
-
catch { }
|
|
674
|
+
catch { } // cleanup failure is safe to ignore
|
|
651
675
|
// Resolve the exit promise via the shared finish handle
|
|
652
676
|
if (childExitFinish)
|
|
653
677
|
childExitFinish(null);
|
|
@@ -1027,7 +1051,7 @@ function gracefulKill(child) {
|
|
|
1027
1051
|
else
|
|
1028
1052
|
child.kill("SIGTERM");
|
|
1029
1053
|
}
|
|
1030
|
-
catch { }
|
|
1054
|
+
catch { } // cleanup failure is safe to ignore
|
|
1031
1055
|
}
|
|
1032
1056
|
function forceKill(child) {
|
|
1033
1057
|
if (!child.pid)
|
|
@@ -1050,7 +1074,7 @@ function forceKill(child) {
|
|
|
1050
1074
|
try {
|
|
1051
1075
|
child.kill("SIGKILL");
|
|
1052
1076
|
}
|
|
1053
|
-
catch { }
|
|
1077
|
+
catch { } // cleanup failure is safe to ignore
|
|
1054
1078
|
}
|
|
1055
1079
|
}
|
|
1056
1080
|
function openStream(path) {
|