patchwarden 1.5.0 → 1.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 +32 -2
- package/README.en.md +194 -26
- package/README.md +160 -22
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +24 -15
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +44 -36
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +98 -11
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +16 -0
- package/dist/control/middleware/auth.js +17 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +78 -0
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +287 -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 +12 -0
- package/dist/control/routes/process.js +487 -0
- package/dist/control/routes/sessions.d.ts +30 -0
- package/dist/control/routes/sessions.js +287 -0
- package/dist/control/routes/status.d.ts +51 -0
- package/dist/control/routes/status.js +307 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +197 -0
- package/dist/control/routes/tasks.d.ts +24 -0
- package/dist/control/routes/tasks.js +309 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +220 -0
- package/dist/control/runtime.d.ts +92 -0
- package/dist/control/runtime.js +425 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +150 -0
- package/dist/control/shared.d.ts +59 -0
- package/dist/control/shared.js +341 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +72 -37
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +614 -373
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +197 -0
- package/dist/goal/goalStatus.d.ts +8 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +221 -0
- package/dist/goal/subgoalSync.js +13 -10
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +126 -44
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +13 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +14 -6
- package/dist/runner/changeCapture.js +287 -81
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +60 -7
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +339 -287
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +52 -38
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +265 -54
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +24 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +280 -273
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +283 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +116 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +92 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +46 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +10 -14
- package/dist/tools/registry.js +47 -1651
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
- package/dist/tools/tasks/evidencePack.js +387 -0
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +1648 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +23 -0
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/desktop-app.md +175 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +168 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +82 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +97 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/examples/config.example.json +4 -2
- package/package.json +28 -11
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +447 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +39 -33
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +30 -4
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/scripts/generate-demo-gif.py +320 -0
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +29 -15
- package/src/assessments/assessmentStore.ts +55 -43
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +105 -11
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +35 -0
- package/src/control/middleware/static.ts +84 -0
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +329 -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 +505 -0
- package/src/control/routes/sessions.ts +314 -0
- package/src/control/routes/status.ts +405 -0
- package/src/control/routes/taskActions.ts +214 -0
- package/src/control/routes/tasks.ts +331 -0
- package/src/control/routes/workspace.ts +234 -0
- package/src/control/runtime.ts +508 -0
- package/src/control/server.ts +165 -0
- package/src/control/shared.ts +360 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +100 -51
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +766 -492
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +263 -0
- package/src/goal/goalStatus.ts +25 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +15 -10
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +130 -44
- package/src/index.ts +7 -4
- package/src/logging.ts +20 -4
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +342 -80
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +55 -7
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +441 -292
- package/src/runner/simpleProcess.ts +68 -33
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +290 -57
- package/src/security/contentRedaction.ts +31 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +276 -273
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +379 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
- package/src/tools/dispatch/directDispatch.ts +168 -0
- package/src/tools/dispatch/goalDispatch.ts +128 -0
- package/src/tools/dispatch/releaseDispatch.ts +66 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +54 -1858
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/tasks/evidencePack.ts +504 -0
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +204 -3
- package/ui/pages/dashboard.html +937 -79
- package/ui/pages/direct-sessions.html +609 -60
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +468 -438
- package/ui/pages/tasks.html +648 -65
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -213
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -130
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -190
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -109
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/evidencePack.js +0 -168
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -297
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -142
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -243
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -150
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/evidencePack.ts +0 -205
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
package/src/runner/runTask.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
2
|
import {
|
|
3
|
-
createWriteStream,
|
|
4
3
|
existsSync,
|
|
5
4
|
readFileSync,
|
|
6
|
-
writeFileSync,
|
|
7
|
-
type WriteStream,
|
|
8
5
|
} from "node:fs";
|
|
9
6
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
10
7
|
import { getTasksDir, getPlansDir, getConfig, resolveWorkspaceRoot } from "../config.js";
|
|
@@ -12,11 +9,17 @@ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
|
12
9
|
import {
|
|
13
10
|
guardTestCommand,
|
|
14
11
|
} from "../security/commandGuard.js";
|
|
15
|
-
import { writeTaskProgress } from "
|
|
16
|
-
import { writeTaskRuntime } from "
|
|
12
|
+
import { writeTaskProgress } from "./taskProgress.js";
|
|
13
|
+
import { writeTaskRuntime } from "./taskRuntime.js";
|
|
14
|
+
import {
|
|
15
|
+
claimPendingTask,
|
|
16
|
+
readTaskStatusFile,
|
|
17
|
+
updateTaskStatusFile,
|
|
18
|
+
type TaskStatusRecord,
|
|
19
|
+
} from "./taskStatusStore.js";
|
|
17
20
|
import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
|
|
18
21
|
import { buildAgentInvocation, buildExecutionPrompt } from "./agentInvocation.js";
|
|
19
|
-
import type { TaskPhase, TaskStatus } from "../tools/createTask.js";
|
|
22
|
+
import type { TaskPhase, TaskStatus } from "../tools/tasks/createTask.js";
|
|
20
23
|
import {
|
|
21
24
|
buildChangeArtifacts,
|
|
22
25
|
captureRepoSnapshot,
|
|
@@ -31,10 +34,22 @@ import {
|
|
|
31
34
|
type ChangeArtifacts,
|
|
32
35
|
type ExternalDirtyFile,
|
|
33
36
|
type ArtifactManifest,
|
|
37
|
+
type RepoSnapshot,
|
|
38
|
+
type ChangedFileGroups,
|
|
34
39
|
} from "./changeCapture.js";
|
|
35
40
|
import { PatchWardenError, errorPayload } from "../errors.js";
|
|
36
|
-
import {
|
|
41
|
+
import { ARTIFACT_SCHEMA_VERSION } from "../version.js";
|
|
42
|
+
import { diagnoseAndroidBuild } from "../tools/workspace/androidDoctor.js";
|
|
37
43
|
import { runPostTaskCleanup, type PostTaskCleanupReport } from "./postTaskCleanup.js";
|
|
44
|
+
import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
45
|
+
import {
|
|
46
|
+
allowedEnvironmentValues,
|
|
47
|
+
buildChildEnvironment,
|
|
48
|
+
redactProcessOutput,
|
|
49
|
+
resolvePackageManagerInvocation,
|
|
50
|
+
resolveTrustedExecutable,
|
|
51
|
+
SecureProcessLogCapture,
|
|
52
|
+
} from "./processSecurity.js";
|
|
38
53
|
|
|
39
54
|
const HEARTBEAT_INTERVAL_MS = 2000;
|
|
40
55
|
const GRACEFUL_KILL_MS = 2000;
|
|
@@ -85,7 +100,63 @@ interface VerifyReport {
|
|
|
85
100
|
failure_reason?: string;
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
interface TaskContext {
|
|
104
|
+
taskId: string;
|
|
105
|
+
taskDir: string;
|
|
106
|
+
statusFile: string;
|
|
107
|
+
repoPath: string;
|
|
108
|
+
wsRoot: string;
|
|
109
|
+
config: ReturnType<typeof getConfig>;
|
|
110
|
+
plansDir: string;
|
|
111
|
+
initialStatus: TaskStatusRecord;
|
|
112
|
+
planId: string;
|
|
113
|
+
agentName: string;
|
|
114
|
+
testCommand: string;
|
|
115
|
+
changePolicy: string;
|
|
116
|
+
verifyCommands: string[];
|
|
117
|
+
timeoutSeconds: number;
|
|
118
|
+
startedAtMs: number;
|
|
119
|
+
deadlineMs: number;
|
|
120
|
+
beforeSnapshot: RepoSnapshot;
|
|
121
|
+
beforeWorkspaceSnapshot: RepoSnapshot;
|
|
122
|
+
externalDirtyBaseline: ExternalDirtyFile[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface ExecutionState {
|
|
126
|
+
agentResult: ManagedProcessResult | null;
|
|
127
|
+
testResult: TestExecutionResult;
|
|
128
|
+
verifyResults: TestExecutionResult[];
|
|
129
|
+
finalStatus: TaskStatus;
|
|
130
|
+
finalError: string | null;
|
|
131
|
+
lastCaughtError: unknown;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface ArtifactEvidence {
|
|
135
|
+
changes: ChangeArtifacts;
|
|
136
|
+
artifactStatus: "collected" | "partial" | "failed" | "timeout";
|
|
137
|
+
artifactCollectionError: string | null;
|
|
138
|
+
artifactCollectionStartedAt: string;
|
|
139
|
+
artifactCollectionFinishedAt: string;
|
|
140
|
+
outOfScopeChanges: ChangedFile[];
|
|
141
|
+
newOutOfScopeChanges: ExternalDirtyFile[];
|
|
142
|
+
preexistingExternalDirty: ExternalDirtyFile[];
|
|
143
|
+
preexistingWarnings: string[];
|
|
144
|
+
cleanupReport: PostTaskCleanupReport;
|
|
145
|
+
artifactManifest: ArtifactManifest;
|
|
146
|
+
changedFileGroups: ChangedFileGroups;
|
|
147
|
+
}
|
|
148
|
+
|
|
88
149
|
export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
150
|
+
const prepared = await prepareTask(taskId);
|
|
151
|
+
if (!("taskDir" in prepared)) return prepared;
|
|
152
|
+
const ctx: TaskContext = prepared;
|
|
153
|
+
const state = await executeAgent(ctx);
|
|
154
|
+
await runVerification(ctx, state);
|
|
155
|
+
const evidence = await collectArtifacts(ctx, state);
|
|
156
|
+
return finalizeTask(ctx, state, evidence);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function prepareTask(taskId: string): Promise<TaskContext | TaskRunResult> {
|
|
89
160
|
const config = getConfig();
|
|
90
161
|
const tasksDir = getTasksDir(config);
|
|
91
162
|
const plansDir = getPlansDir(config);
|
|
@@ -96,7 +167,25 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
96
167
|
const statusFile = join(taskDir, "status.json");
|
|
97
168
|
if (!existsSync(statusFile)) throw new Error(`Task not found: "${taskId}"`);
|
|
98
169
|
|
|
99
|
-
const
|
|
170
|
+
const startedAtMs = Date.now();
|
|
171
|
+
const claim = claimPendingTask(statusFile, {
|
|
172
|
+
status: "running",
|
|
173
|
+
phase: "preparing",
|
|
174
|
+
started_at: new Date(startedAtMs).toISOString(),
|
|
175
|
+
last_heartbeat_at: new Date(startedAtMs).toISOString(),
|
|
176
|
+
current_command: null,
|
|
177
|
+
error: null,
|
|
178
|
+
});
|
|
179
|
+
if (!claim.claimed) {
|
|
180
|
+
const currentStatus = String(claim.status.status || "failed") as TaskStatus;
|
|
181
|
+
return {
|
|
182
|
+
task_id: taskId,
|
|
183
|
+
status: currentStatus,
|
|
184
|
+
error: `Task is ${currentStatus}; only pending tasks can be claimed for execution.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const initialStatus = claim.status;
|
|
100
189
|
const planId = String(initialStatus.plan_id || "");
|
|
101
190
|
const agentName = String(initialStatus.agent || "");
|
|
102
191
|
const rawRepoPath = String(initialStatus.resolved_repo_path || initialStatus.repo_path || wsRoot);
|
|
@@ -109,7 +198,6 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
109
198
|
} catch (error) {
|
|
110
199
|
return failBeforeExecution(taskId, taskDir, errorMessage(error), error);
|
|
111
200
|
}
|
|
112
|
-
const startedAtMs = Date.now();
|
|
113
201
|
const deadlineMs = startedAtMs + timeoutSeconds * 1000;
|
|
114
202
|
|
|
115
203
|
let repoPath: string;
|
|
@@ -126,11 +214,10 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
126
214
|
return failBeforeExecution(taskId, taskDir, message, error);
|
|
127
215
|
}
|
|
128
216
|
|
|
129
|
-
// ── Assessment freshness revalidation before execution ──
|
|
130
217
|
const assessmentId = String(initialStatus.assessment_id || "");
|
|
131
218
|
if (assessmentId) {
|
|
132
219
|
try {
|
|
133
|
-
const preExecSnapshot = captureRepoSnapshot(repoPath);
|
|
220
|
+
const preExecSnapshot = await captureRepoSnapshot(repoPath);
|
|
134
221
|
const validation = validateAssessmentFreshness(assessmentId, preExecSnapshot);
|
|
135
222
|
if (!validation.valid) {
|
|
136
223
|
const message = `assessment validation failed: ${validation.failure_reason}. Re-run create_task with execution_mode=assess_only to get a fresh assessment_id.`;
|
|
@@ -142,166 +229,171 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
142
229
|
}
|
|
143
230
|
}
|
|
144
231
|
|
|
145
|
-
updateStatus(taskDir, {
|
|
146
|
-
status: "running",
|
|
147
|
-
phase: "preparing",
|
|
148
|
-
started_at: new Date(startedAtMs).toISOString(),
|
|
149
|
-
last_heartbeat_at: new Date().toISOString(),
|
|
150
|
-
timeout_seconds: timeoutSeconds,
|
|
151
|
-
current_command: null,
|
|
152
|
-
error: null,
|
|
153
|
-
});
|
|
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.
|
|
232
|
+
updateStatus(taskDir, { timeout_seconds: timeoutSeconds });
|
|
158
233
|
writeTaskRuntime(taskDir, {
|
|
159
234
|
task_started_at: new Date(startedAtMs).toISOString(),
|
|
160
235
|
watcher_instance_id: process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || undefined,
|
|
161
236
|
});
|
|
162
237
|
setTaskPhase(taskDir, "preparing", null, "Capturing pre-task repository state.");
|
|
163
238
|
|
|
164
|
-
let beforeSnapshot;
|
|
165
|
-
let beforeWorkspaceSnapshot;
|
|
239
|
+
let beforeSnapshot: RepoSnapshot;
|
|
240
|
+
let beforeWorkspaceSnapshot: RepoSnapshot;
|
|
166
241
|
let externalDirtyBaseline: ExternalDirtyFile[] = [];
|
|
167
242
|
try {
|
|
168
|
-
beforeSnapshot = captureRepoSnapshot(repoPath);
|
|
169
|
-
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : captureRepoSnapshot(wsRoot);
|
|
243
|
+
beforeSnapshot = await captureRepoSnapshot(repoPath);
|
|
244
|
+
beforeWorkspaceSnapshot = repoPath === wsRoot ? beforeSnapshot : await captureRepoSnapshot(wsRoot);
|
|
170
245
|
writeSnapshot(taskDir, "git-before.json", beforeSnapshot);
|
|
171
246
|
writeSnapshot(taskDir, "workspace-before.json", beforeWorkspaceSnapshot);
|
|
172
|
-
// Phase 4: Record external dirty files as baseline before task execution
|
|
173
247
|
externalDirtyBaseline = extractExternalDirtyFiles(beforeWorkspaceSnapshot, repoPath, wsRoot);
|
|
174
248
|
} catch (error) {
|
|
175
249
|
const message = `Pre-task snapshot failed: ${errorMessage(error)}`;
|
|
176
250
|
return failBeforeExecution(taskId, taskDir, message, error);
|
|
177
251
|
}
|
|
178
252
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
253
|
+
return {
|
|
254
|
+
taskId, taskDir, statusFile, repoPath, wsRoot, config, plansDir, initialStatus,
|
|
255
|
+
planId, agentName, testCommand, changePolicy, verifyCommands, timeoutSeconds,
|
|
256
|
+
startedAtMs, deadlineMs, beforeSnapshot, beforeWorkspaceSnapshot, externalDirtyBaseline,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function executeAgent(ctx: TaskContext): Promise<ExecutionState> {
|
|
261
|
+
const state: ExecutionState = {
|
|
262
|
+
agentResult: null,
|
|
263
|
+
testResult: skippedTest(ctx.testCommand, ctx.repoPath, "Agent did not complete successfully."),
|
|
264
|
+
verifyResults: [],
|
|
265
|
+
finalStatus: "failed",
|
|
266
|
+
finalError: null,
|
|
267
|
+
lastCaughtError: null,
|
|
268
|
+
};
|
|
185
269
|
|
|
186
270
|
try {
|
|
187
|
-
const planFile = resolve(plansDir, planId, "plan.md");
|
|
188
|
-
if (!existsSync(planFile)) throw new Error(`Plan not found: "${planId}". Save the plan first.`);
|
|
271
|
+
const planFile = resolve(ctx.plansDir, ctx.planId, "plan.md");
|
|
272
|
+
if (!existsSync(planFile)) throw new Error(`Plan not found: "${ctx.planId}". Save the plan first.`);
|
|
189
273
|
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;
|
|
274
|
+
const prompt = buildExecutionPrompt(planContent, ctx.repoPath, ctx.testCommand);
|
|
275
|
+
const invocation = buildAgentInvocation(ctx.agentName, ctx.repoPath, prompt, ctx.config);
|
|
193
276
|
|
|
194
|
-
setTaskPhase(taskDir, "executing_agent",
|
|
195
|
-
agentResult = await runManagedProcess({
|
|
277
|
+
setTaskPhase(ctx.taskDir, "executing_agent", invocation.commandLabel);
|
|
278
|
+
state.agentResult = await runManagedProcess({
|
|
196
279
|
command: invocation.command,
|
|
197
280
|
args: invocation.args,
|
|
198
|
-
cwd: repoPath,
|
|
199
|
-
taskDir,
|
|
200
|
-
statusFile,
|
|
281
|
+
cwd: ctx.repoPath,
|
|
282
|
+
taskDir: ctx.taskDir,
|
|
283
|
+
statusFile: ctx.statusFile,
|
|
201
284
|
phase: "executing_agent",
|
|
202
|
-
currentCommand:
|
|
203
|
-
deadlineMs,
|
|
204
|
-
stdoutPath: join(taskDir, "stdout.log"),
|
|
205
|
-
stderrPath: join(taskDir, "stderr.log"),
|
|
285
|
+
currentCommand: invocation.commandLabel,
|
|
286
|
+
deadlineMs: ctx.deadlineMs,
|
|
287
|
+
stdoutPath: join(ctx.taskDir, "stdout.log"),
|
|
288
|
+
stderrPath: join(ctx.taskDir, "stderr.log"),
|
|
289
|
+
environmentVariableNames: invocation.environmentVariableNames,
|
|
290
|
+
blockedEnvironmentVariableNames: invocation.blockedEnvironmentVariableNames,
|
|
206
291
|
});
|
|
207
292
|
|
|
293
|
+
const agentResult = state.agentResult;
|
|
208
294
|
if (agentResult.terminationReason === "canceled" || agentResult.terminationReason === "killed") {
|
|
209
|
-
finalStatus = "canceled";
|
|
210
|
-
finalError = agentResult.terminationReason === "killed"
|
|
295
|
+
state.finalStatus = "canceled";
|
|
296
|
+
state.finalError = agentResult.terminationReason === "killed"
|
|
211
297
|
? "Task was terminated by kill_task."
|
|
212
298
|
: "Task was canceled by user request.";
|
|
213
299
|
} else if (agentResult.terminationReason === "timeout") {
|
|
214
|
-
finalError = `Task timed out after ${timeoutSeconds} seconds during agent execution.`;
|
|
300
|
+
state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during agent execution.`;
|
|
215
301
|
} else if (agentResult.spawnError) {
|
|
216
|
-
finalError = `Agent spawn failed: ${agentResult.spawnError}`;
|
|
302
|
+
state.finalError = `Agent spawn failed: ${agentResult.spawnError}`;
|
|
217
303
|
} else if (agentResult.exitCode !== 0) {
|
|
218
|
-
finalError = `Agent exited with code ${agentResult.exitCode}.`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
304
|
+
state.finalError = `Agent exited with code ${agentResult.exitCode}.`;
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
state.lastCaughtError = error;
|
|
308
|
+
state.finalError = errorMessage(error);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return state;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function runVerification(ctx: TaskContext, state: ExecutionState): Promise<ExecutionState> {
|
|
315
|
+
if (state.finalError !== null) return state;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
if (ctx.verifyCommands.length > 0) {
|
|
319
|
+
for (const command of ctx.verifyCommands) {
|
|
320
|
+
setTaskPhase(ctx.taskDir, "running_tests", command);
|
|
321
|
+
const verification = await runTrustedTestCommand(command, ctx.repoPath, ctx.taskDir, ctx.statusFile, ctx.deadlineMs);
|
|
322
|
+
state.verifyResults.push(verification);
|
|
323
|
+
if (verification.terminationReason || Date.now() >= ctx.deadlineMs) break;
|
|
225
324
|
}
|
|
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);
|
|
325
|
+
state.testResult = state.verifyResults[0] || skippedTest(ctx.testCommand, ctx.repoPath, "No verification command ran.");
|
|
326
|
+
const interrupted = state.verifyResults.find((result) => result.terminationReason);
|
|
327
|
+
const failedVerification = state.verifyResults.find((result) => result.spawnError || result.exitCode !== 0);
|
|
229
328
|
if (interrupted?.terminationReason === "canceled" || interrupted?.terminationReason === "killed") {
|
|
230
|
-
finalStatus = "canceled";
|
|
231
|
-
finalError = interrupted.terminationReason === "killed"
|
|
329
|
+
state.finalStatus = "canceled";
|
|
330
|
+
state.finalError = interrupted.terminationReason === "killed"
|
|
232
331
|
? "Task was terminated by kill_task during verification."
|
|
233
332
|
: "Task was canceled during verification.";
|
|
234
|
-
} else if (interrupted?.terminationReason === "timeout" || Date.now() >= deadlineMs) {
|
|
235
|
-
finalError = `Task timed out after ${timeoutSeconds} seconds during verification.`;
|
|
333
|
+
} else if (interrupted?.terminationReason === "timeout" || Date.now() >= ctx.deadlineMs) {
|
|
334
|
+
state.finalError = `Task timed out after ${ctx.timeoutSeconds} seconds during verification.`;
|
|
236
335
|
} 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";
|
|
336
|
+
state.finalStatus = "failed_verification";
|
|
337
|
+
state.finalError = failedVerification.spawnError
|
|
338
|
+
? `Verification command "${failedVerification.command}" could not start: ${failedVerification.spawnError}`
|
|
339
|
+
: `Verification command "${failedVerification.command}" exited with code ${failedVerification.exitCode}.`;
|
|
340
|
+
} else if (state.verifyResults.length === ctx.verifyCommands.length) {
|
|
341
|
+
state.finalStatus = "done_by_agent";
|
|
243
342
|
} else {
|
|
244
|
-
finalError = "Verification did not complete all configured commands.";
|
|
343
|
+
state.finalError = "Verification did not complete all configured commands.";
|
|
245
344
|
}
|
|
246
345
|
} else {
|
|
247
|
-
testResult = skippedTest(testCommand, repoPath, "No verification command configured.");
|
|
248
|
-
finalStatus = "done_by_agent";
|
|
346
|
+
state.testResult = skippedTest(ctx.testCommand, ctx.repoPath, "No verification command configured.");
|
|
347
|
+
state.finalStatus = "done_by_agent";
|
|
249
348
|
}
|
|
250
349
|
} catch (error) {
|
|
251
|
-
lastCaughtError = error;
|
|
252
|
-
finalError = errorMessage(error);
|
|
350
|
+
state.lastCaughtError = error;
|
|
351
|
+
state.finalError = errorMessage(error);
|
|
253
352
|
}
|
|
254
353
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (finalStatus !== "canceled") {
|
|
354
|
+
return state;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function collectArtifacts(ctx: TaskContext, state: ExecutionState): Promise<ArtifactEvidence> {
|
|
358
|
+
let cleanupReport: PostTaskCleanupReport = { enabled: true, removed: [], skipped: [], source_files_touched: 0 };
|
|
359
|
+
if (state.finalStatus !== "canceled") {
|
|
262
360
|
try {
|
|
263
|
-
cleanupReport = runPostTaskCleanup(repoPath, taskDir);
|
|
264
|
-
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
361
|
+
cleanupReport = runPostTaskCleanup(ctx.repoPath, ctx.taskDir);
|
|
362
|
+
updateStatus(ctx.taskDir, { cleanup: cleanupReport });
|
|
265
363
|
} catch (error) {
|
|
266
364
|
cleanupReport = {
|
|
267
|
-
enabled: true,
|
|
268
|
-
removed: [],
|
|
365
|
+
enabled: true, removed: [],
|
|
269
366
|
skipped: [{ path: ".", reason: "post_task_cleanup", skip_reason: errorMessage(error) }],
|
|
270
367
|
source_files_touched: 0,
|
|
271
368
|
};
|
|
272
|
-
|
|
273
|
-
updateStatus(taskDir, { cleanup: cleanupReport });
|
|
369
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "post-task-cleanup.json"), cleanupReport);
|
|
370
|
+
updateStatus(ctx.taskDir, { cleanup: cleanupReport });
|
|
274
371
|
}
|
|
275
372
|
}
|
|
276
|
-
|
|
373
|
+
|
|
374
|
+
setTaskPhase(ctx.taskDir, "collecting_artifacts", null, "Capturing post-task state and writing reports.");
|
|
277
375
|
const artifactCollectionStartedAt = new Date().toISOString();
|
|
278
376
|
let changes: ChangeArtifacts;
|
|
279
377
|
let artifactStatus: "collected" | "partial" | "failed" | "timeout" = "collected";
|
|
280
378
|
let artifactCollectionError: string | null = null;
|
|
281
379
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
380
|
+
const artifactController = new AbortController();
|
|
282
381
|
try {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
295
|
-
changes = collectionResult;
|
|
382
|
+
timeoutHandle = setTimeout(() => {
|
|
383
|
+
artifactController.abort(new Error("Artifact collection timed out"));
|
|
384
|
+
}, ARTIFACT_COLLECTION_TIMEOUT_MS);
|
|
385
|
+
const afterSnapshot = await captureRepoSnapshot(ctx.repoPath, artifactController.signal);
|
|
386
|
+
writeSnapshot(ctx.taskDir, "git-after.json", afterSnapshot);
|
|
387
|
+
changes = await buildChangeArtifacts(
|
|
388
|
+
ctx.repoPath,
|
|
389
|
+
ctx.beforeSnapshot,
|
|
390
|
+
afterSnapshot,
|
|
391
|
+
artifactController.signal,
|
|
392
|
+
);
|
|
296
393
|
} catch (error) {
|
|
297
|
-
|
|
298
|
-
lastCaughtError = error;
|
|
394
|
+
state.lastCaughtError = error;
|
|
299
395
|
artifactCollectionError = errorMessage(error);
|
|
300
|
-
|
|
301
|
-
artifactStatus = "timeout";
|
|
302
|
-
} else {
|
|
303
|
-
artifactStatus = "failed";
|
|
304
|
-
}
|
|
396
|
+
artifactStatus = artifactCollectionError.includes("timed out") ? "timeout" : "failed";
|
|
305
397
|
changes = {
|
|
306
398
|
changed_files: [],
|
|
307
399
|
diff: `(change capture failed: ${artifactCollectionError})\n`,
|
|
@@ -311,17 +403,16 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
311
403
|
additions: 0,
|
|
312
404
|
deletions: 0,
|
|
313
405
|
file_stats: [],
|
|
314
|
-
workspace_dirty_before: beforeSnapshot.workspace_dirty,
|
|
315
|
-
workspace_dirty_after: beforeSnapshot.workspace_dirty,
|
|
406
|
+
workspace_dirty_before: ctx.beforeSnapshot.workspace_dirty,
|
|
407
|
+
workspace_dirty_after: ctx.beforeSnapshot.workspace_dirty,
|
|
316
408
|
patch_mode: "hash_only",
|
|
317
409
|
unavailable_reason: `Change capture failed: ${artifactCollectionError}`,
|
|
318
410
|
artifact_hygiene: emptyArtifactHygiene(),
|
|
319
411
|
};
|
|
320
|
-
|
|
321
|
-
writeFileSync(join(taskDir, "partial_result.md"), [
|
|
412
|
+
atomicWriteFileSync(join(ctx.taskDir, "partial_result.md"), [
|
|
322
413
|
"# PatchWarden Partial Result",
|
|
323
414
|
"",
|
|
324
|
-
`Task: ${taskId}`,
|
|
415
|
+
`Task: ${ctx.taskId}`,
|
|
325
416
|
"",
|
|
326
417
|
"## Artifact Collection Status",
|
|
327
418
|
artifactStatus,
|
|
@@ -330,112 +421,132 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
330
421
|
artifactCollectionError,
|
|
331
422
|
"",
|
|
332
423
|
"## Agent Status",
|
|
333
|
-
agentResult ? `Exit code: ${agentResult.exitCode}` : "Agent did not run",
|
|
424
|
+
state.agentResult ? `Exit code: ${state.agentResult.exitCode}` : "Agent did not run",
|
|
334
425
|
"",
|
|
335
426
|
"## Verification Status",
|
|
336
|
-
verifyResults.length > 0 ? verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
427
|
+
state.verifyResults.length > 0 ? state.verifyResults.map((r) => `- ${r.command}: exit ${r.exitCode}`).join("\n") : "No verification ran",
|
|
337
428
|
"",
|
|
338
429
|
"## Note",
|
|
339
430
|
"Artifact collection did not complete. The task result may be incomplete. Review stdout.log, stderr.log, and verify.json for details.",
|
|
340
431
|
"",
|
|
341
|
-
].join("\n")
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
finalStatus = "failed";
|
|
432
|
+
].join("\n"));
|
|
433
|
+
if (state.finalStatus === "done_by_agent") {
|
|
434
|
+
state.finalError = `Change capture failed: ${artifactCollectionError}`;
|
|
435
|
+
state.finalStatus = "failed";
|
|
346
436
|
} else {
|
|
347
|
-
finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
437
|
+
state.finalError ||= `Change capture failed: ${artifactCollectionError}`;
|
|
348
438
|
}
|
|
439
|
+
} finally {
|
|
440
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
349
441
|
}
|
|
350
442
|
const artifactCollectionFinishedAt = new Date().toISOString();
|
|
351
443
|
|
|
352
444
|
let outOfScopeChanges: ChangedFile[] = [];
|
|
353
|
-
|
|
445
|
+
const preexistingExternalDirty: ExternalDirtyFile[] = ctx.externalDirtyBaseline;
|
|
354
446
|
let newOutOfScopeChanges: ExternalDirtyFile[] = [];
|
|
355
447
|
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)
|
|
448
|
+
const afterWorkspaceSnapshot = ctx.repoPath === ctx.wsRoot ? await captureRepoSnapshot(ctx.repoPath) : await captureRepoSnapshot(ctx.wsRoot);
|
|
449
|
+
writeSnapshot(ctx.taskDir, "workspace-after.json", afterWorkspaceSnapshot);
|
|
450
|
+
const allExternalDirty = extractExternalDirtyFiles(afterWorkspaceSnapshot, ctx.repoPath, ctx.wsRoot);
|
|
451
|
+
newOutOfScopeChanges = findNewExternalDirtyFiles(ctx.externalDirtyBaseline, allExternalDirty);
|
|
452
|
+
outOfScopeChanges = compareSnapshots(ctx.beforeWorkspaceSnapshot, afterWorkspaceSnapshot)
|
|
362
453
|
.filter((file) =>
|
|
363
|
-
!isPathInside(resolve(wsRoot, file.path), repoPath) ||
|
|
364
|
-
Boolean(file.old_path && !isPathInside(resolve(wsRoot, file.old_path), repoPath))
|
|
454
|
+
!isPathInside(resolve(ctx.wsRoot, file.path), ctx.repoPath) ||
|
|
455
|
+
Boolean(file.old_path && !isPathInside(resolve(ctx.wsRoot, file.old_path), ctx.repoPath))
|
|
365
456
|
);
|
|
366
457
|
} catch (error) {
|
|
367
|
-
finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
368
|
-
if (finalStatus === "done_by_agent") finalStatus = "failed";
|
|
458
|
+
state.finalError ||= `Workspace scope capture failed: ${errorMessage(error)}`;
|
|
459
|
+
if (state.finalStatus === "done_by_agent") state.finalStatus = "failed";
|
|
369
460
|
}
|
|
370
461
|
|
|
371
|
-
// Phase 4: Pre-existing external dirty files are warnings, not failures
|
|
372
462
|
const preexistingWarnings: string[] = preexistingExternalDirty.length > 0
|
|
373
463
|
? [`Pre-existing external dirty files (not caused by this task): ${preexistingExternalDirty.length} file(s)`]
|
|
374
464
|
: [];
|
|
375
465
|
|
|
466
|
+
applyScopeViolationVerdict(ctx, state, changes, newOutOfScopeChanges, preexistingExternalDirty);
|
|
467
|
+
|
|
468
|
+
atomicWriteFileSync(join(ctx.taskDir, "git.diff"), changes.diff);
|
|
469
|
+
atomicWriteFileSync(join(ctx.taskDir, "diff.patch"), changes.diff);
|
|
470
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "changed-files.json"), { schema_version: ARTIFACT_SCHEMA_VERSION, ...changes });
|
|
471
|
+
const artifactManifest = await buildArtifactManifest(changes.changed_files, ctx.repoPath, ctx.taskId);
|
|
472
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "artifact_manifest.json"), artifactManifest);
|
|
473
|
+
const changedFileGroups = groupChangedFiles(changes.changed_files);
|
|
474
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "file-stats.json"), {
|
|
475
|
+
task_id: ctx.taskId,
|
|
476
|
+
additions: changes.additions,
|
|
477
|
+
deletions: changes.deletions,
|
|
478
|
+
files: changes.file_stats,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
|
|
483
|
+
outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
|
|
484
|
+
artifactManifest, changedFileGroups,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function applyScopeViolationVerdict(
|
|
489
|
+
ctx: TaskContext,
|
|
490
|
+
state: ExecutionState,
|
|
491
|
+
changes: ChangeArtifacts,
|
|
492
|
+
newOutOfScopeChanges: ExternalDirtyFile[],
|
|
493
|
+
preexistingExternalDirty: ExternalDirtyFile[],
|
|
494
|
+
): void {
|
|
376
495
|
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
|
-
|
|
380
|
-
task_id: taskId,
|
|
496
|
+
state.finalStatus = "failed_scope_violation";
|
|
497
|
+
state.finalError = `Detected ${newOutOfScopeChanges.length} new change(s) outside resolved_repo_path during task execution.`;
|
|
498
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "rollback-plan.json"), {
|
|
499
|
+
task_id: ctx.taskId,
|
|
381
500
|
status: "review_required",
|
|
382
501
|
automatic_rollback_performed: false,
|
|
383
502
|
warning: "Review ownership and concurrent edits before rollback. PatchWarden did not modify or restore these files.",
|
|
384
503
|
out_of_scope_changes: newOutOfScopeChanges,
|
|
385
504
|
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
386
|
-
}
|
|
387
|
-
|
|
505
|
+
});
|
|
506
|
+
atomicWriteFileSync(join(ctx.taskDir, "rollback_scope_violation_plan.md"), [
|
|
388
507
|
"# Scope Violation Rollback Plan",
|
|
389
508
|
"",
|
|
390
|
-
`Task: ${taskId}`,
|
|
509
|
+
`Task: ${ctx.taskId}`,
|
|
391
510
|
"",
|
|
392
511
|
"PatchWarden did not automatically roll back any file. Review concurrent or user-owned edits before acting.",
|
|
393
512
|
"",
|
|
394
513
|
"## New out-of-scope files (caused by this task)",
|
|
395
514
|
...newOutOfScopeChanges.map((file) => `- ${file.change}: ${file.path}`),
|
|
396
515
|
"",
|
|
397
|
-
].join("\n")
|
|
398
|
-
} else if (
|
|
399
|
-
finalStatus = "failed_policy_violation";
|
|
400
|
-
finalError =
|
|
516
|
+
].join("\n"));
|
|
517
|
+
} else if (changes.diff_redacted) {
|
|
518
|
+
state.finalStatus = "failed_policy_violation";
|
|
519
|
+
state.finalError = "Credential-like content was detected in the task diff. PatchWarden redacted the evidence; remove the sensitive content before retrying.";
|
|
520
|
+
} else if (ctx.changePolicy === "no_changes" && changes.changed_files.length > 0) {
|
|
521
|
+
state.finalStatus = "failed_policy_violation";
|
|
522
|
+
state.finalError = `Task policy requires no repository changes, but detected ${changes.changed_files.length} change(s).`;
|
|
401
523
|
}
|
|
524
|
+
}
|
|
402
525
|
|
|
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);
|
|
526
|
+
function finalizeTask(ctx: TaskContext, state: ExecutionState, evidence: ArtifactEvidence): TaskRunResult {
|
|
527
|
+
const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt, artifactCollectionFinishedAt,
|
|
528
|
+
outOfScopeChanges, newOutOfScopeChanges, preexistingExternalDirty, preexistingWarnings, cleanupReport,
|
|
529
|
+
artifactManifest, changedFileGroups } = evidence;
|
|
411
530
|
|
|
412
|
-
|
|
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.
|
|
531
|
+
atomicWriteFileSync(join(ctx.taskDir, "test.log"), buildTestLog(state.testResult));
|
|
532
|
+
const verifyJson = buildVerifyJson(ctx.verifyCommands, state.verifyResults, ctx.repoPath);
|
|
423
533
|
if (newOutOfScopeChanges.length > 0) {
|
|
424
534
|
verifyJson.status = "failed";
|
|
425
535
|
verifyJson.failure_reason = "scope_violation";
|
|
426
|
-
} else if (finalStatus === "failed_policy_violation") {
|
|
536
|
+
} else if (state.finalStatus === "failed_policy_violation") {
|
|
427
537
|
verifyJson.status = "failed";
|
|
428
|
-
verifyJson.failure_reason =
|
|
538
|
+
verifyJson.failure_reason = changes.diff_redacted
|
|
539
|
+
? "sensitive_content_violation"
|
|
540
|
+
: "change_policy_violation";
|
|
429
541
|
}
|
|
430
|
-
|
|
431
|
-
|
|
542
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "verify.json"), verifyJson);
|
|
543
|
+
atomicWriteFileSync(join(ctx.taskDir, "verify.log"), buildVerifyLog(verifyJson.commands));
|
|
432
544
|
|
|
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);
|
|
545
|
+
if (!["canceled", "done_by_agent", "failed_verification", "failed_scope_violation", "failed_policy_violation"].includes(state.finalStatus)) state.finalStatus = "failed";
|
|
546
|
+
const finalPhase: TaskPhase = state.finalStatus === "done_by_agent" ? "done_by_agent" : (state.finalStatus as TaskPhase);
|
|
547
|
+
const followup = buildFailureFollowup(state.finalStatus, state.finalError, verifyJson.commands);
|
|
436
548
|
|
|
437
|
-
|
|
438
|
-
const androidDiagnostic = diagnoseAndroidBuild(repoPath);
|
|
549
|
+
const androidDiagnostic = diagnoseAndroidBuild(ctx.repoPath);
|
|
439
550
|
let androidWarning: string | null = null;
|
|
440
551
|
if (androidDiagnostic.status !== "skip") {
|
|
441
552
|
if (androidDiagnostic.status === "fail") {
|
|
@@ -449,13 +560,13 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
449
560
|
}
|
|
450
561
|
|
|
451
562
|
const resultMd = buildResultMarkdown({
|
|
452
|
-
taskId,
|
|
453
|
-
planId,
|
|
454
|
-
agent: agentName,
|
|
455
|
-
status: finalStatus,
|
|
456
|
-
error: finalError,
|
|
457
|
-
agentResult,
|
|
458
|
-
testResult,
|
|
563
|
+
taskId: ctx.taskId,
|
|
564
|
+
planId: ctx.planId,
|
|
565
|
+
agent: ctx.agentName,
|
|
566
|
+
status: state.finalStatus,
|
|
567
|
+
error: state.finalError,
|
|
568
|
+
agentResult: state.agentResult,
|
|
569
|
+
testResult: state.testResult,
|
|
459
570
|
verify: verifyJson,
|
|
460
571
|
changes,
|
|
461
572
|
outOfScopeChanges,
|
|
@@ -466,18 +577,91 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
466
577
|
androidWarning,
|
|
467
578
|
preexistingWarnings,
|
|
468
579
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
580
|
+
atomicWriteFileSync(join(ctx.taskDir, "result.md"), resultMd);
|
|
581
|
+
|
|
582
|
+
const resultJson = buildResultJson({
|
|
583
|
+
ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning,
|
|
584
|
+
});
|
|
585
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "result.json"), resultJson);
|
|
586
|
+
|
|
587
|
+
if (state.finalError) {
|
|
588
|
+
const structuredError = errorPayload(state.lastCaughtError);
|
|
589
|
+
atomicWriteJsonFileSync(join(ctx.taskDir, "error.log"), {
|
|
590
|
+
summary: state.finalError,
|
|
591
|
+
...structuredError,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const finishedAt = new Date().toISOString();
|
|
596
|
+
updateStatus(ctx.taskDir, {
|
|
597
|
+
status: state.finalStatus,
|
|
598
|
+
phase: finalPhase,
|
|
599
|
+
current_command: null,
|
|
600
|
+
last_heartbeat_at: finishedAt,
|
|
601
|
+
finished_at: finishedAt,
|
|
602
|
+
error: state.finalError,
|
|
603
|
+
changed_files: changes.changed_files.map(({ path, change }) => ({ path, change })),
|
|
604
|
+
artifact_hygiene_counts: changes.artifact_hygiene.counts,
|
|
605
|
+
artifact_status: artifactStatus,
|
|
606
|
+
artifact_collection_error: artifactCollectionError,
|
|
607
|
+
artifact_collection_started_at: artifactCollectionStartedAt,
|
|
608
|
+
artifact_collection_finished_at: artifactCollectionFinishedAt,
|
|
609
|
+
cleanup: cleanupReport,
|
|
610
|
+
out_of_scope_changes: outOfScopeChanges,
|
|
611
|
+
new_out_of_scope_changes: newOutOfScopeChanges,
|
|
612
|
+
preexisting_external_dirty_files: preexistingExternalDirty,
|
|
613
|
+
verify_status: verifyJson.status,
|
|
614
|
+
verify_commands: ctx.verifyCommands,
|
|
615
|
+
diff_available: changes.diff_available,
|
|
616
|
+
diff_truncated: changes.diff_truncated,
|
|
617
|
+
diff_redacted: changes.diff_redacted === true,
|
|
618
|
+
diff_redaction_categories: changes.diff_redaction_categories ?? [],
|
|
619
|
+
workspace_dirty_before: changes.workspace_dirty_before,
|
|
620
|
+
workspace_dirty_after: changes.workspace_dirty_after,
|
|
621
|
+
workspace_dirty: changes.workspace_dirty_after,
|
|
622
|
+
acceptance_status: state.finalStatus === "done_by_agent" ? "pending" : null,
|
|
623
|
+
});
|
|
624
|
+
writeTaskRuntime(ctx.taskDir, {
|
|
625
|
+
phase: finalPhase,
|
|
626
|
+
current_command: null,
|
|
627
|
+
last_heartbeat_at: finishedAt,
|
|
628
|
+
runner_pid: process.pid,
|
|
629
|
+
child_pid: undefined,
|
|
630
|
+
});
|
|
631
|
+
writeTaskProgress(ctx.taskDir, finalPhase, {
|
|
632
|
+
heartbeatAt: finishedAt,
|
|
633
|
+
note: state.finalError || `Task finished with status ${state.finalStatus}.`,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return { task_id: ctx.taskId, status: state.finalStatus, error: state.finalError };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function buildResultJson(input: {
|
|
640
|
+
ctx: TaskContext;
|
|
641
|
+
state: ExecutionState;
|
|
642
|
+
evidence: ArtifactEvidence;
|
|
643
|
+
verifyJson: VerifyReport;
|
|
644
|
+
followup: ReturnType<typeof buildFailureFollowup>;
|
|
645
|
+
androidDiagnostic: ReturnType<typeof diagnoseAndroidBuild>;
|
|
646
|
+
androidWarning: string | null;
|
|
647
|
+
}): Record<string, unknown> {
|
|
648
|
+
const { ctx, state, evidence, verifyJson, followup, androidDiagnostic, androidWarning } = input;
|
|
649
|
+
const { changes, artifactStatus, artifactCollectionError, artifactCollectionStartedAt,
|
|
650
|
+
artifactCollectionFinishedAt, outOfScopeChanges, newOutOfScopeChanges,
|
|
651
|
+
preexistingExternalDirty, cleanupReport, artifactManifest, changedFileGroups } = evidence;
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
655
|
+
task_id: ctx.taskId,
|
|
656
|
+
status: state.finalStatus,
|
|
657
|
+
agent: ctx.agentName,
|
|
658
|
+
workspace_root: ctx.wsRoot,
|
|
659
|
+
repo_path: ctx.initialStatus.repo_path,
|
|
660
|
+
resolved_repo_path: ctx.repoPath,
|
|
661
|
+
plan_source: ctx.initialStatus.plan_source || "saved",
|
|
662
|
+
template: ctx.initialStatus.template || null,
|
|
663
|
+
change_policy: ctx.changePolicy,
|
|
664
|
+
summary: state.finalError || "Agent execution and configured verification completed successfully.",
|
|
481
665
|
changed_files: changes.changed_files,
|
|
482
666
|
changed_file_groups: {
|
|
483
667
|
source_changes: changedFileGroups.source_changes.length,
|
|
@@ -515,70 +699,24 @@ export async function runTask(taskId: string): Promise<TaskRunResult> {
|
|
|
515
699
|
...(artifactStatus !== "collected" ? ["partial_result.md"] : []),
|
|
516
700
|
],
|
|
517
701
|
warnings: [
|
|
518
|
-
...beforeSnapshot.warnings,
|
|
702
|
+
...ctx.beforeSnapshot.warnings,
|
|
519
703
|
...(changes.diff_truncated ? ["diff.patch was truncated; changed-files.json retains file evidence."] : []),
|
|
520
|
-
...
|
|
704
|
+
...(changes.diff_redacted ? [
|
|
705
|
+
`Credential-like diff content was redacted (${(changes.diff_redaction_categories ?? []).join(", ") || "sensitive_content"}).`,
|
|
706
|
+
] : []),
|
|
707
|
+
...evidence.preexistingWarnings,
|
|
521
708
|
...(androidWarning ? [androidWarning] : []),
|
|
522
709
|
],
|
|
523
|
-
errors: finalError ? [finalError] : [],
|
|
524
|
-
known_issues: finalError ? [finalError] : [],
|
|
710
|
+
errors: state.finalError ? [state.finalError] : [],
|
|
711
|
+
known_issues: state.finalError ? [state.finalError] : [],
|
|
525
712
|
failure_reason: followup.failure_reason,
|
|
526
713
|
failed_command: followup.failed_command,
|
|
527
714
|
suggested_next_action: followup.suggested_next_action,
|
|
528
715
|
safe_followup_prompt: followup.safe_followup_prompt,
|
|
529
|
-
next_steps: finalStatus === "done_by_agent"
|
|
716
|
+
next_steps: state.finalStatus === "done_by_agent"
|
|
530
717
|
? ["Review get_task_summary and audit_task before accepting the work."]
|
|
531
718
|
: ["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 };
|
|
719
|
+
};
|
|
582
720
|
}
|
|
583
721
|
|
|
584
722
|
function buildFailureFollowup(
|
|
@@ -651,18 +789,33 @@ async function runManagedProcess(options: {
|
|
|
651
789
|
deadlineMs: number;
|
|
652
790
|
stdoutPath?: string;
|
|
653
791
|
stderrPath?: string;
|
|
792
|
+
environmentVariableNames?: string[];
|
|
793
|
+
blockedEnvironmentVariableNames?: string[];
|
|
794
|
+
maxLogBytes?: number;
|
|
654
795
|
}): Promise<ManagedProcessResult> {
|
|
655
796
|
if (Date.now() >= options.deadlineMs) {
|
|
656
797
|
return { exitCode: null, stdout: "", stderr: "", spawnError: null, terminationReason: "timeout" };
|
|
657
798
|
}
|
|
658
799
|
|
|
800
|
+
const exactRedactionValues = allowedEnvironmentValues(options.environmentVariableNames);
|
|
801
|
+
const logCapture = new SecureProcessLogCapture(
|
|
802
|
+
[options.stdoutPath, options.stderrPath],
|
|
803
|
+
options.maxLogBytes,
|
|
804
|
+
);
|
|
659
805
|
let child: ChildProcess;
|
|
660
806
|
try {
|
|
661
|
-
|
|
807
|
+
const env = buildChildEnvironment({
|
|
808
|
+
cwd: options.cwd,
|
|
809
|
+
allowedNames: options.environmentVariableNames,
|
|
810
|
+
blockedNames: options.blockedEnvironmentVariableNames,
|
|
811
|
+
});
|
|
812
|
+
const command = resolveTrustedExecutable(options.command, options.cwd, { pathValue: env.PATH });
|
|
813
|
+
child = spawn(command, options.args, {
|
|
662
814
|
cwd: options.cwd,
|
|
663
815
|
stdio: ["ignore", "pipe", "pipe"],
|
|
664
816
|
detached: process.platform !== "win32",
|
|
665
817
|
windowsHide: true,
|
|
818
|
+
env,
|
|
666
819
|
});
|
|
667
820
|
} catch (error) {
|
|
668
821
|
return { exitCode: null, stdout: "", stderr: "", spawnError: errorMessage(error), terminationReason: null };
|
|
@@ -674,8 +827,6 @@ async function runManagedProcess(options: {
|
|
|
674
827
|
child_started_at: new Date().toISOString(),
|
|
675
828
|
});
|
|
676
829
|
|
|
677
|
-
const stdoutStream = openStream(options.stdoutPath);
|
|
678
|
-
const stderrStream = openStream(options.stderrPath);
|
|
679
830
|
let stdout = "";
|
|
680
831
|
let stderr = "";
|
|
681
832
|
let spawnError: string | null = null;
|
|
@@ -724,8 +875,8 @@ async function runManagedProcess(options: {
|
|
|
724
875
|
// force-resolve to prevent the runner from hanging indefinitely.
|
|
725
876
|
// Only starts AFTER termination is requested — normal long tasks are unaffected.
|
|
726
877
|
fallbackTimer = setTimeout(() => {
|
|
727
|
-
try { forceKill(child); } catch {}
|
|
728
|
-
try { child.kill("SIGKILL"); } catch {}
|
|
878
|
+
try { forceKill(child); } catch {} // cleanup failure is safe to ignore
|
|
879
|
+
try { child.kill("SIGKILL"); } catch {} // cleanup failure is safe to ignore
|
|
729
880
|
// Resolve the exit promise via the shared finish handle
|
|
730
881
|
if (childExitFinish) childExitFinish(null);
|
|
731
882
|
}, 10000);
|
|
@@ -734,12 +885,12 @@ async function runManagedProcess(options: {
|
|
|
734
885
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
735
886
|
const text = chunk.toString("utf-8");
|
|
736
887
|
stdout = appendBounded(stdout, text);
|
|
737
|
-
|
|
888
|
+
logCapture.append(options.stdoutPath, chunk);
|
|
738
889
|
});
|
|
739
890
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
740
891
|
const text = chunk.toString("utf-8");
|
|
741
892
|
stderr = appendBounded(stderr, text);
|
|
742
|
-
|
|
893
|
+
logCapture.append(options.stderrPath, chunk);
|
|
743
894
|
});
|
|
744
895
|
|
|
745
896
|
heartbeat();
|
|
@@ -755,12 +906,6 @@ async function runManagedProcess(options: {
|
|
|
755
906
|
};
|
|
756
907
|
childExitFinish = finish;
|
|
757
908
|
child.once("close", (code) => finish(code));
|
|
758
|
-
child.once("exit", (code) => {
|
|
759
|
-
// On Windows, "close" can lag behind "exit" when stdio pipes drain
|
|
760
|
-
// after taskkill. Resolve on exit to avoid hanging the runner, but
|
|
761
|
-
// still let "close" fire if it arrives first.
|
|
762
|
-
finish(code);
|
|
763
|
-
});
|
|
764
909
|
child.once("error", (error) => {
|
|
765
910
|
spawnError = error.message;
|
|
766
911
|
finish(null);
|
|
@@ -769,8 +914,7 @@ async function runManagedProcess(options: {
|
|
|
769
914
|
|
|
770
915
|
clearInterval(heartbeatTimer);
|
|
771
916
|
if (forceTimer) clearTimeout(forceTimer);
|
|
772
|
-
|
|
773
|
-
stderrStream?.destroy();
|
|
917
|
+
logCapture.flush(exactRedactionValues);
|
|
774
918
|
writeTaskRuntime(options.taskDir, {
|
|
775
919
|
phase: options.phase,
|
|
776
920
|
last_heartbeat_at: new Date().toISOString(),
|
|
@@ -778,8 +922,16 @@ async function runManagedProcess(options: {
|
|
|
778
922
|
runner_pid: process.pid,
|
|
779
923
|
child_pid: undefined,
|
|
780
924
|
});
|
|
925
|
+
const safeStdout = redactProcessOutput(stdout, exactRedactionValues);
|
|
926
|
+
const safeStderr = redactProcessOutput(stderr, exactRedactionValues);
|
|
781
927
|
|
|
782
|
-
return {
|
|
928
|
+
return {
|
|
929
|
+
exitCode,
|
|
930
|
+
stdout: safeStdout.length > MAX_CAPTURE_CHARS ? safeStdout.slice(-MAX_CAPTURE_CHARS) : safeStdout,
|
|
931
|
+
stderr: safeStderr.length > MAX_CAPTURE_CHARS ? safeStderr.slice(-MAX_CAPTURE_CHARS) : safeStderr,
|
|
932
|
+
spawnError,
|
|
933
|
+
terminationReason,
|
|
934
|
+
};
|
|
783
935
|
}
|
|
784
936
|
|
|
785
937
|
async function runTrustedTestCommand(
|
|
@@ -797,9 +949,9 @@ async function runTrustedTestCommand(
|
|
|
797
949
|
let command = parts[0];
|
|
798
950
|
let args = parts.slice(1);
|
|
799
951
|
if (process.platform === "win32" && /^(npm|npm\.cmd|pnpm|pnpm\.cmd)$/i.test(command)) {
|
|
800
|
-
const
|
|
801
|
-
command =
|
|
802
|
-
args = [
|
|
952
|
+
const packageManager = resolvePackageManagerInvocation(command, repoPath);
|
|
953
|
+
command = packageManager.command;
|
|
954
|
+
args = [...packageManager.argsPrefix, ...args];
|
|
803
955
|
}
|
|
804
956
|
const result = await runManagedProcess({
|
|
805
957
|
command,
|
|
@@ -1059,10 +1211,10 @@ function normalizeTimeout(value: unknown, config: ReturnType<typeof getConfig>):
|
|
|
1059
1211
|
function failBeforeExecution(taskId: string, taskDir: string, message: string, caughtError?: unknown): TaskRunResult {
|
|
1060
1212
|
// Phase 9: Preserve PatchWardenError structured info in error.log
|
|
1061
1213
|
const structuredError = caughtError ? errorPayload(caughtError) : { error: message };
|
|
1062
|
-
|
|
1214
|
+
atomicWriteJsonFileSync(join(taskDir, "error.log"), {
|
|
1063
1215
|
summary: message,
|
|
1064
1216
|
...structuredError,
|
|
1065
|
-
}
|
|
1217
|
+
});
|
|
1066
1218
|
const current = readStatus(join(taskDir, "status.json"));
|
|
1067
1219
|
const now = new Date().toISOString();
|
|
1068
1220
|
const verify: VerifyReport = {
|
|
@@ -1070,17 +1222,17 @@ function failBeforeExecution(taskId: string, taskDir: string, message: string, c
|
|
|
1070
1222
|
requested_commands: Array.isArray(current.verify_commands) ? current.verify_commands : [],
|
|
1071
1223
|
commands: [],
|
|
1072
1224
|
};
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1225
|
+
atomicWriteJsonFileSync(join(taskDir, "verify.json"), verify);
|
|
1226
|
+
atomicWriteFileSync(join(taskDir, "verify.log"), `Verification did not run: ${message}\n`);
|
|
1227
|
+
atomicWriteFileSync(join(taskDir, "test.log"), `(not run)\nExit code: not run\n${message}\n`);
|
|
1228
|
+
atomicWriteFileSync(join(taskDir, "git.diff"), "(task failed before change capture)\n");
|
|
1229
|
+
atomicWriteFileSync(join(taskDir, "diff.patch"), "(task failed before change capture)\n");
|
|
1230
|
+
atomicWriteJsonFileSync(join(taskDir, "file-stats.json"), {
|
|
1079
1231
|
task_id: taskId,
|
|
1080
1232
|
additions: 0,
|
|
1081
1233
|
deletions: 0,
|
|
1082
1234
|
files: [],
|
|
1083
|
-
}
|
|
1235
|
+
});
|
|
1084
1236
|
const result = {
|
|
1085
1237
|
task_id: taskId,
|
|
1086
1238
|
status: "failed",
|
|
@@ -1106,8 +1258,8 @@ function failBeforeExecution(taskId: string, taskDir: string, message: string, c
|
|
|
1106
1258
|
safe_followup_prompt: "Inspect result.json and error.log, correct the task metadata or configuration, and retry without widening repository scope.",
|
|
1107
1259
|
next_steps: ["Fix task metadata or configuration and retry the task."],
|
|
1108
1260
|
};
|
|
1109
|
-
|
|
1110
|
-
|
|
1261
|
+
atomicWriteJsonFileSync(join(taskDir, "result.json"), result);
|
|
1262
|
+
atomicWriteFileSync(join(taskDir, "result.md"), `# PatchWarden Task Result\n\n## Status\nfailed\n\n## Summary\n${message}\n`);
|
|
1111
1263
|
updateStatus(taskDir, {
|
|
1112
1264
|
status: "failed",
|
|
1113
1265
|
phase: "failed",
|
|
@@ -1140,44 +1292,41 @@ function setTaskPhase(
|
|
|
1140
1292
|
|
|
1141
1293
|
function updateStatus(taskDir: string, patch: Record<string, unknown>): void {
|
|
1142
1294
|
const statusFile = join(taskDir, "status.json");
|
|
1143
|
-
|
|
1144
|
-
const next = { ...current, ...patch, updated_at: new Date().toISOString() };
|
|
1145
|
-
writeFileSync(statusFile, JSON.stringify(next, null, 2), "utf-8");
|
|
1295
|
+
updateTaskStatusFile(statusFile, patch);
|
|
1146
1296
|
}
|
|
1147
1297
|
|
|
1148
|
-
function readStatus(statusFile: string):
|
|
1149
|
-
return
|
|
1298
|
+
function readStatus(statusFile: string): TaskStatusRecord {
|
|
1299
|
+
return readTaskStatusFile(statusFile);
|
|
1150
1300
|
}
|
|
1151
1301
|
|
|
1152
1302
|
function gracefulKill(child: ChildProcess): void {
|
|
1153
1303
|
try {
|
|
1154
1304
|
if (process.platform !== "win32" && child.pid) process.kill(-child.pid, "SIGTERM");
|
|
1155
1305
|
else child.kill("SIGTERM");
|
|
1156
|
-
} catch {}
|
|
1306
|
+
} catch {} // cleanup failure is safe to ignore
|
|
1157
1307
|
}
|
|
1158
1308
|
|
|
1159
1309
|
function forceKill(child: ChildProcess): void {
|
|
1160
1310
|
if (!child.pid) return;
|
|
1161
1311
|
try {
|
|
1162
1312
|
if (process.platform === "win32") {
|
|
1163
|
-
const
|
|
1313
|
+
const systemRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
|
|
1314
|
+
const taskkill = resolveTrustedExecutable(`${systemRoot}\\System32\\taskkill.exe`, process.cwd());
|
|
1315
|
+
const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
|
|
1164
1316
|
stdio: "ignore",
|
|
1165
1317
|
timeout: 5000,
|
|
1166
1318
|
windowsHide: true,
|
|
1319
|
+
env: buildChildEnvironment({ cwd: process.cwd() }),
|
|
1167
1320
|
});
|
|
1168
1321
|
if (result.status !== 0) child.kill("SIGKILL");
|
|
1169
1322
|
} else {
|
|
1170
1323
|
process.kill(-child.pid, "SIGKILL");
|
|
1171
1324
|
}
|
|
1172
1325
|
} catch {
|
|
1173
|
-
try { child.kill("SIGKILL"); } catch {}
|
|
1326
|
+
try { child.kill("SIGKILL"); } catch {} // cleanup failure is safe to ignore
|
|
1174
1327
|
}
|
|
1175
1328
|
}
|
|
1176
1329
|
|
|
1177
|
-
function openStream(path?: string): WriteStream | null {
|
|
1178
|
-
return path ? createWriteStream(path, { flags: "a" }) : null;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
1330
|
function appendBounded(current: string, next: string): string {
|
|
1182
1331
|
const combined = current + next;
|
|
1183
1332
|
return combined.length <= MAX_CAPTURE_CHARS
|