patchwarden 1.1.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 +83 -8
- package/README.md +78 -10
- 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 -2001
- package/dist/direct/directGuards.js +30 -8
- package/dist/direct/directSessionStore.d.ts +2 -0
- package/dist/direct/directVerification.js +7 -0
- 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/policy/projectPolicy.d.ts +55 -0
- package/dist/policy/projectPolicy.js +286 -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 +261 -255
- 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.d.ts +1 -0
- package/dist/test/unit/evidence-pack.test.js +224 -0
- 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/project-policy-release-mode.test.d.ts +1 -0
- package/dist/test/unit/project-policy-release-mode.test.js +125 -0
- package/dist/test/unit/run-task-loop.test.d.ts +1 -0
- package/dist/test/unit/run-task-loop.test.js +380 -0
- package/dist/test/unit/schema-drift-check.test.js +10 -9
- 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 +45 -0
- package/dist/tools/evidencePack.js +375 -0
- 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/recommendAgentForTask.d.ts +19 -0
- package/dist/tools/recommendAgentForTask.js +56 -0
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +333 -402
- package/dist/tools/releaseMode.d.ts +50 -0
- package/dist/tools/releaseMode.js +370 -0
- package/dist/tools/retryTask.d.ts +2 -2
- package/dist/tools/retryTask.js +2 -2
- package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
- package/dist/tools/runDirectVerificationBundle.js +64 -0
- package/dist/tools/runTaskLoop.d.ts +57 -0
- package/dist/tools/runTaskLoop.js +417 -0
- package/dist/tools/runVerification.d.ts +4 -0
- package/dist/tools/runVerification.js +4 -0
- package/dist/tools/safeViews.d.ts +8 -2
- package/dist/tools/safeViews.js +4 -2
- package/dist/tools/taskLineage.d.ts +91 -0
- package/dist/tools/taskLineage.js +175 -0
- package/dist/tools/toolCatalog.d.ts +2 -2
- package/dist/tools/toolCatalog.js +6 -0
- package/dist/tools/toolRegistry.js +132 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- 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/chatgpt-usage.md +31 -0
- package/docs/control-center/README.md +9 -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 +3 -3
- package/scripts/checks/control-center-smoke.js +443 -0
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/lifecycle-smoke.js +29 -23
- package/scripts/checks/mcp-manifest-check.js +12 -0
- package/scripts/checks/mcp-smoke.js +33 -7
- package/scripts/checks/watcher-supervisor-smoke.js +1 -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 -2150
- package/src/direct/directGuards.ts +28 -7
- package/src/direct/directSessionStore.ts +2 -0
- package/src/direct/directVerification.ts +7 -0
- 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/policy/projectPolicy.ts +344 -0
- 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 +254 -252
- 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 +251 -0
- 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/project-policy-release-mode.test.ts +156 -0
- package/src/test/unit/run-task-loop.test.ts +425 -0
- package/src/test/unit/schema-drift-check.test.ts +11 -9
- 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 +490 -0
- package/src/tools/finalizeDirectSession.ts +4 -4
- package/src/tools/goalSubgoalTask.ts +2 -2
- package/src/tools/healthCheck.ts +3 -3
- package/src/tools/listWorkspace.ts +71 -71
- package/src/tools/recommendAgentForTask.ts +79 -0
- package/src/tools/registry.ts +352 -509
- package/src/tools/releaseMode.ts +450 -0
- package/src/tools/retryTask.ts +2 -2
- package/src/tools/runDirectVerificationBundle.ts +98 -0
- package/src/tools/runTaskLoop.ts +526 -0
- package/src/tools/runVerification.ts +8 -0
- package/src/tools/safeViews.ts +4 -2
- package/src/tools/taskLineage.ts +300 -0
- package/src/tools/toolCatalog.ts +6 -0
- package/src/tools/toolRegistry.ts +132 -0
- package/src/version.ts +2 -2
- package/ui/pages/audit.html +192 -3
- package/ui/pages/dashboard.html +1070 -14
- package/ui/pages/direct-sessions.html +505 -53
- package/ui/pages/task-detail.html +456 -438
- package/ui/pages/tasks.html +598 -61
package/src/runner/runTask.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
type ChangeArtifacts,
|
|
32
32
|
type ExternalDirtyFile,
|
|
33
33
|
type ArtifactManifest,
|
|
34
|
+
type RepoSnapshot,
|
|
35
|
+
type ChangedFileGroups,
|
|
34
36
|
} from "./changeCapture.js";
|
|
35
37
|
import { PatchWardenError, errorPayload } from "../errors.js";
|
|
36
38
|
import { diagnoseAndroidBuild } from "../tools/androidDoctor.js";
|
|
@@ -85,7 +87,63 @@ interface VerifyReport {
|
|
|
85
87
|
failure_reason?: string;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
interface TaskContext {
|
|
91
|
+
taskId: string;
|
|
92
|
+
taskDir: string;
|
|
93
|
+
statusFile: string;
|
|
94
|
+
repoPath: string;
|
|
95
|
+
wsRoot: string;
|
|
96
|
+
config: ReturnType<typeof getConfig>;
|
|
97
|
+
plansDir: string;
|
|
98
|
+
initialStatus: Record<string, any>;
|
|
99
|
+
planId: string;
|
|
100
|
+
agentName: string;
|
|
101
|
+
testCommand: string;
|
|
102
|
+
changePolicy: string;
|
|
103
|
+
verifyCommands: string[];
|
|
104
|
+
timeoutSeconds: number;
|
|
105
|
+
startedAtMs: number;
|
|
106
|
+
deadlineMs: number;
|
|
107
|
+
beforeSnapshot: RepoSnapshot;
|
|
108
|
+
beforeWorkspaceSnapshot: RepoSnapshot;
|
|
109
|
+
externalDirtyBaseline: ExternalDirtyFile[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface ExecutionState {
|
|
113
|
+
agentResult: ManagedProcessResult | null;
|
|
114
|
+
testResult: TestExecutionResult;
|
|
115
|
+
verifyResults: TestExecutionResult[];
|
|
116
|
+
finalStatus: TaskStatus;
|
|
117
|
+
finalError: string | null;
|
|
118
|
+
lastCaughtError: unknown;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface ArtifactEvidence {
|
|
122
|
+
changes: ChangeArtifacts;
|
|
123
|
+
artifactStatus: "collected" | "partial" | "failed" | "timeout";
|
|
124
|
+
artifactCollectionError: string | null;
|
|
125
|
+
artifactCollectionStartedAt: string;
|
|
126
|
+
artifactCollectionFinishedAt: string;
|
|
127
|
+
outOfScopeChanges: ChangedFile[];
|
|
128
|
+
newOutOfScopeChanges: ExternalDirtyFile[];
|
|
129
|
+
preexistingExternalDirty: ExternalDirtyFile[];
|
|
130
|
+
preexistingWarnings: string[];
|
|
131
|
+
cleanupReport: PostTaskCleanupReport;
|
|
132
|
+
artifactManifest: ArtifactManifest;
|
|
133
|
+
changedFileGroups: ChangedFileGroups;
|
|
134
|
+
}
|
|
135
|
+
|
|
88
136
|
export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
137
|
+
const prepared = await prepareTask(taskId);
|
|
138
|
+
if (!("taskDir" in prepared)) return prepared;
|
|
139
|
+
const ctx: TaskContext = prepared;
|
|
140
|
+
const state = await executeAgent(ctx);
|
|
141
|
+
await runVerification(ctx, state);
|
|
142
|
+
const evidence = await collectArtifacts(ctx, state);
|
|
143
|
+
return finalizeTask(ctx, state, evidence);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult> {
|
|
89
147
|
const config = getConfig();
|
|
90
148
|
const tasksDir = getTasksDir(config);
|
|
91
149
|
const plansDir = getPlansDir(config);
|
|
@@ -126,11 +184,10 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
126
184
|
return failBeforeExecution(taskId, taskDir, message, error);
|
|
127
185
|
}
|
|
128
186
|
|
|
129
|
-
// ── Assessment freshness revalidation before execution ──
|
|
130
187
|
const assessmentId = String(initialStatus.assessment_id || "");
|
|
131
188
|
if (assessmentId) {
|
|
132
189
|
try {
|
|
133
|
-
const preExecSnapshot = captureRepoSnapshot(repoPath);
|
|
190
|
+
const preExecSnapshot = await captureRepoSnapshot(repoPath);
|
|
134
191
|
const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
|
|
135
192
|
if (!validation.valid) {
|
|
136
193
|
const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
|
|
@@ -151,141 +208,157 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
151
208
|
current_command: null,
|
|
152
209
|
error: null,
|
|
153
210
|
});
|
|
154
|
-
// v0.7.0: record task_started_at and watcher_instance_id so diagnose_task
|
|
155
|
-
// can detect PID reuse and orphaned tasks. watcher_instance_id comes from
|
|
156
|
-
// the watcher process env; when run_task MCP tool is used directly, this
|
|
157
|
-
// field is intentionally left undefined so ownership cannot be falsely claimed.
|
|
158
211
|
writeTaskRuntime(taskDir, {
|
|
159
212
|
task_started_at: new Date(startedAtMs).toISOString(),
|
|
160
213
|
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
|
|
161
214
|
});
|
|
162
215
|
setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
|
|
163
216
|
|
|
164
|
-
let beforeSnapshot;
|
|
165
|
-
let beforeWorkspaceSnapshot;
|
|
217
|
+
let beforeSnapshot: RepoSnapshot;
|
|
218
|
+
let beforeWorkspaceSnapshot: RepoSnapshot;
|
|
166
219
|
let externalDirtyBaseline: ExternalDirtyFile[] = [];
|
|
167
220
|
try {
|
|
168
|
-
beforeSnapshot = captureRepoSnapshot(repoPath);
|
|
169
|
-
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
|
|
221
|
+
beforeSnapshot = await captureRepoSnapshot(repoPath);
|
|
222
|
+
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : await captureRepoSnapshot(wsRoot);
|
|
170
223
|
writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
|
|
171
224
|
writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
|
|
172
|
-
// Phase 4: Record external dirty files as baseline before task execution
|
|
173
225
|
externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
|
|
174
226
|
} catch (error) {
|
|
175
227
|
const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
|
|
176
228
|
return failBeforeExecution(taskId, taskDir, message, error);
|
|
177
229
|
}
|
|
178
230
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
return {
|
|
232
|
+
taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
|
|
233
|
+
planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
|
|
234
|
+
startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function executeAgent(ctx: TaskContext): Promise<ExecutionState> {
|
|
239
|
+
const state: ExecutionState = {
|
|
240
|
+
agentResult: null,
|
|
241
|
+
testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
|
|
242
|
+
verifyResults: [],
|
|
243
|
+
finalStatus: "failed",
|
|
244
|
+
finalError: null,
|
|
245
|
+
lastCaughtError: null,
|
|
246
|
+
};
|
|
185
247
|
|
|
186
248
|
try {
|
|
187
|
-
const planFile = resolve(plansDir, planId, "plan.md");
|
|
188
|
-
if (!existsSync(planFile)) throw new Error(`Plan not found: "${planId}". Save the plan first.`);
|
|
249
|
+
const planFile = resolve(ctx.plansDir, ctx.planId, "plan.md");
|
|
250
|
+
if (!existsSync(planFile)) throw new Error(`Plan not found: "${ctx.planId}". Save the plan first.`);
|
|
189
251
|
const planContent = readFileSync(planFile, "utf-8");
|
|
190
|
-
const prompt = buildExecutionPrompt(planContent, repoPath, testCommand);
|
|
191
|
-
const invocation = buildAgentInvocation(agentName, repoPath, prompt, config);
|
|
192
|
-
const agentCommandLabel = invocation.commandLabel;
|
|
252
|
+
const prompt = buildExecutionPrompt(planContent, ctx.repoPath, ctx.testCommand);
|
|
253
|
+
const invocation = buildAgentInvocation(ctx.agentName, ctx.repoPath, prompt, ctx.config);
|
|
193
254
|
|
|
194
|
-
setTaskPhase(taskDir, "executing_agent",
|
|
195
|
-
agentResult = await runManagedProcess({
|
|
255
|
+
setTaskPhase(ctx.taskDir, "executing_agent", invocation.commandLabel);
|
|
256
|
+
state.agentResult = await runManagedProcess({
|
|
196
257
|
command: invocation.command,
|
|
197
258
|
args: invocation.args,
|
|
198
|
-
cwd: repoPath,
|
|
199
|
-
taskDir,
|
|
200
|
-
statusFile,
|
|
259
|
+
cwd: ctx.repoPath,
|
|
260
|
+
taskDir: ctx.taskDir,
|
|
261
|
+
statusFile: ctx.statusFile,
|
|
201
262
|
phase: "executing_agent",
|
|
202
|
-
currentCommand:
|
|
203
|
-
deadlineMs,
|
|
204
|
-
stdoutPath: join(taskDir, "stdout.log"),
|
|
205
|
-
stderrPath: join(taskDir, "stderr.log"),
|
|
263
|
+
currentCommand: invocation.commandLabel,
|
|
264
|
+
deadlineMs: ctx.deadlineMs,
|
|
265
|
+
stdoutPath: join(ctx.taskDir, "stdout.log"),
|
|
266
|
+
stderrPath: join(ctx.taskDir, "stderr.log"),
|
|
206
267
|
});
|
|
207
268
|
|
|
269
|
+
const agentResult = state.agentResult;
|
|
208
270
|
if (agentResult.terminationReason === "canceled" || agentResult.terminationReason === "killed") {
|
|
209
|
-
finalStatus = "canceled";
|
|
210
|
-
finalError = agentResult.terminationReason === "killed"
|
|
271
|
+
state.finalStatus = "canceled";
|
|
272
|
+
state.finalError = agentResult.terminationReason === "killed"
|
|
211
273
|
? "Task was terminated by kill_task."
|
|
212
274
|
: "Task was canceled by user request.";
|
|
213
275
|
} else if (agentResult.terminationReason === "timeout") {
|
|
214
|
-
finalError = `Task timed out after ${timeoutSeconds} seconds during agent execution.`;
|
|
276
|
+
state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
|
|
215
277
|
} else if (agentResult.spawnError) {
|
|
216
|
-
finalError = `Agent spawn failed: ${agentResult.spawnError}`;
|
|
278
|
+
state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
|
|
217
279
|
} else if (agentResult.exitCode !== 0) {
|
|
218
|
-
finalError = `Agent exited with code ${agentResult.exitCode}.`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
280
|
+
state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
state.lastCaughtError = error;
|
|
284
|
+
state.finalError = errorMessage(error);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return state;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function runVerification(ctx: TaskContext, state: ExecutionState): Promise<ExecutionState> {
|
|
291
|
+
if (state.finalError !== null) return state;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
if (ctx.verifyCommands.length > 0) {
|
|
295
|
+
for (const command of ctx.verifyCommands) {
|
|
296
|
+
setTaskPhase(ctx.taskDir, "running_tests", command);
|
|
297
|
+
const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
|
|
298
|
+
state.verifyResults.push(verification);
|
|
299
|
+
if (verification.terminationReason || Date.now() >= ctx.deadlineMs) break;
|
|
225
300
|
}
|
|
226
|
-
testResult = verifyResults[0] || skippedTest(testCommand, repoPath, "No verification command ran.");
|
|
227
|
-
const interrupted = verifyResults.find((result) => result.terminationReason);
|
|
228
|
-
const failedVerification = verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
|
|
301
|
+
state.testResult = state.verifyResults[0] || skippedTest(ctx.testCommand, ctx.repoPath, "No verification command ran.");
|
|
302
|
+
const interrupted = state.verifyResults.find((result) => result.terminationReason);
|
|
303
|
+
const failedVerification = state.verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
|
|
229
304
|
if (interrupted?.terminationReason === "canceled" || interrupted?.terminationReason === "killed") {
|
|
230
|
-
finalStatus = "canceled";
|
|
231
|
-
finalError = interrupted.terminationReason === "killed"
|
|
305
|
+
state.finalStatus = "canceled";
|
|
306
|
+
state.finalError = interrupted.terminationReason === "killed"
|
|
232
307
|
? "Task was terminated by kill_task during verification."
|
|
233
308
|
: "Task was canceled during verification.";
|
|
234
|
-
} else if (interrupted?.terminationReason === "timeout" || Date.now() >= deadlineMs) {
|
|
235
|
-
finalError = `Task timed out after ${timeoutSeconds} seconds during verification.`;
|
|
309
|
+
} else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
|
|
310
|
+
state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
|
|
236
311
|
} else if (failedVerification) {
|
|
237
|
-
finalStatus = "failed_verification";
|
|
238
|
-
finalError = failedVerification.spawnError
|
|
239
|
-
? `Verification command
|
|
240
|
-
: `Verification command
|
|
241
|
-
} else if (verifyResults.length === verifyCommands.length) {
|
|
242
|
-
finalStatus = "done_by_agent";
|
|
312
|
+
state.finalStatus = "failed_verification";
|
|
313
|
+
state.finalError = failedVerification.spawnError
|
|
314
|
+
? `Verification command "${failedVerification.command}" could not start: ${failedVerification.spawnError}`
|
|
315
|
+
: `Verification command "${failedVerification.command}" exited with code ${failedVerification.exitCode}.`;
|
|
316
|
+
} else if (state.verifyResults.length === ctx.verifyCommands.length) {
|
|
317
|
+
state.finalStatus = "done_by_agent";
|
|
243
318
|
} else {
|
|
244
|
-
finalError = "Verification did not complete all configured commands.";
|
|
319
|
+
state.finalError = "Verification did not complete all configured commands.";
|
|
245
320
|
}
|
|
246
321
|
} else {
|
|
247
|
-
testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
|
|
248
|
-
finalStatus = "done_by_agent";
|
|
322
|
+
state.testResult = skippedTest(ctx.testCommand, ctx.repoPath, "No verification command configured.");
|
|
323
|
+
state.finalStatus = "done_by_agent";
|
|
249
324
|
}
|
|
250
325
|
} catch (error) {
|
|
251
|
-
lastCaughtError = error;
|
|
252
|
-
finalError = errorMessage(error);
|
|
326
|
+
state.lastCaughtError = error;
|
|
327
|
+
state.finalError = errorMessage(error);
|
|
253
328
|
}
|
|
254
329
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (finalStatus !== "canceled") {
|
|
330
|
+
return state;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promise<ArtifactEvidence> {
|
|
334
|
+
let cleanupReport: PostTaskCleanupReport = { enabled: true, removed: [], skipped: [], source_files_touched: 0 };
|
|
335
|
+
if (state.finalStatus !== "canceled") {
|
|
262
336
|
try {
|
|
263
|
-
cleanupReport = runPostTaskCleanup(repoPath, taskDir);
|
|
264
|
-
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
337
|
+
cleanupReport = runPostTaskCleanup(ctx.repoPath, ctx.taskDir);
|
|
338
|
+
updateStatus(ctx.taskDir, { cleanup: cleanupReport });
|
|
265
339
|
} catch (error) {
|
|
266
340
|
cleanupReport = {
|
|
267
|
-
enabled: true,
|
|
268
|
-
removed: [],
|
|
341
|
+
enabled: true, removed: [],
|
|
269
342
|
skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
|
|
270
343
|
source_files_touched: 0,
|
|
271
344
|
};
|
|
272
|
-
writeFileSync(join(taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
|
|
273
|
-
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
345
|
+
writeFileSync(join(ctx.taskDir, "post-task-cleanup.json"), JSON.stringify(cleanupReport, null, 2), "utf-8");
|
|
346
|
+
updateStatus(ctx.taskDir, { cleanup: cleanupReport });
|
|
274
347
|
}
|
|
275
348
|
}
|
|
276
|
-
|
|
349
|
+
|
|
350
|
+
setTaskPhase(ctx.taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
277
351
|
const artifactCollectionStartedAt = new Date().toISOString();
|
|
278
352
|
let changes: ChangeArtifacts;
|
|
279
353
|
let artifactStatus: "collected" | "partial" | "failed" | "timeout" = "collected";
|
|
280
354
|
let artifactCollectionError: string | null = null;
|
|
281
355
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
282
356
|
try {
|
|
283
|
-
// Phase 5: Wrap artifact collection with a timeout
|
|
284
357
|
const collectionResult = await Promise.race([
|
|
285
|
-
Promise.resolve().then(() => {
|
|
286
|
-
const afterSnapshot = captureRepoSnapshot(repoPath);
|
|
287
|
-
writeSnapshot(taskDir, "git-after.json", afterSnapshot);
|
|
288
|
-
return buildChangeArtifacts(repoPath, beforeSnapshot, afterSnapshot);
|
|
358
|
+
Promise.resolve().then(async () => {
|
|
359
|
+
const afterSnapshot = await captureRepoSnapshot(ctx.repoPath);
|
|
360
|
+
writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
|
|
361
|
+
return await buildChangeArtifacts(ctx.repoPath, ctx.beforeSnapshot, afterSnapshot);
|
|
289
362
|
}),
|
|
290
363
|
new Promise<never>((_, reject) => {
|
|
291
364
|
timeoutHandle = setTimeout(() => reject(new Error("Artifact collection timed out")), ARTIFACT_COLLECTION_TIMEOUT_MS);
|
|
@@ -295,13 +368,9 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
295
368
|
changes = collectionResult;
|
|
296
369
|
} catch (error) {
|
|
297
370
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
298
|
-
lastCaughtError = error;
|
|
371
|
+
state.lastCaughtError = error;
|
|
299
372
|
artifactCollectionError = errorMessage(error);
|
|
300
|
-
|
|
301
|
-
artifactStatus = "timeout";
|
|
302
|
-
} else {
|
|
303
|
-
artifactStatus = "failed";
|
|
304
|
-
}
|
|
373
|
+
artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
|
|
305
374
|
changes = {
|
|
306
375
|
changed_files: [],
|
|
307
376
|
diff: `(change capture failed: ${artifactCollectionError})\n`,
|
|
@@ -311,17 +380,16 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
311
380
|
additions: 0,
|
|
312
381
|
deletions: 0,
|
|
313
382
|
file_stats: [],
|
|
314
|
-
workspace_dirty_before: beforeSnapshot.workspace_dirty,
|
|
315
|
-
workspace_dirty_after: beforeSnapshot.workspace_dirty,
|
|
383
|
+
workspace_dirty_before: ctx.beforeSnapshot.workspace_dirty,
|
|
384
|
+
workspace_dirty_after: ctx.beforeSnapshot.workspace_dirty,
|
|
316
385
|
patch_mode: "hash_only",
|
|
317
386
|
unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
|
|
318
387
|
artifact_hygiene: emptyArtifactHygiene(),
|
|
319
388
|
};
|
|
320
|
-
|
|
321
|
-
writeFileSync(join(taskDir, "partial_result.md"), [
|
|
389
|
+
writeFileSync(join(ctx.taskDir, "partial_result.md"), [
|
|
322
390
|
"# PatchWarden Partial Result",
|
|
323
391
|
"",
|
|
324
|
-
`Task: ${taskId}`,
|
|
392
|
+
`Task: ${ctx.taskId}`,
|
|
325
393
|
"",
|
|
326
394
|
"## Artifact Collection Status",
|
|
327
395
|
artifactStatus,
|
|
@@ -330,64 +398,90 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
330
398
|
artifactCollectionError,
|
|
331
399
|
"",
|
|
332
400
|
"## Agent Status",
|
|
333
|
-
agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
|
|
401
|
+
state.agentResult ? `Exit code: ${state.agentResult.exitCode}` : "Agent did not run",
|
|
334
402
|
"",
|
|
335
403
|
"## Verification Status",
|
|
336
|
-
verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
404
|
+
state.verifyResults.length > 0 ? state.verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
337
405
|
"",
|
|
338
406
|
"## Note",
|
|
339
407
|
"Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
|
|
340
408
|
"",
|
|
341
409
|
].join("\n"), "utf-8");
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
finalStatus = "failed";
|
|
410
|
+
if (state.finalStatus === "done_by_agent") {
|
|
411
|
+
state.finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
412
|
+
state.finalStatus = "failed";
|
|
346
413
|
} else {
|
|
347
|
-
finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
414
|
+
state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
348
415
|
}
|
|
349
416
|
}
|
|
350
417
|
const artifactCollectionFinishedAt = new Date().toISOString();
|
|
351
418
|
|
|
352
419
|
let outOfScopeChanges: ChangedFile[] = [];
|
|
353
|
-
|
|
420
|
+
const preexistingExternalDirty: ExternalDirtyFile[] = ctx.externalDirtyBaseline;
|
|
354
421
|
let newOutOfScopeChanges: ExternalDirtyFile[] = [];
|
|
355
422
|
try {
|
|
356
|
-
const afterWorkspaceSnapshot = repoPath === wsRoot ? captureRepoSnapshot(repoPath) : captureRepoSnapshot(wsRoot);
|
|
357
|
-
writeSnapshot(taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
358
|
-
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, repoPath, wsRoot);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
outOfScopeChanges = compareSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
|
|
423
|
+
const afterWorkspaceSnapshot = ctx.repoPath === ctx.wsRoot ? await captureRepoSnapshot(ctx.repoPath) : await captureRepoSnapshot(ctx.wsRoot);
|
|
424
|
+
writeSnapshot(ctx.taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
425
|
+
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, ctx.repoPath, ctx.wsRoot);
|
|
426
|
+
newOutOfScopeChanges = findNewExternalDirtyFiles(ctx.externalDirtyBaseline, allExternalDirty);
|
|
427
|
+
outOfScopeChanges = compareSnapshots(ctx.beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
|
|
362
428
|
.filter((file) =>
|
|
363
|
-
!isPathInside(resolve(wsRoot, file.path), repoPath) ||
|
|
364
|
-
Boolean(file.old_path && !isPathInside(resolve(wsRoot, file.old_path), repoPath))
|
|
429
|
+
!isPathInside(resolve(ctx.wsRoot, file.path), ctx.repoPath) ||
|
|
430
|
+
Boolean(file.old_path && !isPathInside(resolve(ctx.wsRoot, file.old_path), ctx.repoPath))
|
|
365
431
|
);
|
|
366
432
|
} catch (error) {
|
|
367
|
-
finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
368
|
-
if (finalStatus === "done_by_agent") finalStatus = "failed";
|
|
433
|
+
state.finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
434
|
+
if (state.finalStatus === "done_by_agent") state.finalStatus = "failed";
|
|
369
435
|
}
|
|
370
436
|
|
|
371
|
-
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
372
437
|
const preexistingWarnings: string[] = preexistingExternalDirty.length > 0
|
|
373
438
|
? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
|
|
374
439
|
: [];
|
|
375
440
|
|
|
441
|
+
applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
|
|
442
|
+
|
|
443
|
+
writeFileSync(join(ctx.taskDir, "git.diff"), changes.diff, "utf-8");
|
|
444
|
+
writeFileSync(join(ctx.taskDir, "diff.patch"), changes.diff, "utf-8");
|
|
445
|
+
writeFileSync(join(ctx.taskDir, "changed-files.json"), JSON.stringify(changes, null, 2), "utf-8");
|
|
446
|
+
const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
|
|
447
|
+
writeFileSync(join(ctx.taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
|
|
448
|
+
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
449
|
+
writeFileSync(join(ctx.taskDir, "file-stats.json"), JSON.stringify({
|
|
450
|
+
task_id: ctx.taskId,
|
|
451
|
+
additions: changes.additions,
|
|
452
|
+
deletions: changes.deletions,
|
|
453
|
+
files: changes.file_stats,
|
|
454
|
+
}, null, 2), "utf-8");
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
|
|
458
|
+
outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
|
|
459
|
+
artifactManifest, changedFileGroups,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function applyScopeViolationVerdict(
|
|
464
|
+
ctx: TaskContext,
|
|
465
|
+
state: ExecutionState,
|
|
466
|
+
changes: ChangeArtifacts,
|
|
467
|
+
newOutOfScopeChanges: ExternalDirtyFile[],
|
|
468
|
+
preexistingExternalDirty: ExternalDirtyFile[],
|
|
469
|
+
): void {
|
|
376
470
|
if (newOutOfScopeChanges.length > 0) {
|
|
377
|
-
finalStatus = "failed_scope_violation";
|
|
378
|
-
finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
379
|
-
writeFileSync(join(taskDir, "rollback-plan.json"), JSON.stringify({
|
|
380
|
-
task_id: taskId,
|
|
471
|
+
state.finalStatus = "failed_scope_violation";
|
|
472
|
+
state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
473
|
+
writeFileSync(join(ctx.taskDir, "rollback-plan.json"), JSON.stringify({
|
|
474
|
+
task_id: ctx.taskId,
|
|
381
475
|
status: "review_required",
|
|
382
476
|
automatic_rollback_performed: false,
|
|
383
477
|
warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
|
|
384
478
|
out_of_scope_changes: newOutOfScopeChanges,
|
|
385
479
|
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
386
480
|
}, null, 2), "utf-8");
|
|
387
|
-
writeFileSync(join(taskDir, "rollback_scope_violation_plan.md"), [
|
|
481
|
+
writeFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
|
|
388
482
|
"# Scope Violation Rollback Plan",
|
|
389
483
|
"",
|
|
390
|
-
`Task: ${taskId}`,
|
|
484
|
+
`Task: ${ctx.taskId}`,
|
|
391
485
|
"",
|
|
392
486
|
"PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
|
|
393
487
|
"",
|
|
@@ -395,47 +489,34 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
395
489
|
...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
|
|
396
490
|
"",
|
|
397
491
|
].join("\n"), "utf-8");
|
|
398
|
-
} else if (changePolicy === "no_changes" && changes.changed_files.length > 0) {
|
|
399
|
-
finalStatus = "failed_policy_violation";
|
|
400
|
-
finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
|
|
492
|
+
} else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
|
|
493
|
+
state.finalStatus = "failed_policy_violation";
|
|
494
|
+
state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
|
|
401
495
|
}
|
|
496
|
+
}
|
|
402
497
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Phase 6: Generate artifact_manifest.json and group changed files
|
|
408
|
-
const artifactManifest: ArtifactManifest = buildArtifactManifest(changes.changed_files, repoPath, taskId);
|
|
409
|
-
writeFileSync(join(taskDir, "artifact_manifest.json"), JSON.stringify(artifactManifest, null, 2), "utf-8");
|
|
410
|
-
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
498
|
+
function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: ArtifactEvidence): TaskRunResult {
|
|
499
|
+
const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
|
|
500
|
+
outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
|
|
501
|
+
artifactManifest, changedFileGroups } = evidence;
|
|
411
502
|
|
|
412
|
-
writeFileSync(join(taskDir, "
|
|
413
|
-
|
|
414
|
-
additions: changes.additions,
|
|
415
|
-
deletions: changes.deletions,
|
|
416
|
-
files: changes.file_stats,
|
|
417
|
-
}, null, 2), "utf-8");
|
|
418
|
-
writeFileSync(join(taskDir, "test.log"), buildTestLog(testResult), "utf-8");
|
|
419
|
-
const verifyJson = buildVerifyJson(verifyCommands, verifyResults, repoPath);
|
|
420
|
-
// Phase 4: Use newOutOfScopeChanges (not outOfScopeChanges) for verify_status
|
|
421
|
-
// to stay consistent with finalStatus. Pre-existing external dirty files
|
|
422
|
-
// that didn't change during the task should NOT cause verify_status to fail.
|
|
503
|
+
writeFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult), "utf-8");
|
|
504
|
+
const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
|
|
423
505
|
if (newOutOfScopeChanges.length > 0) {
|
|
424
506
|
verifyJson.status = "failed";
|
|
425
507
|
verifyJson.failure_reason = "scope_violation";
|
|
426
|
-
} else if (finalStatus === "failed_policy_violation") {
|
|
508
|
+
} else if (state.finalStatus === "failed_policy_violation") {
|
|
427
509
|
verifyJson.status = "failed";
|
|
428
510
|
verifyJson.failure_reason = "change_policy_violation";
|
|
429
511
|
}
|
|
430
|
-
writeFileSync(join(taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
|
|
431
|
-
writeFileSync(join(taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
|
|
512
|
+
writeFileSync(join(ctx.taskDir, "verify.json"), JSON.stringify(verifyJson, null, 2), "utf-8");
|
|
513
|
+
writeFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands), "utf-8");
|
|
432
514
|
|
|
433
|
-
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(finalStatus)) finalStatus = "failed";
|
|
434
|
-
const finalPhase: TaskPhase = finalStatus === "done_by_agent" ? "done_by_agent" : finalStatus;
|
|
435
|
-
const followup = buildFailureFollowup(finalStatus, finalError, verifyJson.commands);
|
|
515
|
+
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus)) state.finalStatus = "failed";
|
|
516
|
+
const finalPhase: TaskPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : (state.finalStatus as TaskPhase);
|
|
517
|
+
const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
|
|
436
518
|
|
|
437
|
-
|
|
438
|
-
const androidDiagnostic = diagnoseAndroidBuild(repoPath);
|
|
519
|
+
const androidDiagnostic = diagnoseAndroidBuild(ctx.repoPath);
|
|
439
520
|
let androidWarning: string | null = null;
|
|
440
521
|
if (androidDiagnostic.status !== "skip") {
|
|
441
522
|
if (androidDiagnostic.status === "fail") {
|
|
@@ -449,13 +530,13 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
449
530
|
}
|
|
450
531
|
|
|
451
532
|
const resultMd = buildResultMarkdown({
|
|
452
|
-
taskId,
|
|
453
|
-
planId,
|
|
454
|
-
agent: agentName,
|
|
455
|
-
status: finalStatus,
|
|
456
|
-
error: finalError,
|
|
457
|
-
agentResult,
|
|
458
|
-
testResult,
|
|
533
|
+
taskId: ctx.taskId,
|
|
534
|
+
planId: ctx.planId,
|
|
535
|
+
agent: ctx.agentName,
|
|
536
|
+
status: state.finalStatus,
|
|
537
|
+
error: state.finalError,
|
|
538
|
+
agentResult: state.agentResult,
|
|
539
|
+
testResult: state.testResult,
|
|
459
540
|
verify: verifyJson,
|
|
460
541
|
changes,
|
|
461
542
|
outOfScopeChanges,
|
|
@@ -466,18 +547,88 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
466
547
|
androidWarning,
|
|
467
548
|
preexistingWarnings,
|
|
468
549
|
});
|
|
469
|
-
writeFileSync(join(taskDir, "result.md"), resultMd, "utf-8");
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
550
|
+
writeFileSync(join(ctx.taskDir, "result.md"), resultMd, "utf-8");
|
|
551
|
+
|
|
552
|
+
const resultJson = buildResultJson({
|
|
553
|
+
ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
|
|
554
|
+
});
|
|
555
|
+
writeFileSync(join(ctx.taskDir, "result.json"), JSON.stringify(resultJson, null, 2), "utf-8");
|
|
556
|
+
|
|
557
|
+
if (state.finalError) {
|
|
558
|
+
const structuredError = errorPayload(state.lastCaughtError);
|
|
559
|
+
writeFileSync(join(ctx.taskDir, "error.log"), JSON.stringify({
|
|
560
|
+
summary: state.finalError,
|
|
561
|
+
...structuredError,
|
|
562
|
+
}, null, 2), "utf-8");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const finishedAt = new Date().toISOString();
|
|
566
|
+
updateStatus(ctx.taskDir, {
|
|
567
|
+
status: state.finalStatus,
|
|
568
|
+
phase: finalPhase,
|
|
569
|
+
current_command: null,
|
|
570
|
+
last_heartbeat_at: finishedAt,
|
|
571
|
+
finished_at: finishedAt,
|
|
572
|
+
error: state.finalError,
|
|
573
|
+
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
574
|
+
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
575
|
+
artifact_status: artifactStatus,
|
|
576
|
+
artifact_collection_error: artifactCollectionError,
|
|
577
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
578
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
579
|
+
cleanup: cleanupReport,
|
|
580
|
+
out_of_scope_changes: outOfScopeChanges,
|
|
581
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
582
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
583
|
+
verify_status: verifyJson.status,
|
|
584
|
+
verify_commands: ctx.verifyCommands,
|
|
585
|
+
diff_available: changes.diff_available,
|
|
586
|
+
diff_truncated: changes.diff_truncated,
|
|
587
|
+
workspace_dirty_before: changes.workspace_dirty_before,
|
|
588
|
+
workspace_dirty_after: changes.workspace_dirty_after,
|
|
589
|
+
workspace_dirty: changes.workspace_dirty_after,
|
|
590
|
+
acceptance_status: state.finalStatus === "done_by_agent" ? "pending" : null,
|
|
591
|
+
});
|
|
592
|
+
writeTaskRuntime(ctx.taskDir, {
|
|
593
|
+
phase: finalPhase,
|
|
594
|
+
current_command: null,
|
|
595
|
+
last_heartbeat_at: finishedAt,
|
|
596
|
+
runner_pid: process.pid,
|
|
597
|
+
child_pid: undefined,
|
|
598
|
+
});
|
|
599
|
+
writeTaskProgress(ctx.taskDir, finalPhase, {
|
|
600
|
+
heartbeatAt: finishedAt,
|
|
601
|
+
note: state.finalError || `Task finished with status ${state.finalStatus}.`,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return { task_id: ctx.taskId, status: state.finalStatus, error: state.finalError };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function buildResultJson(input: {
|
|
608
|
+
ctx: TaskContext;
|
|
609
|
+
state: ExecutionState;
|
|
610
|
+
evidence: ArtifactEvidence;
|
|
611
|
+
verifyJson: VerifyReport;
|
|
612
|
+
followup: ReturnType<typeof buildFailureFollowup>;
|
|
613
|
+
androidDiagnostic: ReturnType<typeof diagnoseAndroidBuild>;
|
|
614
|
+
androidWarning: string | null;
|
|
615
|
+
}): Record<string, unknown> {
|
|
616
|
+
const { ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning } = input;
|
|
617
|
+
const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt,
|
|
618
|
+
artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges,
|
|
619
|
+
preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
task_id: ctx.taskId,
|
|
623
|
+
status: state.finalStatus,
|
|
624
|
+
agent: ctx.agentName,
|
|
625
|
+
workspace_root: ctx.wsRoot,
|
|
626
|
+
repo_path: ctx.initialStatus.repo_path,
|
|
627
|
+
resolved_repo_path: ctx.repoPath,
|
|
628
|
+
plan_source: ctx.initialStatus.plan_source || "saved",
|
|
629
|
+
template: ctx.initialStatus.template || null,
|
|
630
|
+
change_policy: ctx.changePolicy,
|
|
631
|
+
summary: state.finalError || "Agent execution and configured verification completed successfully.",
|
|
481
632
|
changed_files: changes.changed_files,
|
|
482
633
|
changed_file_groups: {
|
|
483
634
|
source_changes: changedFileGroups.source_changes.length,
|
|
@@ -515,70 +666,21 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
515
666
|
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
516
667
|
],
|
|
517
668
|
warnings: [
|
|
518
|
-
...beforeSnapshot.warnings,
|
|
669
|
+
...ctx.beforeSnapshot.warnings,
|
|
519
670
|
...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
|
|
520
|
-
...preexistingWarnings,
|
|
671
|
+
...evidence.preexistingWarnings,
|
|
521
672
|
...(androidWarning ? [androidWarning] : []),
|
|
522
673
|
],
|
|
523
|
-
errors: finalError ? [finalError] : [],
|
|
524
|
-
known_issues: finalError ? [finalError] : [],
|
|
674
|
+
errors: state.finalError ? [state.finalError] : [],
|
|
675
|
+
known_issues: state.finalError ? [state.finalError] : [],
|
|
525
676
|
failure_reason: followup.failure_reason,
|
|
526
677
|
failed_command: followup.failed_command,
|
|
527
678
|
suggested_next_action: followup.suggested_next_action,
|
|
528
679
|
safe_followup_prompt: followup.safe_followup_prompt,
|
|
529
|
-
next_steps: finalStatus === "done_by_agent"
|
|
680
|
+
next_steps: state.finalStatus === "done_by_agent"
|
|
530
681
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
531
682
|
: ["Resolve the reported failure before accepting the work."],
|
|
532
|
-
}
|
|
533
|
-
if (finalError) {
|
|
534
|
-
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
535
|
-
const structuredError = errorPayload(lastCaughtError);
|
|
536
|
-
writeFileSync(join(taskDir, "error.log"), JSON.stringify({
|
|
537
|
-
summary: finalError,
|
|
538
|
-
...structuredError,
|
|
539
|
-
}, null, 2), "utf-8");
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const finishedAt = new Date().toISOString();
|
|
543
|
-
updateStatus(taskDir, {
|
|
544
|
-
status: finalStatus,
|
|
545
|
-
phase: finalPhase,
|
|
546
|
-
current_command: null,
|
|
547
|
-
last_heartbeat_at: finishedAt,
|
|
548
|
-
finished_at: finishedAt,
|
|
549
|
-
error: finalError,
|
|
550
|
-
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
551
|
-
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
552
|
-
artifact_status: artifactStatus,
|
|
553
|
-
artifact_collection_error: artifactCollectionError,
|
|
554
|
-
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
555
|
-
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
556
|
-
cleanup: cleanupReport,
|
|
557
|
-
out_of_scope_changes: outOfScopeChanges,
|
|
558
|
-
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
559
|
-
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
560
|
-
verify_status: verifyJson.status,
|
|
561
|
-
verify_commands: verifyCommands,
|
|
562
|
-
diff_available: changes.diff_available,
|
|
563
|
-
diff_truncated: changes.diff_truncated,
|
|
564
|
-
workspace_dirty_before: changes.workspace_dirty_before,
|
|
565
|
-
workspace_dirty_after: changes.workspace_dirty_after,
|
|
566
|
-
workspace_dirty: changes.workspace_dirty_after,
|
|
567
|
-
acceptance_status: finalStatus === "done_by_agent" ? "pending" : null,
|
|
568
|
-
});
|
|
569
|
-
writeTaskRuntime(taskDir, {
|
|
570
|
-
phase: finalPhase,
|
|
571
|
-
current_command: null,
|
|
572
|
-
last_heartbeat_at: finishedAt,
|
|
573
|
-
runner_pid: process.pid,
|
|
574
|
-
child_pid: undefined,
|
|
575
|
-
});
|
|
576
|
-
writeTaskProgress(taskDir, finalPhase, {
|
|
577
|
-
heartbeatAt: finishedAt,
|
|
578
|
-
note: finalError || `Task finished with status ${finalStatus}.`,
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
return { task_id: taskId, status: finalStatus, error: finalError };
|
|
683
|
+
};
|
|
582
684
|
}
|
|
583
685
|
|
|
584
686
|
function buildFailureFollowup(
|
|
@@ -724,8 +826,8 @@ async function runManagedProcess(options: {
|
|
|
724
826
|
// force-resolve to prevent the runner from hanging indefinitely.
|
|
725
827
|
// Only starts AFTER termination is requested — normal long tasks are unaffected.
|
|
726
828
|
fallbackTimer = setTimeout(() => {
|
|
727
|
-
try { forceKill(child); } catch {}
|
|
728
|
-
try { child.kill("SIGKILL"); } catch {}
|
|
829
|
+
try { forceKill(child); } catch {} // cleanup failure is safe to ignore
|
|
830
|
+
try { child.kill("SIGKILL"); } catch {} // cleanup failure is safe to ignore
|
|
729
831
|
// Resolve the exit promise via the shared finish handle
|
|
730
832
|
if (childExitFinish) childExitFinish(null);
|
|
731
833
|
}, 10000);
|
|
@@ -1153,7 +1255,7 @@ function gracefulKill(child: ChildProcess): void {
|
|
|
1153
1255
|
try {
|
|
1154
1256
|
if (process.platform !== "win32" && child.pid) process.kill(-child.pid, "SIGTERM");
|
|
1155
1257
|
else child.kill("SIGTERM");
|
|
1156
|
-
} catch {}
|
|
1258
|
+
} catch {} // cleanup failure is safe to ignore
|
|
1157
1259
|
}
|
|
1158
1260
|
|
|
1159
1261
|
function forceKill(child: ChildProcess): void {
|
|
@@ -1170,7 +1272,7 @@ function forceKill(child: ChildProcess): void {
|
|
|
1170
1272
|
process.kill(-child.pid, "SIGKILL");
|
|
1171
1273
|
}
|
|
1172
1274
|
} catch {
|
|
1173
|
-
try { child.kill("SIGKILL"); } catch {}
|
|
1275
|
+
try { child.kill("SIGKILL"); } catch {} // cleanup failure is safe to ignore
|
|
1174
1276
|
}
|
|
1175
1277
|
}
|
|
1176
1278
|
|