patchwarden 0.4.0 → 0.6.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/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
package/dist/runner/runTask.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { createWriteStream, existsSync, readFileSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { getTasksDir, getPlansDir, getConfig, resolveWorkspaceRoot } from "../config.js";
|
|
5
5
|
import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
6
|
-
import {
|
|
6
|
+
import { guardTestCommand, } from "../security/commandGuard.js";
|
|
7
7
|
import { writeTaskProgress } from "../taskProgress.js";
|
|
8
8
|
import { writeTaskRuntime } from "../taskRuntime.js";
|
|
9
|
-
import {
|
|
9
|
+
import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
|
|
10
|
+
import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js";
|
|
11
|
+
import { buildChangeArtifacts, captureRepoSnapshot, compareSnapshots, emptyArtifactHygiene, writeSnapshot, extractExternalDirtyFiles, findNewExternalDirtyFiles, buildArtifactManifest, groupChangedFiles, } from "./changeCapture.js";
|
|
12
|
+
import { errorPayload } from "../errors.js";
|
|
13
|
+
import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
|
|
10
14
|
const HEARTBEAT_INTERVAL_MS = 2000;
|
|
11
15
|
const GRACEFUL_KILL_MS = 2000;
|
|
12
16
|
const MAX_CAPTURE_CHARS = 100_000;
|
|
17
|
+
const ARTIFACT_COLLECTION_TIMEOUT_MS = 60_000;
|
|
13
18
|
export async function runTask(taskId) {
|
|
14
19
|
const config = getConfig();
|
|
15
20
|
const tasksDir = getTasksDir(config);
|
|
@@ -29,11 +34,10 @@ export async function runTask(taskId) {
|
|
|
29
34
|
let verifyCommands;
|
|
30
35
|
let timeoutSeconds;
|
|
31
36
|
try {
|
|
32
|
-
verifyCommands = normalizeVerifyCommands(initialStatus.verify_commands, testCommand, config);
|
|
33
37
|
timeoutSeconds = normalizeTimeout(initialStatus.timeout_seconds, config);
|
|
34
38
|
}
|
|
35
39
|
catch (error) {
|
|
36
|
-
return failBeforeExecution(taskId, taskDir, errorMessage(error));
|
|
40
|
+
return failBeforeExecution(taskId, taskDir, errorMessage(error), error);
|
|
37
41
|
}
|
|
38
42
|
const startedAtMs = Date.now();
|
|
39
43
|
const deadlineMs = startedAtMs + timeoutSeconds * 1000;
|
|
@@ -43,7 +47,30 @@ export async function runTask(taskId) {
|
|
|
43
47
|
}
|
|
44
48
|
catch (error) {
|
|
45
49
|
const message = `repo_path validation failed: ${errorMessage(error)}`;
|
|
46
|
-
return failBeforeExecution(taskId, taskDir, message);
|
|
50
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
verifyCommands = normalizeVerifyCommands(initialStatus.verify_commands, testCommand, config, repoPath);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const message = `verification metadata validation failed: ${errorMessage(error)}`;
|
|
57
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
58
|
+
}
|
|
59
|
+
// ── Assessment freshness revalidation before execution ──
|
|
60
|
+
const assessmentId = String(initialStatus.assessment_id || "");
|
|
61
|
+
if (assessmentId) {
|
|
62
|
+
try {
|
|
63
|
+
const preExecSnapshot = captureRepoSnapshot(repoPath);
|
|
64
|
+
const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
|
|
67
|
+
return failBeforeExecution(taskId, taskDir, message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const message = `assessment validation error: ${errorMessage(error)}`;
|
|
72
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
73
|
+
}
|
|
47
74
|
}
|
|
48
75
|
updateStatus(taskDir, {
|
|
49
76
|
status: "running",
|
|
@@ -57,40 +84,37 @@ export async function runTask(taskId) {
|
|
|
57
84
|
setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
|
|
58
85
|
let beforeSnapshot;
|
|
59
86
|
let beforeWorkspaceSnapshot;
|
|
87
|
+
let externalDirtyBaseline = [];
|
|
60
88
|
try {
|
|
61
89
|
beforeSnapshot = captureRepoSnapshot(repoPath);
|
|
62
90
|
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
|
|
63
91
|
writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
|
|
64
92
|
writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
|
|
93
|
+
// Phase 4: Record external dirty files as baseline before task execution
|
|
94
|
+
externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
|
|
65
95
|
}
|
|
66
96
|
catch (error) {
|
|
67
97
|
const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
|
|
68
|
-
return failBeforeExecution(taskId, taskDir, message);
|
|
98
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
69
99
|
}
|
|
70
100
|
let agentResult = null;
|
|
71
101
|
let testResult = skippedTest(testCommand, repoPath, "Agent did not complete successfully.");
|
|
72
102
|
const verifyResults = [];
|
|
73
103
|
let finalStatus = "failed";
|
|
74
104
|
let finalError = null;
|
|
105
|
+
let lastCaughtError = null;
|
|
75
106
|
try {
|
|
76
107
|
const planFile = resolve(plansDir, planId, "plan.md");
|
|
77
108
|
if (!existsSync(planFile))
|
|
78
109
|
throw new Error(`Plan not found: "${planId}". Save the plan first.`);
|
|
79
110
|
const planContent = readFileSync(planFile, "utf-8");
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
if (arg === "{repo}")
|
|
84
|
-
return repoPath;
|
|
85
|
-
if (arg === "{prompt}")
|
|
86
|
-
return prompt;
|
|
87
|
-
return arg;
|
|
88
|
-
});
|
|
89
|
-
const agentCommandLabel = `${basename(agentCmd.command)} (configured agent command)`;
|
|
111
|
+
const prompt = buildExecutionPrompt(planContent, repoPath, testCommand);
|
|
112
|
+
const invocation = buildAgentInvocation(agentName, repoPath, prompt, config);
|
|
113
|
+
const agentCommandLabel = invocation.commandLabel;
|
|
90
114
|
setTaskPhase(taskDir, "executing_agent", agentCommandLabel);
|
|
91
115
|
agentResult = await runManagedProcess({
|
|
92
|
-
command:
|
|
93
|
-
args:
|
|
116
|
+
command: invocation.command,
|
|
117
|
+
args: invocation.args,
|
|
94
118
|
cwd: repoPath,
|
|
95
119
|
taskDir,
|
|
96
120
|
statusFile,
|
|
@@ -154,19 +178,45 @@ export async function runTask(taskId) {
|
|
|
154
178
|
}
|
|
155
179
|
}
|
|
156
180
|
catch (error) {
|
|
181
|
+
lastCaughtError = error;
|
|
157
182
|
finalError = errorMessage(error);
|
|
158
183
|
}
|
|
159
184
|
setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
185
|
+
const artifactCollectionStartedAt = new Date().toISOString();
|
|
160
186
|
let changes;
|
|
187
|
+
let artifactStatus = "collected";
|
|
188
|
+
let artifactCollectionError = null;
|
|
189
|
+
let timeoutHandle;
|
|
161
190
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
191
|
+
// Phase 5: Wrap artifact collection with a timeout
|
|
192
|
+
const collectionResult = await Promise.race([
|
|
193
|
+
Promise.resolve().then(() => {
|
|
194
|
+
const afterSnapshot = captureRepoSnapshot(repoPath);
|
|
195
|
+
writeSnapshot(taskDir, "git-after.json", afterSnapshot);
|
|
196
|
+
return buildChangeArtifacts(repoPath, beforeSnapshot, afterSnapshot);
|
|
197
|
+
}),
|
|
198
|
+
new Promise((_, reject) => {
|
|
199
|
+
timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
|
|
200
|
+
}),
|
|
201
|
+
]);
|
|
202
|
+
if (timeoutHandle)
|
|
203
|
+
clearTimeout(timeoutHandle);
|
|
204
|
+
changes = collectionResult;
|
|
165
205
|
}
|
|
166
206
|
catch (error) {
|
|
207
|
+
if (timeoutHandle)
|
|
208
|
+
clearTimeout(timeoutHandle);
|
|
209
|
+
lastCaughtError = error;
|
|
210
|
+
artifactCollectionError = errorMessage(error);
|
|
211
|
+
if (artifactCollectionError.includes("timed out")) {
|
|
212
|
+
artifactStatus = "timeout";
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
artifactStatus = "failed";
|
|
216
|
+
}
|
|
167
217
|
changes = {
|
|
168
218
|
changed_files: [],
|
|
169
|
-
diff: `(change capture failed: ${
|
|
219
|
+
diff: `(change capture failed: ${artifactCollectionError})\n`,
|
|
170
220
|
diff_available: false,
|
|
171
221
|
diff_truncated: false,
|
|
172
222
|
diff_size_bytes: 0,
|
|
@@ -176,15 +226,50 @@ export async function runTask(taskId) {
|
|
|
176
226
|
workspace_dirty_before: beforeSnapshot.workspace_dirty,
|
|
177
227
|
workspace_dirty_after: beforeSnapshot.workspace_dirty,
|
|
178
228
|
patch_mode: "hash_only",
|
|
179
|
-
unavailable_reason: `Change capture failed: ${
|
|
229
|
+
unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
|
|
230
|
+
artifact_hygiene: emptyArtifactHygiene(),
|
|
180
231
|
};
|
|
181
|
-
|
|
182
|
-
|
|
232
|
+
// Phase 5: Write partial_result.md when artifact collection fails
|
|
233
|
+
writeFileSync(join(taskDir, "partial_result.md"), [
|
|
234
|
+
"# PatchWarden Partial Result",
|
|
235
|
+
"",
|
|
236
|
+
`Task: ${taskId}`,
|
|
237
|
+
"",
|
|
238
|
+
"## Artifact Collection Status",
|
|
239
|
+
artifactStatus,
|
|
240
|
+
"",
|
|
241
|
+
"## Error",
|
|
242
|
+
artifactCollectionError,
|
|
243
|
+
"",
|
|
244
|
+
"## Agent Status",
|
|
245
|
+
agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
|
|
246
|
+
"",
|
|
247
|
+
"## Verification Status",
|
|
248
|
+
verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
249
|
+
"",
|
|
250
|
+
"## Note",
|
|
251
|
+
"Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
|
|
252
|
+
"",
|
|
253
|
+
].join("\n"), "utf-8");
|
|
254
|
+
// Don't override test failure with artifact failure
|
|
255
|
+
if (finalStatus === "done") {
|
|
256
|
+
finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
257
|
+
finalStatus = "failed";
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
261
|
+
}
|
|
183
262
|
}
|
|
263
|
+
const artifactCollectionFinishedAt = new Date().toISOString();
|
|
184
264
|
let outOfScopeChanges = [];
|
|
265
|
+
let preexistingExternalDirty = externalDirtyBaseline;
|
|
266
|
+
let newOutOfScopeChanges = [];
|
|
185
267
|
try {
|
|
186
268
|
const afterWorkspaceSnapshot = repoPath === wsRoot ? captureRepoSnapshot(repoPath) : captureRepoSnapshot(wsRoot);
|
|
187
269
|
writeSnapshot(taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
270
|
+
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, repoPath, wsRoot);
|
|
271
|
+
// Phase 4: Only NEW external dirty files (not in baseline) are scope violations
|
|
272
|
+
newOutOfScopeChanges = findNewExternalDirtyFiles(externalDirtyBaseline, allExternalDirty);
|
|
188
273
|
outOfScopeChanges = compareSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
|
|
189
274
|
.filter((file) => !isPathInside(resolve(wsRoot, file.path), repoPath) ||
|
|
190
275
|
Boolean(file.old_path && !isPathInside(resolve(wsRoot, file.old_path), repoPath)));
|
|
@@ -194,15 +279,20 @@ export async function runTask(taskId) {
|
|
|
194
279
|
if (finalStatus === "done")
|
|
195
280
|
finalStatus = "failed";
|
|
196
281
|
}
|
|
197
|
-
|
|
282
|
+
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
283
|
+
const preexistingWarnings = preexistingExternalDirty.length > 0
|
|
284
|
+
? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
|
|
285
|
+
: [];
|
|
286
|
+
if (newOutOfScopeChanges.length > 0) {
|
|
198
287
|
finalStatus = "failed_scope_violation";
|
|
199
|
-
finalError = `Detected ${
|
|
288
|
+
finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
200
289
|
writeFileSync(join(taskDir, "rollback-plan.json"), JSON.stringify({
|
|
201
290
|
task_id: taskId,
|
|
202
291
|
status: "review_required",
|
|
203
292
|
automatic_rollback_performed: false,
|
|
204
293
|
warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
|
|
205
|
-
out_of_scope_changes:
|
|
294
|
+
out_of_scope_changes: newOutOfScopeChanges,
|
|
295
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
206
296
|
}, null, 2), "utf-8");
|
|
207
297
|
writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
|
|
208
298
|
"# Scope Violation Rollback Plan",
|
|
@@ -211,8 +301,8 @@ export async function runTask(taskId) {
|
|
|
211
301
|
"",
|
|
212
302
|
"PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
|
|
213
303
|
"",
|
|
214
|
-
"##
|
|
215
|
-
...
|
|
304
|
+
"## New out-of-scope files (caused by this task)",
|
|
305
|
+
...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
|
|
216
306
|
"",
|
|
217
307
|
].join("\n"), "utf-8");
|
|
218
308
|
}
|
|
@@ -223,6 +313,10 @@ export async function runTask(taskId) {
|
|
|
223
313
|
writeFileSync(join(taskDir, "git.diff"), changes.diff, "utf-8");
|
|
224
314
|
writeFileSync(join(taskDir, "diff.patch"), changes.diff, "utf-8");
|
|
225
315
|
writeFileSync(join(taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
|
|
316
|
+
// Phase 6: Generate artifact_manifest.json and group changed files
|
|
317
|
+
const artifactManifest = buildArtifactManifest(changes.changed_files, repoPath, taskId);
|
|
318
|
+
writeFileSync(join(taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
|
|
319
|
+
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
226
320
|
writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
|
|
227
321
|
task_id: taskId,
|
|
228
322
|
additions: changes.additions,
|
|
@@ -231,7 +325,10 @@ export async function runTask(taskId) {
|
|
|
231
325
|
}, null, 2), "utf-8");
|
|
232
326
|
writeFileSync(join(taskDir, "test.log"), buildTestLog(testResult), "utf-8");
|
|
233
327
|
const verifyJson = buildVerifyJson(verifyCommands, verifyResults, repoPath);
|
|
234
|
-
|
|
328
|
+
// Phase 4: Use newOutOfScopeChanges (not outOfScopeChanges) for verify_status
|
|
329
|
+
// to stay consistent with finalStatus. Pre-existing external dirty files
|
|
330
|
+
// that didn't change during the task should NOT cause verify_status to fail.
|
|
331
|
+
if (newOutOfScopeChanges.length > 0) {
|
|
235
332
|
verifyJson.status = "failed";
|
|
236
333
|
verifyJson.failure_reason = "scope_violation";
|
|
237
334
|
}
|
|
@@ -245,6 +342,20 @@ export async function runTask(taskId) {
|
|
|
245
342
|
finalStatus = "failed";
|
|
246
343
|
const finalPhase = finalStatus === "done" ? "completed" : finalStatus;
|
|
247
344
|
const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
|
|
345
|
+
// Phase 7: Run Android build environment diagnostics if android_app exists
|
|
346
|
+
const androidDiagnostic = diagnoseAndroidBuild(repoPath);
|
|
347
|
+
let androidWarning = null;
|
|
348
|
+
if (androidDiagnostic.status !== "skip") {
|
|
349
|
+
if (androidDiagnostic.status === "fail") {
|
|
350
|
+
androidWarning = "Android project exists, APK not built because Android SDK is missing.";
|
|
351
|
+
}
|
|
352
|
+
else if (androidDiagnostic.status === "warn") {
|
|
353
|
+
const apkCheck = androidDiagnostic.checks.find((c) => c.check === "APK output path");
|
|
354
|
+
if (apkCheck && apkCheck.status !== "ok") {
|
|
355
|
+
androidWarning = `Android build environment has warnings: ${apkCheck.reason}`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
248
359
|
const resultMd = buildResultMarkdown({
|
|
249
360
|
taskId,
|
|
250
361
|
planId,
|
|
@@ -256,6 +367,12 @@ export async function runTask(taskId) {
|
|
|
256
367
|
verify: verifyJson,
|
|
257
368
|
changes,
|
|
258
369
|
outOfScopeChanges,
|
|
370
|
+
artifactStatus,
|
|
371
|
+
artifactManifest,
|
|
372
|
+
changedFileGroups,
|
|
373
|
+
androidDiagnostic,
|
|
374
|
+
androidWarning,
|
|
375
|
+
preexistingWarnings,
|
|
259
376
|
});
|
|
260
377
|
writeFileSync(join(taskDir, "result.md"), resultMd, "utf-8");
|
|
261
378
|
writeFileSync(join(taskDir, "result.json"), JSON.stringify({
|
|
@@ -270,7 +387,26 @@ export async function runTask(taskId) {
|
|
|
270
387
|
change_policy: changePolicy,
|
|
271
388
|
summary: finalError || "Agent execution and configured verification completed successfully.",
|
|
272
389
|
changed_files: changes.changed_files,
|
|
390
|
+
changed_file_groups: {
|
|
391
|
+
source_changes: changedFileGroups.source_changes.length,
|
|
392
|
+
docs_changes: changedFileGroups.docs_changes.length,
|
|
393
|
+
config_changes: changedFileGroups.config_changes.length,
|
|
394
|
+
test_changes: changedFileGroups.test_changes.length,
|
|
395
|
+
release_artifacts: changedFileGroups.release_artifacts.length,
|
|
396
|
+
runtime_generated_files: changedFileGroups.runtime_generated_files.length,
|
|
397
|
+
},
|
|
398
|
+
artifact_hygiene: changes.artifact_hygiene,
|
|
399
|
+
artifact_status: artifactStatus,
|
|
400
|
+
artifact_collection_error: artifactCollectionError,
|
|
401
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
402
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
403
|
+
artifact_manifest: artifactManifest,
|
|
273
404
|
out_of_scope_changes: outOfScopeChanges,
|
|
405
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
406
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
407
|
+
target_repo_status: changes.workspace_dirty_after ? "dirty" : "clean",
|
|
408
|
+
workspace_status: changes.workspace_dirty_after ? "dirty" : "clean",
|
|
409
|
+
android_diagnostic: androidDiagnostic,
|
|
274
410
|
verify_status: verifyJson.status,
|
|
275
411
|
verify_commands: verifyJson.commands,
|
|
276
412
|
commands_run: verifyJson.commands.map((command) => ({
|
|
@@ -281,12 +417,15 @@ export async function runTask(taskId) {
|
|
|
281
417
|
commands_observed: [],
|
|
282
418
|
verify: verifyJson,
|
|
283
419
|
artifacts: [
|
|
284
|
-
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json",
|
|
420
|
+
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json",
|
|
285
421
|
...(outOfScopeChanges.length > 0 ? ["rollback_scope_violation_plan.md", "rollback-plan.json"] : []),
|
|
422
|
+
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
286
423
|
],
|
|
287
424
|
warnings: [
|
|
288
425
|
...beforeSnapshot.warnings,
|
|
289
426
|
...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
|
|
427
|
+
...preexistingWarnings,
|
|
428
|
+
...(androidWarning ? [androidWarning] : []),
|
|
290
429
|
],
|
|
291
430
|
errors: finalError ? [finalError] : [],
|
|
292
431
|
known_issues: finalError ? [finalError] : [],
|
|
@@ -298,8 +437,14 @@ export async function runTask(taskId) {
|
|
|
298
437
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
299
438
|
: ["Resolve the reported failure before accepting the work."],
|
|
300
439
|
}, null, 2), "utf-8");
|
|
301
|
-
if (finalError)
|
|
302
|
-
|
|
440
|
+
if (finalError) {
|
|
441
|
+
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
442
|
+
const structuredError = errorPayload(lastCaughtError);
|
|
443
|
+
writeFileSync(join(taskDir, "error.log"), JSON.stringify({
|
|
444
|
+
summary: finalError,
|
|
445
|
+
...structuredError,
|
|
446
|
+
}, null, 2), "utf-8");
|
|
447
|
+
}
|
|
303
448
|
const finishedAt = new Date().toISOString();
|
|
304
449
|
updateStatus(taskDir, {
|
|
305
450
|
status: finalStatus,
|
|
@@ -309,7 +454,14 @@ export async function runTask(taskId) {
|
|
|
309
454
|
finished_at: finishedAt,
|
|
310
455
|
error: finalError,
|
|
311
456
|
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
457
|
+
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
458
|
+
artifact_status: artifactStatus,
|
|
459
|
+
artifact_collection_error: artifactCollectionError,
|
|
460
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
461
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
312
462
|
out_of_scope_changes: outOfScopeChanges,
|
|
463
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
464
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
313
465
|
verify_status: verifyJson.status,
|
|
314
466
|
verify_commands: verifyCommands,
|
|
315
467
|
diff_available: changes.diff_available,
|
|
@@ -403,6 +555,8 @@ async function runManagedProcess(options) {
|
|
|
403
555
|
let spawnError = null;
|
|
404
556
|
let terminationReason = null;
|
|
405
557
|
let forceTimer = null;
|
|
558
|
+
let fallbackTimer = null;
|
|
559
|
+
let childExitFinish = null;
|
|
406
560
|
let terminationStarted = false;
|
|
407
561
|
const heartbeat = () => {
|
|
408
562
|
const now = new Date().toISOString();
|
|
@@ -442,6 +596,22 @@ async function runManagedProcess(options) {
|
|
|
442
596
|
gracefulKill(child);
|
|
443
597
|
forceTimer = setTimeout(() => forceKill(child), GRACEFUL_KILL_MS);
|
|
444
598
|
}
|
|
599
|
+
// Fallback: if neither close nor exit fires within 10s after taskkill,
|
|
600
|
+
// force-resolve to prevent the runner from hanging indefinitely.
|
|
601
|
+
// Only starts AFTER termination is requested — normal long tasks are unaffected.
|
|
602
|
+
fallbackTimer = setTimeout(() => {
|
|
603
|
+
try {
|
|
604
|
+
forceKill(child);
|
|
605
|
+
}
|
|
606
|
+
catch { }
|
|
607
|
+
try {
|
|
608
|
+
child.kill("SIGKILL");
|
|
609
|
+
}
|
|
610
|
+
catch { }
|
|
611
|
+
// Resolve the exit promise via the shared finish handle
|
|
612
|
+
if (childExitFinish)
|
|
613
|
+
childExitFinish(null);
|
|
614
|
+
}, 10000);
|
|
445
615
|
};
|
|
446
616
|
child.stdout?.on("data", (chunk) => {
|
|
447
617
|
const text = chunk.toString("utf-8");
|
|
@@ -461,9 +631,19 @@ async function runManagedProcess(options) {
|
|
|
461
631
|
if (settled)
|
|
462
632
|
return;
|
|
463
633
|
settled = true;
|
|
634
|
+
if (fallbackTimer)
|
|
635
|
+
clearTimeout(fallbackTimer);
|
|
636
|
+
childExitFinish = null;
|
|
464
637
|
resolveExit(code);
|
|
465
638
|
};
|
|
639
|
+
childExitFinish = finish;
|
|
466
640
|
child.once("close", (code) => finish(code));
|
|
641
|
+
child.once("exit", (code) => {
|
|
642
|
+
// On Windows, "close" can lag behind "exit" when stdio pipes drain
|
|
643
|
+
// after taskkill. Resolve on exit to avoid hanging the runner, but
|
|
644
|
+
// still let "close" fire if it arrives first.
|
|
645
|
+
finish(code);
|
|
646
|
+
});
|
|
467
647
|
child.once("error", (error) => {
|
|
468
648
|
spawnError = error.message;
|
|
469
649
|
finish(null);
|
|
@@ -472,8 +652,8 @@ async function runManagedProcess(options) {
|
|
|
472
652
|
clearInterval(heartbeatTimer);
|
|
473
653
|
if (forceTimer)
|
|
474
654
|
clearTimeout(forceTimer);
|
|
475
|
-
stdoutStream?.
|
|
476
|
-
stderrStream?.
|
|
655
|
+
stdoutStream?.destroy();
|
|
656
|
+
stderrStream?.destroy();
|
|
477
657
|
writeTaskRuntime(options.taskDir, {
|
|
478
658
|
phase: options.phase,
|
|
479
659
|
last_heartbeat_at: new Date().toISOString(),
|
|
@@ -485,7 +665,7 @@ async function runManagedProcess(options) {
|
|
|
485
665
|
}
|
|
486
666
|
async function runTrustedTestCommand(testCommand, repoPath, taskDir, statusFile, deadlineMs) {
|
|
487
667
|
const config = getConfig();
|
|
488
|
-
const trusted = guardTestCommand(testCommand, config);
|
|
668
|
+
const trusted = guardTestCommand(testCommand, config, repoPath);
|
|
489
669
|
const startedAtMs = Date.now();
|
|
490
670
|
const startedAt = new Date(startedAtMs).toISOString();
|
|
491
671
|
const parts = trusted.split(/\s+/).filter(Boolean);
|
|
@@ -518,28 +698,6 @@ async function runTrustedTestCommand(testCommand, repoPath, taskDir, statusFile,
|
|
|
518
698
|
duration_ms: finishedAtMs - startedAtMs,
|
|
519
699
|
};
|
|
520
700
|
}
|
|
521
|
-
function buildExecutionPrompt(plan, repoPath, testCommand) {
|
|
522
|
-
let prompt = `You are executing a pre-written plan in a local repository.
|
|
523
|
-
|
|
524
|
-
## Repository
|
|
525
|
-
${repoPath}
|
|
526
|
-
|
|
527
|
-
## Plan
|
|
528
|
-
${plan}
|
|
529
|
-
|
|
530
|
-
## Instructions
|
|
531
|
-
1. Read the plan carefully.
|
|
532
|
-
2. Implement the changes in this repository only.
|
|
533
|
-
3. Do NOT modify files outside this repository.
|
|
534
|
-
4. Do NOT commit or push changes.
|
|
535
|
-
5. After implementing, describe what you changed.
|
|
536
|
-
6. Output a summary with what was done, files modified, and issues encountered.
|
|
537
|
-
`;
|
|
538
|
-
if (testCommand) {
|
|
539
|
-
prompt += `\n7. You may run ${testCommand}; PatchWarden will independently run it again for verification.`;
|
|
540
|
-
}
|
|
541
|
-
return prompt;
|
|
542
|
-
}
|
|
543
701
|
function buildTestLog(result) {
|
|
544
702
|
if (result.skipped)
|
|
545
703
|
return `${result.command || "(no test command)"}\nExit code: not run\n${result.stderr}\n`;
|
|
@@ -560,6 +718,40 @@ function buildResultMarkdown(input) {
|
|
|
560
718
|
const outOfScope = input.outOfScopeChanges.length
|
|
561
719
|
? input.outOfScopeChanges.map((file) => `- ${file.change}: ${file.old_path ? `${file.old_path} -> ` : ""}${file.path}`).join("\n")
|
|
562
720
|
: "(none)";
|
|
721
|
+
// Phase 6: Artifact manifest summary
|
|
722
|
+
const artifactCount = input.artifactManifest?.artifacts.length || 0;
|
|
723
|
+
const artifactLines = artifactCount > 0
|
|
724
|
+
? input.artifactManifest.artifacts.map((a) => `- ${a.type}: ${a.path} (${a.size} bytes, sha256: ${a.sha256.slice(0, 16)}...)`).join("\n")
|
|
725
|
+
: "(no release artifacts)";
|
|
726
|
+
// Phase 6: Changed file group summary
|
|
727
|
+
const groupLines = input.changedFileGroups
|
|
728
|
+
? [
|
|
729
|
+
`- source_changes: ${input.changedFileGroups.source_changes.length}`,
|
|
730
|
+
`- docs_changes: ${input.changedFileGroups.docs_changes.length}`,
|
|
731
|
+
`- config_changes: ${input.changedFileGroups.config_changes.length}`,
|
|
732
|
+
`- test_changes: ${input.changedFileGroups.test_changes.length}`,
|
|
733
|
+
`- release_artifacts: ${input.changedFileGroups.release_artifacts.length}`,
|
|
734
|
+
`- runtime_generated_files: ${input.changedFileGroups.runtime_generated_files.length}`,
|
|
735
|
+
].join("\n")
|
|
736
|
+
: "";
|
|
737
|
+
// Phase 7: Android diagnostic summary
|
|
738
|
+
let androidSection = "";
|
|
739
|
+
if (input.androidDiagnostic && input.androidDiagnostic.status !== "skip") {
|
|
740
|
+
const checkLines = (input.androidDiagnostic.checks || [])
|
|
741
|
+
.map((c) => `- [${c.status}] ${c.check}: ${c.reason}`)
|
|
742
|
+
.join("\n");
|
|
743
|
+
androidSection = [
|
|
744
|
+
"## Android Build Environment",
|
|
745
|
+
`Status: ${input.androidDiagnostic.status}`,
|
|
746
|
+
...(input.androidWarning ? [`Warning: ${input.androidWarning}`] : []),
|
|
747
|
+
checkLines,
|
|
748
|
+
"",
|
|
749
|
+
].join("\n");
|
|
750
|
+
}
|
|
751
|
+
// Phase 4: Pre-existing warnings
|
|
752
|
+
const preexistingSection = input.preexistingWarnings && input.preexistingWarnings.length > 0
|
|
753
|
+
? ["## Pre-existing Warnings", ...input.preexistingWarnings, ""].join("\n")
|
|
754
|
+
: "";
|
|
563
755
|
return [
|
|
564
756
|
"# PatchWarden Task Result",
|
|
565
757
|
"",
|
|
@@ -578,6 +770,12 @@ function buildResultMarkdown(input) {
|
|
|
578
770
|
"## Files changed",
|
|
579
771
|
changed,
|
|
580
772
|
"",
|
|
773
|
+
"## Changed file groups",
|
|
774
|
+
groupLines,
|
|
775
|
+
"",
|
|
776
|
+
"## Release artifacts",
|
|
777
|
+
artifactLines,
|
|
778
|
+
"",
|
|
581
779
|
"## Verification",
|
|
582
780
|
`- diff_available: ${input.changes.diff_available}`,
|
|
583
781
|
`- diff_truncated: ${input.changes.diff_truncated}`,
|
|
@@ -586,10 +784,13 @@ function buildResultMarkdown(input) {
|
|
|
586
784
|
`- verify_status: ${input.verify.status}`,
|
|
587
785
|
`- verify_commands: ${input.verify.commands.length}/${input.verify.requested_commands.length} executed`,
|
|
588
786
|
`- out_of_scope_changes: ${input.outOfScopeChanges.length}`,
|
|
787
|
+
`- artifact_status: ${input.artifactStatus || "collected"}`,
|
|
589
788
|
"",
|
|
590
789
|
"## Out-of-scope changes",
|
|
591
790
|
outOfScope,
|
|
592
791
|
"",
|
|
792
|
+
preexistingSection,
|
|
793
|
+
androidSection,
|
|
593
794
|
"## Summary",
|
|
594
795
|
input.error || "Agent execution and configured verification completed successfully.",
|
|
595
796
|
"",
|
|
@@ -671,13 +872,13 @@ function buildVerifyLog(commands) {
|
|
|
671
872
|
entry.stderr_tail || "(empty)",
|
|
672
873
|
].join("\n")).join("\n\n");
|
|
673
874
|
}
|
|
674
|
-
function normalizeVerifyCommands(value, legacyTestCommand, config) {
|
|
875
|
+
function normalizeVerifyCommands(value, legacyTestCommand, config, repoPath) {
|
|
675
876
|
if (value !== undefined && !Array.isArray(value)) {
|
|
676
877
|
throw new Error("Invalid task verify_commands metadata; expected an array.");
|
|
677
878
|
}
|
|
678
879
|
return [...new Set([
|
|
679
|
-
...(value || []).map((command) => guardTestCommand(String(command), config)),
|
|
680
|
-
...(legacyTestCommand ? [guardTestCommand(legacyTestCommand, config)] : []),
|
|
880
|
+
...(value || []).map((command) => guardTestCommand(String(command), config, repoPath)),
|
|
881
|
+
...(legacyTestCommand ? [guardTestCommand(legacyTestCommand, config, repoPath)] : []),
|
|
681
882
|
])];
|
|
682
883
|
}
|
|
683
884
|
function summarizeOutput(value) {
|
|
@@ -695,8 +896,13 @@ function normalizeTimeout(value, config) {
|
|
|
695
896
|
}
|
|
696
897
|
return timeout;
|
|
697
898
|
}
|
|
698
|
-
function failBeforeExecution(taskId, taskDir, message) {
|
|
699
|
-
|
|
899
|
+
function failBeforeExecution(taskId, taskDir, message, caughtError) {
|
|
900
|
+
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
901
|
+
const structuredError = caughtError ? errorPayload(caughtError) : { error: message };
|
|
902
|
+
writeFileSync(join(taskDir, "error.log"), JSON.stringify({
|
|
903
|
+
summary: message,
|
|
904
|
+
...structuredError,
|
|
905
|
+
}, null, 2), "utf-8");
|
|
700
906
|
const current = readStatus(join(taskDir, "status.json"));
|
|
701
907
|
const now = new Date().toISOString();
|
|
702
908
|
const verify = {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface SimpleProcessOptions {
|
|
2
|
+
command: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
cwd: string;
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
maxStdoutBytes?: number;
|
|
7
|
+
maxStderrBytes?: number;
|
|
8
|
+
stdoutPath?: string;
|
|
9
|
+
stderrPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SimpleProcessResult {
|
|
12
|
+
exitCode: number | null;
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
spawnError: string | null;
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
stdoutTruncated: boolean;
|
|
18
|
+
stderrTruncated: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function runSimpleProcessSync(options: SimpleProcessOptions): SimpleProcessResult;
|
|
21
|
+
export declare function runSimpleProcess(options: SimpleProcessOptions): Promise<SimpleProcessResult>;
|