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/src/runner/runTask.ts
CHANGED
|
@@ -6,29 +6,39 @@ import {
|
|
|
6
6
|
writeFileSync,
|
|
7
7
|
type WriteStream,
|
|
8
8
|
} from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
10
10
|
import { getTasksDir, getPlansDir, getConfig, resolveWorkspaceRoot } from "../config.js";
|
|
11
11
|
import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
12
12
|
import {
|
|
13
|
-
guardAgentCommand,
|
|
14
13
|
guardTestCommand,
|
|
15
|
-
sanitizePromptArg,
|
|
16
14
|
} from "../security/commandGuard.js";
|
|
17
15
|
import { writeTaskProgress } from "../taskProgress.js";
|
|
18
16
|
import { writeTaskRuntime } from "../taskRuntime.js";
|
|
17
|
+
import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
|
|
18
|
+
import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js";
|
|
19
19
|
import type { TaskPhase, TaskStatus } from "../tools/createTask.js";
|
|
20
20
|
import {
|
|
21
21
|
buildChangeArtifacts,
|
|
22
22
|
captureRepoSnapshot,
|
|
23
23
|
compareSnapshots,
|
|
24
|
+
emptyArtifactHygiene,
|
|
24
25
|
writeSnapshot,
|
|
26
|
+
extractExternalDirtyFiles,
|
|
27
|
+
findNewExternalDirtyFiles,
|
|
28
|
+
buildArtifactManifest,
|
|
29
|
+
groupChangedFiles,
|
|
25
30
|
type ChangedFile,
|
|
26
31
|
type ChangeArtifacts,
|
|
32
|
+
type ExternalDirtyFile,
|
|
33
|
+
type ArtifactManifest,
|
|
27
34
|
} from "./changeCapture.js";
|
|
35
|
+
import { PatchWardenError, errorPayload } from "../errors.js";
|
|
36
|
+
import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
|
|
28
37
|
|
|
29
38
|
const HEARTBEAT_INTERVAL_MS = 2000;
|
|
30
39
|
const GRACEFUL_KILL_MS = 2000;
|
|
31
40
|
const MAX_CAPTURE_CHARS = 100_000;
|
|
41
|
+
const ARTIFACT_COLLECTION_TIMEOUT_MS = 60_000;
|
|
32
42
|
|
|
33
43
|
interface TaskRunResult {
|
|
34
44
|
task_id: string;
|
|
@@ -94,10 +104,9 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
94
104
|
let verifyCommands: string[];
|
|
95
105
|
let timeoutSeconds: number;
|
|
96
106
|
try {
|
|
97
|
-
verifyCommands = normalizeVerifyCommands(initialStatus.verify_commands, testCommand, config);
|
|
98
107
|
timeoutSeconds = normalizeTimeout(initialStatus.timeout_seconds, config);
|
|
99
108
|
} catch (error) {
|
|
100
|
-
return failBeforeExecution(taskId, taskDir, errorMessage(error));
|
|
109
|
+
return failBeforeExecution(taskId, taskDir, errorMessage(error), error);
|
|
101
110
|
}
|
|
102
111
|
const startedAtMs = Date.now();
|
|
103
112
|
const deadlineMs = startedAtMs + timeoutSeconds * 1000;
|
|
@@ -107,7 +116,29 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
107
116
|
repoPath = guardWorkspacePath(rawRepoPath, wsRoot);
|
|
108
117
|
} catch (error) {
|
|
109
118
|
const message = `repo_path validation failed: ${errorMessage(error)}`;
|
|
110
|
-
return failBeforeExecution(taskId, taskDir, message);
|
|
119
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
verifyCommands = normalizeVerifyCommands(initialStatus.verify_commands, testCommand, config, repoPath);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
const message = `verification metadata validation failed: ${errorMessage(error)}`;
|
|
125
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Assessment freshness revalidation before execution ──
|
|
129
|
+
const assessmentId = String(initialStatus.assessment_id || "");
|
|
130
|
+
if (assessmentId) {
|
|
131
|
+
try {
|
|
132
|
+
const preExecSnapshot = captureRepoSnapshot(repoPath);
|
|
133
|
+
const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
|
|
134
|
+
if (!validation.valid) {
|
|
135
|
+
const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
|
|
136
|
+
return failBeforeExecution(taskId, taskDir, message);
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const message = `assessment validation error: ${errorMessage(error)}`;
|
|
140
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
141
|
+
}
|
|
111
142
|
}
|
|
112
143
|
|
|
113
144
|
updateStatus(taskDir, {
|
|
@@ -123,14 +154,17 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
123
154
|
|
|
124
155
|
let beforeSnapshot;
|
|
125
156
|
let beforeWorkspaceSnapshot;
|
|
157
|
+
let externalDirtyBaseline: ExternalDirtyFile[] = [];
|
|
126
158
|
try {
|
|
127
159
|
beforeSnapshot = captureRepoSnapshot(repoPath);
|
|
128
160
|
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
|
|
129
161
|
writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
|
|
130
162
|
writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
|
|
163
|
+
// Phase 4: Record external dirty files as baseline before task execution
|
|
164
|
+
externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
|
|
131
165
|
} catch (error) {
|
|
132
166
|
const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
|
|
133
|
-
return failBeforeExecution(taskId, taskDir, message);
|
|
167
|
+
return failBeforeExecution(taskId, taskDir, message, error);
|
|
134
168
|
}
|
|
135
169
|
|
|
136
170
|
let agentResult: ManagedProcessResult | null = null;
|
|
@@ -138,24 +172,20 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
138
172
|
const verifyResults: TestExecutionResult[] = [];
|
|
139
173
|
let finalStatus: TaskStatus = "failed";
|
|
140
174
|
let finalError: string | null = null;
|
|
175
|
+
let lastCaughtError: unknown = null;
|
|
141
176
|
|
|
142
177
|
try {
|
|
143
178
|
const planFile = resolve(plansDir, planId, "plan.md");
|
|
144
179
|
if (!existsSync(planFile)) throw new Error(`Plan not found: "${planId}". Save the plan first.`);
|
|
145
180
|
const planContent = readFileSync(planFile, "utf-8");
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
if (arg === "{repo}") return repoPath;
|
|
150
|
-
if (arg === "{prompt}") return prompt;
|
|
151
|
-
return arg;
|
|
152
|
-
});
|
|
153
|
-
const agentCommandLabel = `${basename(agentCmd.command)} (configured agent command)`;
|
|
181
|
+
const prompt = buildExecutionPrompt(planContent, repoPath, testCommand);
|
|
182
|
+
const invocation = buildAgentInvocation(agentName, repoPath, prompt, config);
|
|
183
|
+
const agentCommandLabel = invocation.commandLabel;
|
|
154
184
|
|
|
155
185
|
setTaskPhase(taskDir, "executing_agent", agentCommandLabel);
|
|
156
186
|
agentResult = await runManagedProcess({
|
|
157
|
-
command:
|
|
158
|
-
args:
|
|
187
|
+
command: invocation.command,
|
|
188
|
+
args: invocation.args,
|
|
159
189
|
cwd: repoPath,
|
|
160
190
|
taskDir,
|
|
161
191
|
statusFile,
|
|
@@ -209,19 +239,42 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
209
239
|
finalStatus = "done";
|
|
210
240
|
}
|
|
211
241
|
} catch (error) {
|
|
242
|
+
lastCaughtError = error;
|
|
212
243
|
finalError = errorMessage(error);
|
|
213
244
|
}
|
|
214
245
|
|
|
215
246
|
setTaskPhase(taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
247
|
+
const artifactCollectionStartedAt = new Date().toISOString();
|
|
216
248
|
let changes: ChangeArtifacts;
|
|
249
|
+
let artifactStatus: "collected" | "partial" | "failed" | "timeout" = "collected";
|
|
250
|
+
let artifactCollectionError: string | null = null;
|
|
251
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
217
252
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
// Phase 5: Wrap artifact collection with a timeout
|
|
254
|
+
const collectionResult = await Promise.race([
|
|
255
|
+
Promise.resolve().then(() => {
|
|
256
|
+
const afterSnapshot = captureRepoSnapshot(repoPath);
|
|
257
|
+
writeSnapshot(taskDir, "git-after.json", afterSnapshot);
|
|
258
|
+
return buildChangeArtifacts(repoPath, beforeSnapshot, afterSnapshot);
|
|
259
|
+
}),
|
|
260
|
+
new Promise<never>((_, reject) => {
|
|
261
|
+
timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
|
|
262
|
+
}),
|
|
263
|
+
]);
|
|
264
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
265
|
+
changes = collectionResult;
|
|
221
266
|
} catch (error) {
|
|
267
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
268
|
+
lastCaughtError = error;
|
|
269
|
+
artifactCollectionError = errorMessage(error);
|
|
270
|
+
if (artifactCollectionError.includes("timed out")) {
|
|
271
|
+
artifactStatus = "timeout";
|
|
272
|
+
} else {
|
|
273
|
+
artifactStatus = "failed";
|
|
274
|
+
}
|
|
222
275
|
changes = {
|
|
223
276
|
changed_files: [],
|
|
224
|
-
diff: `(change capture failed: ${
|
|
277
|
+
diff: `(change capture failed: ${artifactCollectionError})\n`,
|
|
225
278
|
diff_available: false,
|
|
226
279
|
diff_truncated: false,
|
|
227
280
|
diff_size_bytes: 0,
|
|
@@ -231,16 +284,50 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
231
284
|
workspace_dirty_before: beforeSnapshot.workspace_dirty,
|
|
232
285
|
workspace_dirty_after: beforeSnapshot.workspace_dirty,
|
|
233
286
|
patch_mode: "hash_only",
|
|
234
|
-
unavailable_reason: `Change capture failed: ${
|
|
287
|
+
unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
|
|
288
|
+
artifact_hygiene: emptyArtifactHygiene(),
|
|
235
289
|
};
|
|
236
|
-
|
|
237
|
-
|
|
290
|
+
// Phase 5: Write partial_result.md when artifact collection fails
|
|
291
|
+
writeFileSync(join(taskDir, "partial_result.md"), [
|
|
292
|
+
"# PatchWarden Partial Result",
|
|
293
|
+
"",
|
|
294
|
+
`Task: ${taskId}`,
|
|
295
|
+
"",
|
|
296
|
+
"## Artifact Collection Status",
|
|
297
|
+
artifactStatus,
|
|
298
|
+
"",
|
|
299
|
+
"## Error",
|
|
300
|
+
artifactCollectionError,
|
|
301
|
+
"",
|
|
302
|
+
"## Agent Status",
|
|
303
|
+
agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
|
|
304
|
+
"",
|
|
305
|
+
"## Verification Status",
|
|
306
|
+
verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
307
|
+
"",
|
|
308
|
+
"## Note",
|
|
309
|
+
"Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
|
|
310
|
+
"",
|
|
311
|
+
].join("\n"), "utf-8");
|
|
312
|
+
// Don't override test failure with artifact failure
|
|
313
|
+
if (finalStatus === "done") {
|
|
314
|
+
finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
315
|
+
finalStatus = "failed";
|
|
316
|
+
} else {
|
|
317
|
+
finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
318
|
+
}
|
|
238
319
|
}
|
|
320
|
+
const artifactCollectionFinishedAt = new Date().toISOString();
|
|
239
321
|
|
|
240
322
|
let outOfScopeChanges: ChangedFile[] = [];
|
|
323
|
+
let preexistingExternalDirty: ExternalDirtyFile[] = externalDirtyBaseline;
|
|
324
|
+
let newOutOfScopeChanges: ExternalDirtyFile[] = [];
|
|
241
325
|
try {
|
|
242
326
|
const afterWorkspaceSnapshot = repoPath === wsRoot ? captureRepoSnapshot(repoPath) : captureRepoSnapshot(wsRoot);
|
|
243
327
|
writeSnapshot(taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
328
|
+
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, repoPath, wsRoot);
|
|
329
|
+
// Phase 4: Only NEW external dirty files (not in baseline) are scope violations
|
|
330
|
+
newOutOfScopeChanges = findNewExternalDirtyFiles(externalDirtyBaseline, allExternalDirty);
|
|
244
331
|
outOfScopeChanges = compareSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
|
|
245
332
|
.filter((file) =>
|
|
246
333
|
!isPathInside(resolve(wsRoot, file.path), repoPath) ||
|
|
@@ -251,15 +338,21 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
251
338
|
if (finalStatus === "done") finalStatus = "failed";
|
|
252
339
|
}
|
|
253
340
|
|
|
254
|
-
|
|
341
|
+
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
342
|
+
const preexistingWarnings: string[] = preexistingExternalDirty.length > 0
|
|
343
|
+
? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
|
|
344
|
+
: [];
|
|
345
|
+
|
|
346
|
+
if (newOutOfScopeChanges.length > 0) {
|
|
255
347
|
finalStatus = "failed_scope_violation";
|
|
256
|
-
finalError = `Detected ${
|
|
348
|
+
finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
257
349
|
writeFileSync(join(taskDir, "rollback-plan.json"), JSON.stringify({
|
|
258
350
|
task_id: taskId,
|
|
259
351
|
status: "review_required",
|
|
260
352
|
automatic_rollback_performed: false,
|
|
261
353
|
warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
|
|
262
|
-
out_of_scope_changes:
|
|
354
|
+
out_of_scope_changes: newOutOfScopeChanges,
|
|
355
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
263
356
|
}, null, 2), "utf-8");
|
|
264
357
|
writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
|
|
265
358
|
"# Scope Violation Rollback Plan",
|
|
@@ -268,8 +361,8 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
268
361
|
"",
|
|
269
362
|
"PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
|
|
270
363
|
"",
|
|
271
|
-
"##
|
|
272
|
-
...
|
|
364
|
+
"## New out-of-scope files (caused by this task)",
|
|
365
|
+
...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
|
|
273
366
|
"",
|
|
274
367
|
].join("\n"), "utf-8");
|
|
275
368
|
} else if (changePolicy === "no_changes" && changes.changed_files.length > 0) {
|
|
@@ -280,6 +373,12 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
280
373
|
writeFileSync(join(taskDir, "git.diff"), changes.diff, "utf-8");
|
|
281
374
|
writeFileSync(join(taskDir, "diff.patch"), changes.diff, "utf-8");
|
|
282
375
|
writeFileSync(join(taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
|
|
376
|
+
|
|
377
|
+
// Phase 6: Generate artifact_manifest.json and group changed files
|
|
378
|
+
const artifactManifest: ArtifactManifest = buildArtifactManifest(changes.changed_files, repoPath, taskId);
|
|
379
|
+
writeFileSync(join(taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
|
|
380
|
+
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
381
|
+
|
|
283
382
|
writeFileSync(join(taskDir, "file-stats.json"), JSON.stringify({
|
|
284
383
|
task_id: taskId,
|
|
285
384
|
additions: changes.additions,
|
|
@@ -288,7 +387,10 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
288
387
|
}, null, 2), "utf-8");
|
|
289
388
|
writeFileSync(join(taskDir, "test.log"), buildTestLog(testResult), "utf-8");
|
|
290
389
|
const verifyJson = buildVerifyJson(verifyCommands, verifyResults, repoPath);
|
|
291
|
-
|
|
390
|
+
// Phase 4: Use newOutOfScopeChanges (not outOfScopeChanges) for verify_status
|
|
391
|
+
// to stay consistent with finalStatus. Pre-existing external dirty files
|
|
392
|
+
// that didn't change during the task should NOT cause verify_status to fail.
|
|
393
|
+
if (newOutOfScopeChanges.length > 0) {
|
|
292
394
|
verifyJson.status = "failed";
|
|
293
395
|
verifyJson.failure_reason = "scope_violation";
|
|
294
396
|
} else if (finalStatus === "failed_policy_violation") {
|
|
@@ -301,6 +403,21 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
301
403
|
if (!["canceled", "done", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus)) finalStatus = "failed";
|
|
302
404
|
const finalPhase: TaskPhase = finalStatus === "done" ? "completed" : finalStatus;
|
|
303
405
|
const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
|
|
406
|
+
|
|
407
|
+
// Phase 7: Run Android build environment diagnostics if android_app exists
|
|
408
|
+
const androidDiagnostic = diagnoseAndroidBuild(repoPath);
|
|
409
|
+
let androidWarning: string | null = null;
|
|
410
|
+
if (androidDiagnostic.status !== "skip") {
|
|
411
|
+
if (androidDiagnostic.status === "fail") {
|
|
412
|
+
androidWarning = "Android project exists, APK not built because Android SDK is missing.";
|
|
413
|
+
} else if (androidDiagnostic.status === "warn") {
|
|
414
|
+
const apkCheck = androidDiagnostic.checks.find((c) => c.check === "APK output path");
|
|
415
|
+
if (apkCheck && apkCheck.status !== "ok") {
|
|
416
|
+
androidWarning = `Android build environment has warnings: ${apkCheck.reason}`;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
304
421
|
const resultMd = buildResultMarkdown({
|
|
305
422
|
taskId,
|
|
306
423
|
planId,
|
|
@@ -312,6 +429,12 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
312
429
|
verify: verifyJson,
|
|
313
430
|
changes,
|
|
314
431
|
outOfScopeChanges,
|
|
432
|
+
artifactStatus,
|
|
433
|
+
artifactManifest,
|
|
434
|
+
changedFileGroups,
|
|
435
|
+
androidDiagnostic,
|
|
436
|
+
androidWarning,
|
|
437
|
+
preexistingWarnings,
|
|
315
438
|
});
|
|
316
439
|
writeFileSync(join(taskDir, "result.md"), resultMd, "utf-8");
|
|
317
440
|
writeFileSync(join(taskDir, "result.json"), JSON.stringify({
|
|
@@ -326,7 +449,26 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
326
449
|
change_policy: changePolicy,
|
|
327
450
|
summary: finalError || "Agent execution and configured verification completed successfully.",
|
|
328
451
|
changed_files: changes.changed_files,
|
|
452
|
+
changed_file_groups: {
|
|
453
|
+
source_changes: changedFileGroups.source_changes.length,
|
|
454
|
+
docs_changes: changedFileGroups.docs_changes.length,
|
|
455
|
+
config_changes: changedFileGroups.config_changes.length,
|
|
456
|
+
test_changes: changedFileGroups.test_changes.length,
|
|
457
|
+
release_artifacts: changedFileGroups.release_artifacts.length,
|
|
458
|
+
runtime_generated_files: changedFileGroups.runtime_generated_files.length,
|
|
459
|
+
},
|
|
460
|
+
artifact_hygiene: changes.artifact_hygiene,
|
|
461
|
+
artifact_status: artifactStatus,
|
|
462
|
+
artifact_collection_error: artifactCollectionError,
|
|
463
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
464
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
465
|
+
artifact_manifest: artifactManifest,
|
|
329
466
|
out_of_scope_changes: outOfScopeChanges,
|
|
467
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
468
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
469
|
+
target_repo_status: changes.workspace_dirty_after ? "dirty" : "clean",
|
|
470
|
+
workspace_status: changes.workspace_dirty_after ? "dirty" : "clean",
|
|
471
|
+
android_diagnostic: androidDiagnostic,
|
|
330
472
|
verify_status: verifyJson.status,
|
|
331
473
|
verify_commands: verifyJson.commands,
|
|
332
474
|
commands_run: verifyJson.commands.map((command) => ({
|
|
@@ -337,12 +479,15 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
337
479
|
commands_observed: [],
|
|
338
480
|
verify: verifyJson,
|
|
339
481
|
artifacts: [
|
|
340
|
-
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json",
|
|
482
|
+
"result.md", "result.json", "diff.patch", "git.diff", "test.log", "verify.log", "verify.json", "changed-files.json", "file-stats.json", "artifact_manifest.json",
|
|
341
483
|
...(outOfScopeChanges.length > 0 ? ["rollback_scope_violation_plan.md", "rollback-plan.json"] : []),
|
|
484
|
+
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
342
485
|
],
|
|
343
486
|
warnings: [
|
|
344
487
|
...beforeSnapshot.warnings,
|
|
345
488
|
...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
|
|
489
|
+
...preexistingWarnings,
|
|
490
|
+
...(androidWarning ? [androidWarning] : []),
|
|
346
491
|
],
|
|
347
492
|
errors: finalError ? [finalError] : [],
|
|
348
493
|
known_issues: finalError ? [finalError] : [],
|
|
@@ -354,7 +499,14 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
354
499
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
355
500
|
: ["Resolve the reported failure before accepting the work."],
|
|
356
501
|
}, null, 2), "utf-8");
|
|
357
|
-
if (finalError)
|
|
502
|
+
if (finalError) {
|
|
503
|
+
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
504
|
+
const structuredError = errorPayload(lastCaughtError);
|
|
505
|
+
writeFileSync(join(taskDir, "error.log"), JSON.stringify({
|
|
506
|
+
summary: finalError,
|
|
507
|
+
...structuredError,
|
|
508
|
+
}, null, 2), "utf-8");
|
|
509
|
+
}
|
|
358
510
|
|
|
359
511
|
const finishedAt = new Date().toISOString();
|
|
360
512
|
updateStatus(taskDir, {
|
|
@@ -365,7 +517,14 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
365
517
|
finished_at: finishedAt,
|
|
366
518
|
error: finalError,
|
|
367
519
|
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
520
|
+
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
521
|
+
artifact_status: artifactStatus,
|
|
522
|
+
artifact_collection_error: artifactCollectionError,
|
|
523
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
524
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
368
525
|
out_of_scope_changes: outOfScopeChanges,
|
|
526
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
527
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
369
528
|
verify_status: verifyJson.status,
|
|
370
529
|
verify_commands: verifyCommands,
|
|
371
530
|
diff_available: changes.diff_available,
|
|
@@ -483,6 +642,8 @@ async function runManagedProcess(options: {
|
|
|
483
642
|
let spawnError: string | null = null;
|
|
484
643
|
let terminationReason: TerminationReason = null;
|
|
485
644
|
let forceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
645
|
+
let fallbackTimer: ReturnType<typeof setTimeout> | null = null;
|
|
646
|
+
let childExitFinish: ((code: number | null) => void) | null = null;
|
|
486
647
|
let terminationStarted = false;
|
|
487
648
|
|
|
488
649
|
const heartbeat = () => {
|
|
@@ -520,6 +681,15 @@ async function runManagedProcess(options: {
|
|
|
520
681
|
gracefulKill(child);
|
|
521
682
|
forceTimer = setTimeout(() => forceKill(child), GRACEFUL_KILL_MS);
|
|
522
683
|
}
|
|
684
|
+
// Fallback: if neither close nor exit fires within 10s after taskkill,
|
|
685
|
+
// force-resolve to prevent the runner from hanging indefinitely.
|
|
686
|
+
// Only starts AFTER termination is requested — normal long tasks are unaffected.
|
|
687
|
+
fallbackTimer = setTimeout(() => {
|
|
688
|
+
try { forceKill(child); } catch {}
|
|
689
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
690
|
+
// Resolve the exit promise via the shared finish handle
|
|
691
|
+
if (childExitFinish) childExitFinish(null);
|
|
692
|
+
}, 10000);
|
|
523
693
|
};
|
|
524
694
|
|
|
525
695
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
@@ -540,9 +710,18 @@ async function runManagedProcess(options: {
|
|
|
540
710
|
const finish = (code: number | null) => {
|
|
541
711
|
if (settled) return;
|
|
542
712
|
settled = true;
|
|
713
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
714
|
+
childExitFinish = null;
|
|
543
715
|
resolveExit(code);
|
|
544
716
|
};
|
|
717
|
+
childExitFinish = finish;
|
|
545
718
|
child.once("close", (code) => finish(code));
|
|
719
|
+
child.once("exit", (code) => {
|
|
720
|
+
// On Windows, "close" can lag behind "exit" when stdio pipes drain
|
|
721
|
+
// after taskkill. Resolve on exit to avoid hanging the runner, but
|
|
722
|
+
// still let "close" fire if it arrives first.
|
|
723
|
+
finish(code);
|
|
724
|
+
});
|
|
546
725
|
child.once("error", (error) => {
|
|
547
726
|
spawnError = error.message;
|
|
548
727
|
finish(null);
|
|
@@ -551,8 +730,8 @@ async function runManagedProcess(options: {
|
|
|
551
730
|
|
|
552
731
|
clearInterval(heartbeatTimer);
|
|
553
732
|
if (forceTimer) clearTimeout(forceTimer);
|
|
554
|
-
stdoutStream?.
|
|
555
|
-
stderrStream?.
|
|
733
|
+
stdoutStream?.destroy();
|
|
734
|
+
stderrStream?.destroy();
|
|
556
735
|
writeTaskRuntime(options.taskDir, {
|
|
557
736
|
phase: options.phase,
|
|
558
737
|
last_heartbeat_at: new Date().toISOString(),
|
|
@@ -572,7 +751,7 @@ async function runTrustedTestCommand(
|
|
|
572
751
|
deadlineMs: number
|
|
573
752
|
): Promise<TestExecutionResult> {
|
|
574
753
|
const config = getConfig();
|
|
575
|
-
const trusted = guardTestCommand(testCommand, config);
|
|
754
|
+
const trusted = guardTestCommand(testCommand, config, repoPath);
|
|
576
755
|
const startedAtMs = Date.now();
|
|
577
756
|
const startedAt = new Date(startedAtMs).toISOString();
|
|
578
757
|
const parts = trusted.split(/\s+/).filter(Boolean);
|
|
@@ -606,29 +785,6 @@ async function runTrustedTestCommand(
|
|
|
606
785
|
};
|
|
607
786
|
}
|
|
608
787
|
|
|
609
|
-
function buildExecutionPrompt(plan: string, repoPath: string, testCommand: string): string {
|
|
610
|
-
let prompt = `You are executing a pre-written plan in a local repository.
|
|
611
|
-
|
|
612
|
-
## Repository
|
|
613
|
-
${repoPath}
|
|
614
|
-
|
|
615
|
-
## Plan
|
|
616
|
-
${plan}
|
|
617
|
-
|
|
618
|
-
## Instructions
|
|
619
|
-
1. Read the plan carefully.
|
|
620
|
-
2. Implement the changes in this repository only.
|
|
621
|
-
3. Do NOT modify files outside this repository.
|
|
622
|
-
4. Do NOT commit or push changes.
|
|
623
|
-
5. After implementing, describe what you changed.
|
|
624
|
-
6. Output a summary with what was done, files modified, and issues encountered.
|
|
625
|
-
`;
|
|
626
|
-
if (testCommand) {
|
|
627
|
-
prompt += `\n7. You may run ${testCommand}; PatchWarden will independently run it again for verification.`;
|
|
628
|
-
}
|
|
629
|
-
return prompt;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
788
|
function buildTestLog(result: TestExecutionResult): string {
|
|
633
789
|
if (result.skipped) return `${result.command || "(no test command)"}\nExit code: not run\n${result.stderr}\n`;
|
|
634
790
|
return [
|
|
@@ -653,6 +809,12 @@ function buildResultMarkdown(input: {
|
|
|
653
809
|
verify: VerifyReport;
|
|
654
810
|
changes: ChangeArtifacts;
|
|
655
811
|
outOfScopeChanges: ChangedFile[];
|
|
812
|
+
artifactStatus?: string;
|
|
813
|
+
artifactManifest?: ArtifactManifest;
|
|
814
|
+
changedFileGroups?: { source_changes: ChangedFile[]; docs_changes: ChangedFile[]; config_changes: ChangedFile[]; test_changes: ChangedFile[]; release_artifacts: ChangedFile[]; runtime_generated_files: ChangedFile[] };
|
|
815
|
+
androidDiagnostic?: { status: string; checks?: Array<{ check: string; status: string; reason: string }> };
|
|
816
|
+
androidWarning?: string | null;
|
|
817
|
+
preexistingWarnings?: string[];
|
|
656
818
|
}): string {
|
|
657
819
|
const changed = input.changes.changed_files.length
|
|
658
820
|
? input.changes.changed_files.map((file) => `- ${file.change}: ${file.path}`).join("\n")
|
|
@@ -660,6 +822,45 @@ function buildResultMarkdown(input: {
|
|
|
660
822
|
const outOfScope = input.outOfScopeChanges.length
|
|
661
823
|
? input.outOfScopeChanges.map((file) => `- ${file.change}: ${file.old_path ? `${file.old_path} -> ` : ""}${file.path}`).join("\n")
|
|
662
824
|
: "(none)";
|
|
825
|
+
|
|
826
|
+
// Phase 6: Artifact manifest summary
|
|
827
|
+
const artifactCount = input.artifactManifest?.artifacts.length || 0;
|
|
828
|
+
const artifactLines = artifactCount > 0
|
|
829
|
+
? input.artifactManifest!.artifacts.map((a) => `- ${a.type}: ${a.path} (${a.size} bytes, sha256: ${a.sha256.slice(0, 16)}...)`).join("\n")
|
|
830
|
+
: "(no release artifacts)";
|
|
831
|
+
|
|
832
|
+
// Phase 6: Changed file group summary
|
|
833
|
+
const groupLines = input.changedFileGroups
|
|
834
|
+
? [
|
|
835
|
+
`- source_changes: ${input.changedFileGroups.source_changes.length}`,
|
|
836
|
+
`- docs_changes: ${input.changedFileGroups.docs_changes.length}`,
|
|
837
|
+
`- config_changes: ${input.changedFileGroups.config_changes.length}`,
|
|
838
|
+
`- test_changes: ${input.changedFileGroups.test_changes.length}`,
|
|
839
|
+
`- release_artifacts: ${input.changedFileGroups.release_artifacts.length}`,
|
|
840
|
+
`- runtime_generated_files: ${input.changedFileGroups.runtime_generated_files.length}`,
|
|
841
|
+
].join("\n")
|
|
842
|
+
: "";
|
|
843
|
+
|
|
844
|
+
// Phase 7: Android diagnostic summary
|
|
845
|
+
let androidSection = "";
|
|
846
|
+
if (input.androidDiagnostic && input.androidDiagnostic.status !== "skip") {
|
|
847
|
+
const checkLines = (input.androidDiagnostic.checks || [])
|
|
848
|
+
.map((c) => `- [${c.status}] ${c.check}: ${c.reason}`)
|
|
849
|
+
.join("\n");
|
|
850
|
+
androidSection = [
|
|
851
|
+
"## Android Build Environment",
|
|
852
|
+
`Status: ${input.androidDiagnostic.status}`,
|
|
853
|
+
...(input.androidWarning ? [`Warning: ${input.androidWarning}`] : []),
|
|
854
|
+
checkLines,
|
|
855
|
+
"",
|
|
856
|
+
].join("\n");
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Phase 4: Pre-existing warnings
|
|
860
|
+
const preexistingSection = input.preexistingWarnings && input.preexistingWarnings.length > 0
|
|
861
|
+
? ["## Pre-existing Warnings", ...input.preexistingWarnings, ""].join("\n")
|
|
862
|
+
: "";
|
|
863
|
+
|
|
663
864
|
return [
|
|
664
865
|
"# PatchWarden Task Result",
|
|
665
866
|
"",
|
|
@@ -678,6 +879,12 @@ function buildResultMarkdown(input: {
|
|
|
678
879
|
"## Files changed",
|
|
679
880
|
changed,
|
|
680
881
|
"",
|
|
882
|
+
"## Changed file groups",
|
|
883
|
+
groupLines,
|
|
884
|
+
"",
|
|
885
|
+
"## Release artifacts",
|
|
886
|
+
artifactLines,
|
|
887
|
+
"",
|
|
681
888
|
"## Verification",
|
|
682
889
|
`- diff_available: ${input.changes.diff_available}`,
|
|
683
890
|
`- diff_truncated: ${input.changes.diff_truncated}`,
|
|
@@ -686,10 +893,13 @@ function buildResultMarkdown(input: {
|
|
|
686
893
|
`- verify_status: ${input.verify.status}`,
|
|
687
894
|
`- verify_commands: ${input.verify.commands.length}/${input.verify.requested_commands.length} executed`,
|
|
688
895
|
`- out_of_scope_changes: ${input.outOfScopeChanges.length}`,
|
|
896
|
+
`- artifact_status: ${input.artifactStatus || "collected"}`,
|
|
689
897
|
"",
|
|
690
898
|
"## Out-of-scope changes",
|
|
691
899
|
outOfScope,
|
|
692
900
|
"",
|
|
901
|
+
preexistingSection,
|
|
902
|
+
androidSection,
|
|
693
903
|
"## Summary",
|
|
694
904
|
input.error || "Agent execution and configured verification completed successfully.",
|
|
695
905
|
"",
|
|
@@ -777,14 +987,15 @@ function buildVerifyLog(commands: VerifyCommandRecord[]): string {
|
|
|
777
987
|
function normalizeVerifyCommands(
|
|
778
988
|
value: unknown,
|
|
779
989
|
legacyTestCommand: string,
|
|
780
|
-
config: ReturnType<typeof getConfig
|
|
990
|
+
config: ReturnType<typeof getConfig>,
|
|
991
|
+
repoPath: string
|
|
781
992
|
): string[] {
|
|
782
993
|
if (value !== undefined && !Array.isArray(value)) {
|
|
783
994
|
throw new Error("Invalid task verify_commands metadata; expected an array.");
|
|
784
995
|
}
|
|
785
996
|
return [...new Set([
|
|
786
|
-
...((value as unknown[] | undefined) || []).map((command) => guardTestCommand(String(command), config)),
|
|
787
|
-
...(legacyTestCommand ? [guardTestCommand(legacyTestCommand, config)] : []),
|
|
997
|
+
...((value as unknown[] | undefined) || []).map((command) => guardTestCommand(String(command), config, repoPath)),
|
|
998
|
+
...(legacyTestCommand ? [guardTestCommand(legacyTestCommand, config, repoPath)] : []),
|
|
788
999
|
])];
|
|
789
1000
|
}
|
|
790
1001
|
|
|
@@ -806,8 +1017,13 @@ function normalizeTimeout(value: unknown, config: ReturnType<typeof getConfig>):
|
|
|
806
1017
|
return timeout;
|
|
807
1018
|
}
|
|
808
1019
|
|
|
809
|
-
function failBeforeExecution(taskId: string, taskDir: string, message: string): TaskRunResult {
|
|
810
|
-
|
|
1020
|
+
function failBeforeExecution(taskId: string, taskDir: string, message: string, caughtError?: unknown): TaskRunResult {
|
|
1021
|
+
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
1022
|
+
const structuredError = caughtError ? errorPayload(caughtError) : { error: message };
|
|
1023
|
+
writeFileSync(join(taskDir, "error.log"), JSON.stringify({
|
|
1024
|
+
summary: message,
|
|
1025
|
+
...structuredError,
|
|
1026
|
+
}, null, 2), "utf-8");
|
|
811
1027
|
const current = readStatus(join(taskDir, "status.json"));
|
|
812
1028
|
const now = new Date().toISOString();
|
|
813
1029
|
const verify: VerifyReport = {
|