patchwarden 1.5.1 → 1.6.2
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 +158 -26
- package/README.md +124 -24
- package/dist/assessments/agentAssessor.js +21 -12
- package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
- package/dist/assessments/assessmentDiagnostics.js +132 -0
- package/dist/assessments/assessmentStore.d.ts +39 -0
- package/dist/assessments/assessmentStore.js +207 -45
- package/dist/assessments/securitySnapshot.d.ts +50 -0
- package/dist/assessments/securitySnapshot.js +158 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +66 -13
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +6 -0
- package/dist/control/middleware/auth.js +14 -5
- package/dist/control/middleware/static.js +16 -3
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.js +17 -7
- package/dist/control/routes/evidence.js +1 -1
- package/dist/control/routes/lineage.js +1 -1
- package/dist/control/routes/process.d.ts +7 -0
- package/dist/control/routes/process.js +329 -42
- package/dist/control/routes/sessions.d.ts +9 -1
- package/dist/control/routes/sessions.js +84 -21
- package/dist/control/routes/status.d.ts +60 -1
- package/dist/control/routes/status.js +94 -15
- package/dist/control/routes/taskActions.js +24 -60
- package/dist/control/routes/tasks.d.ts +4 -0
- package/dist/control/routes/tasks.js +61 -62
- package/dist/control/routes/workspace.js +50 -23
- package/dist/control/runtime.d.ts +3 -2
- package/dist/control/runtime.js +65 -40
- package/dist/control/server.js +37 -365
- package/dist/control/shared.d.ts +25 -1
- package/dist/control/shared.js +64 -11
- package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
- package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +42 -29
- 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 +128 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.js +65 -57
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.js +5 -12
- package/dist/goal/goalStatus.d.ts +2 -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.js +97 -96
- package/dist/goal/subgoalSync.js +11 -9
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +111 -32
- package/dist/logging.js +5 -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 +13 -5
- package/dist/runner/changeCapture.js +243 -61
- package/dist/runner/postTaskCleanup.js +48 -19
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +2 -1
- package/dist/runner/runTask.js +162 -93
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +50 -36
- 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} +3 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +35 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +320 -49
- package/dist/security/contentRedaction.js +2 -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 +27 -26
- 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} +6 -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} +2 -1
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
- 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} +12 -8
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
- 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.js → direct/createDirectSession.js} +6 -6
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
- 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.js +32 -31
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
- package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
- package/dist/tools/dispatch/directDispatch.js +11 -10
- package/dist/tools/dispatch/goalDispatch.js +5 -4
- package/dist/tools/dispatch/releaseDispatch.js +3 -2
- 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} +1 -1
- 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 +7 -11
- package/dist/tools/registry.js +17 -1178
- 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 +86 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
- package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
- 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} +11 -6
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
- 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} +5 -4
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
- package/dist/tools/tasks/reconcileTasks.js +496 -0
- 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} +63 -41
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/tasks/taskStates.d.ts +6 -0
- package/dist/tools/tasks/taskStates.js +52 -0
- package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
- 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/workspace/listAgents.d.ts +23 -0
- package/dist/tools/workspace/listAgents.js +56 -0
- 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 +67 -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 +808 -143
- package/docs/architecture.md +15 -55
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +4 -4
- package/docs/desktop-app.md +175 -0
- package/docs/evidence-pack-schema.md +3 -3
- package/docs/open-source-application.md +16 -11
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +35 -39
- package/docs/task-safe-review-workflow.md +5 -5
- package/docs/threat-model.md +19 -1
- package/examples/config.example.json +4 -2
- package/package.json +27 -10
- 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 +102 -10
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +15 -12
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +58 -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 +38 -1
- 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 +191 -33
- package/scripts/e2e/demo-runtime.mjs +153 -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 +26 -12
- package/src/assessments/assessmentDiagnostics.ts +172 -0
- package/src/assessments/assessmentStore.ts +288 -51
- package/src/assessments/securitySnapshot.ts +231 -0
- package/src/config.ts +70 -13
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +22 -4
- package/src/control/middleware/static.ts +16 -3
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +14 -6
- package/src/control/routes/evidence.ts +1 -1
- package/src/control/routes/lineage.ts +1 -1
- package/src/control/routes/process.ts +341 -40
- package/src/control/routes/sessions.ts +88 -25
- package/src/control/routes/status.ts +134 -17
- package/src/control/routes/taskActions.ts +24 -58
- package/src/control/routes/tasks.ts +66 -58
- package/src/control/routes/workspace.ts +56 -25
- package/src/control/runtime.ts +71 -43
- package/src/control/server.ts +37 -342
- package/src/control/shared.ts +77 -11
- package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +72 -44
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +213 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +76 -64
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +5 -13
- package/src/goal/goalStatus.ts +19 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +12 -12
- package/src/goal/subgoalSync.ts +11 -8
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +113 -30
- package/src/logging.ts +10 -3
- 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 +291 -57
- package/src/runner/postTaskCleanup.ts +41 -17
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +197 -97
- package/src/runner/simpleProcess.ts +66 -31
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
- package/src/runner/taskStatusStore.ts +69 -0
- package/src/runner/watch.ts +348 -52
- package/src/security/contentRedaction.ts +2 -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 +27 -26
- 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} +7 -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} +136 -39
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
- 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} +27 -17
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
- 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 +36 -31
- package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
- package/src/tools/dispatch/directDispatch.ts +11 -10
- package/src/tools/dispatch/goalDispatch.ts +5 -4
- package/src/tools/dispatch/releaseDispatch.ts +3 -2
- 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} +41 -26
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +17 -1261
- 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 +110 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
- package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
- 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/tasks/taskStates.ts +54 -0
- package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
- 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/workspace/listAgents.ts +75 -0
- 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 +74 -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 +12 -0
- package/ui/pages/dashboard.html +94 -147
- package/ui/pages/direct-sessions.html +123 -26
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +134 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +12 -0
- package/ui/pages/tasks.html +54 -8
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +254 -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/apply-patch.test.d.ts +0 -1
- package/dist/test/unit/apply-patch.test.js +0 -225
- 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/create-task.test.d.ts +0 -1
- package/dist/test/unit/create-task.test.js +0 -197
- 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 -328
- 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 -224
- package/dist/test/unit/get-task-status.test.d.ts +0 -1
- package/dist/test/unit/get-task-status.test.js +0 -174
- package/dist/test/unit/get-task-summary.test.d.ts +0 -1
- package/dist/test/unit/get-task-summary.test.js +0 -146
- 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-report.test.d.ts +0 -1
- package/dist/test/unit/goal-report.test.js +0 -159
- 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 -192
- 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 -133
- 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/spec-kit-import.test.d.ts +0 -1
- package/dist/test/unit/spec-kit-import.test.js +0 -341
- 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/wait-for-task.test.d.ts +0 -1
- package/dist/test/unit/wait-for-task.test.js +0 -144
- 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/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/listAgents.d.ts +0 -12
- package/dist/tools/listAgents.js +0 -42
- package/dist/tools/reconcileTasks.js +0 -352
- 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/execution-plan-2026-07-09.md +0 -315
- 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/docs/roadmap-execution-and-acceptance.md +0 -365
- 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/apply-patch.test.ts +0 -293
- 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/create-task.test.ts +0 -255
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -467
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -251
- package/src/test/unit/get-task-status.test.ts +0 -203
- package/src/test/unit/get-task-summary.test.ts +0 -173
- 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-report.test.ts +0 -189
- 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 -245
- 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 -180
- 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/spec-kit-import.test.ts +0 -429
- 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/wait-for-task.test.ts +0 -176
- 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/listAgents.ts +0 -54
- 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/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +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/{evidencePack.d.ts → tasks/evidencePack.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/{retryTask.d.ts → tasks/retryTask.d.ts} +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/{waitForTask.d.ts → tasks/waitForTask.d.ts} +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
|
@@ -6,15 +6,19 @@ import {
|
|
|
6
6
|
readFileSync,
|
|
7
7
|
readdirSync,
|
|
8
8
|
statSync,
|
|
9
|
-
writeFileSync,
|
|
10
9
|
} from "node:fs";
|
|
11
10
|
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
12
11
|
import { execFile } from "node:child_process";
|
|
12
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
13
13
|
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
14
|
+
import { nullDevice } from "../utils/platform.js";
|
|
15
|
+
import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
|
|
16
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
14
17
|
|
|
15
18
|
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
16
19
|
const MAX_SNAPSHOT_FILES = 5000;
|
|
17
20
|
const MAX_DIFF_BYTES = 20 * 1024 * 1024;
|
|
21
|
+
const DIFF_TRUNCATION_MARKER = "\n[PATCHWARDEN DIFF TRUNCATED]\n";
|
|
18
22
|
const SKIP_DIRECTORIES = new Set([".git", ".patchwarden", "node_modules"]);
|
|
19
23
|
|
|
20
24
|
export interface FileFingerprint {
|
|
@@ -75,6 +79,8 @@ export interface ChangeArtifacts {
|
|
|
75
79
|
diff: string;
|
|
76
80
|
diff_available: boolean;
|
|
77
81
|
diff_truncated: boolean;
|
|
82
|
+
diff_redacted?: boolean;
|
|
83
|
+
diff_redaction_categories?: string[];
|
|
78
84
|
diff_size_bytes: number;
|
|
79
85
|
additions: number;
|
|
80
86
|
deletions: number;
|
|
@@ -91,25 +97,26 @@ export interface ChangeArtifacts {
|
|
|
91
97
|
artifact_hygiene: ArtifactHygiene;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapshot> {
|
|
100
|
+
export async function captureRepoSnapshot(repoPath: string, signal?: AbortSignal): Promise<RepoSnapshot> {
|
|
101
|
+
throwIfAborted(signal);
|
|
95
102
|
const warnings: string[] = [];
|
|
96
|
-
const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
103
|
+
const isGitResult = await runGit(repoPath, ["rev-parse", "--is-inside-work-tree"], signal);
|
|
97
104
|
const isGit = isGitResult.stdout.trim() === "true";
|
|
98
105
|
let head: string | null = null;
|
|
99
106
|
let status = "";
|
|
100
107
|
let paths: string[] = [];
|
|
101
|
-
const trackedPaths = new Set<string
|
|
102
|
-
const ignoredPaths = new Set<string
|
|
108
|
+
const trackedPaths = new Map<string, Set<string>>();
|
|
109
|
+
const ignoredPaths = new Map<string, Set<string>>();
|
|
103
110
|
|
|
104
111
|
const dirtyPaths = new Set<string>();
|
|
105
112
|
if (isGit) {
|
|
106
113
|
// The following five git calls are independent of each other and can run in parallel.
|
|
107
114
|
const [headResult, statusResult, trackedResult, ignoredResult, listedResult] = await Promise.all([
|
|
108
|
-
runGit(repoPath, ["rev-parse", "HEAD"]),
|
|
109
|
-
runGit(repoPath, ["status", "--porcelain=v1", "-uall"]),
|
|
110
|
-
runGit(repoPath, ["ls-files", "-z"]),
|
|
111
|
-
runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]),
|
|
112
|
-
runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]),
|
|
115
|
+
runGit(repoPath, ["rev-parse", "HEAD"], signal),
|
|
116
|
+
runGit(repoPath, ["status", "--porcelain=v1", "-uall"], signal),
|
|
117
|
+
runGit(repoPath, ["ls-files", "-z"], signal),
|
|
118
|
+
runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"], signal),
|
|
119
|
+
runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"], signal),
|
|
113
120
|
]);
|
|
114
121
|
|
|
115
122
|
if (headResult.status === 0) head = headResult.stdout.trim() || null;
|
|
@@ -136,25 +143,25 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
|
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
if (trackedResult.status === 0) {
|
|
139
|
-
for (const path of trackedResult.stdout.split("\0").filter(Boolean)) trackedPaths
|
|
146
|
+
for (const path of trackedResult.stdout.split("\0").filter(Boolean)) addSnapshotPath(trackedPaths, path);
|
|
140
147
|
}
|
|
141
148
|
if (ignoredResult.status === 0) {
|
|
142
|
-
for (const path of ignoredResult.stdout.split("\0").filter(Boolean)) ignoredPaths
|
|
149
|
+
for (const path of ignoredResult.stdout.split("\0").filter(Boolean)) addSnapshotPath(ignoredPaths, path);
|
|
143
150
|
} else {
|
|
144
151
|
warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
|
|
145
152
|
}
|
|
146
153
|
if (listedResult.status === 0) {
|
|
147
154
|
paths = [...new Set([
|
|
148
155
|
...listedResult.stdout.split("\0").filter(Boolean),
|
|
149
|
-
...walkWorkspace(repoPath),
|
|
156
|
+
...walkWorkspace(repoPath, signal),
|
|
150
157
|
])];
|
|
151
158
|
} else {
|
|
152
159
|
warnings.push("git ls-files failed; using bounded filesystem scan");
|
|
153
|
-
paths = walkWorkspace(repoPath);
|
|
160
|
+
paths = walkWorkspace(repoPath, signal);
|
|
154
161
|
}
|
|
155
162
|
} else {
|
|
156
163
|
warnings.push("repository is not a Git worktree; diff will contain file-change evidence only");
|
|
157
|
-
paths = walkWorkspace(repoPath);
|
|
164
|
+
paths = walkWorkspace(repoPath, signal);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
if (paths.length > MAX_SNAPSHOT_FILES) {
|
|
@@ -164,6 +171,7 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
|
|
|
164
171
|
|
|
165
172
|
const files: Record<string, FileFingerprint> = {};
|
|
166
173
|
for (const inputPath of paths.sort()) {
|
|
174
|
+
throwIfAborted(signal);
|
|
167
175
|
const normalized = normalizePath(inputPath);
|
|
168
176
|
if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized)) continue;
|
|
169
177
|
const absolutePath = resolve(repoPath, inputPath);
|
|
@@ -176,8 +184,8 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
|
|
|
176
184
|
files[normalized] = {
|
|
177
185
|
size: stat.size,
|
|
178
186
|
sha256,
|
|
179
|
-
tracked: trackedPaths
|
|
180
|
-
ignored: !trackedPaths
|
|
187
|
+
tracked: hasSnapshotPath(trackedPaths, normalized),
|
|
188
|
+
ignored: !hasSnapshotPath(trackedPaths, normalized) && hasSnapshotPath(ignoredPaths, normalized),
|
|
181
189
|
};
|
|
182
190
|
} catch {
|
|
183
191
|
warnings.push(`could not fingerprint: ${normalized}`);
|
|
@@ -197,55 +205,96 @@ export async function captureRepoSnapshot(repoPath: string): Promise<RepoSnapsho
|
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
export function writeSnapshot(taskDir: string, filename: string, snapshot: RepoSnapshot): void {
|
|
200
|
-
|
|
208
|
+
atomicWriteJsonFileSync(join(taskDir, filename), snapshot);
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
export async function buildChangeArtifacts(
|
|
204
212
|
repoPath: string,
|
|
205
213
|
before: RepoSnapshot,
|
|
206
|
-
after: RepoSnapshot
|
|
214
|
+
after: RepoSnapshot,
|
|
215
|
+
signal?: AbortSignal,
|
|
207
216
|
): Promise<ChangeArtifacts> {
|
|
217
|
+
throwIfAborted(signal);
|
|
208
218
|
const changedFiles = compareSnapshots(before, after);
|
|
209
219
|
const artifactHygiene = classifyArtifactHygiene(changedFiles);
|
|
210
220
|
const sections: string[] = [];
|
|
211
221
|
const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
|
|
222
|
+
const evidence = [
|
|
223
|
+
"# PatchWarden change evidence",
|
|
224
|
+
`# changed_files: ${changedFiles.length}`,
|
|
225
|
+
`# workspace_dirty_before: ${before.workspace_dirty}`,
|
|
226
|
+
`# workspace_dirty_after: ${after.workspace_dirty}`,
|
|
227
|
+
...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
|
|
228
|
+
].join("\n");
|
|
229
|
+
let retainedSectionBytes = 0;
|
|
230
|
+
let diffTruncated = Buffer.byteLength(evidence, "utf-8") > MAX_DIFF_BYTES;
|
|
231
|
+
const sectionBudget = Math.max(
|
|
232
|
+
0,
|
|
233
|
+
MAX_DIFF_BYTES
|
|
234
|
+
- Math.min(Buffer.byteLength(evidence, "utf-8"), MAX_DIFF_BYTES)
|
|
235
|
+
- Buffer.byteLength(DIFF_TRUNCATION_MARKER, "utf-8")
|
|
236
|
+
- 2,
|
|
237
|
+
);
|
|
238
|
+
const appendSection = (label: string, content: string, commandTruncated = false): void => {
|
|
239
|
+
if (!content.trim()) {
|
|
240
|
+
if (commandTruncated) diffTruncated = true;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const block = `${sections.length > 0 ? "\n\n" : ""}${label}\n\n${content.trimEnd()}`;
|
|
244
|
+
const remaining = Math.max(0, sectionBudget - retainedSectionBytes);
|
|
245
|
+
const bounded = utf8Prefix(block, remaining);
|
|
246
|
+
if (bounded) {
|
|
247
|
+
sections.push(bounded);
|
|
248
|
+
retainedSectionBytes += Buffer.byteLength(bounded, "utf-8");
|
|
249
|
+
}
|
|
250
|
+
if (commandTruncated || Buffer.byteLength(bounded, "utf-8") < Buffer.byteLength(block, "utf-8")) {
|
|
251
|
+
diffTruncated = true;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
212
254
|
|
|
213
255
|
if (before.is_git && after.is_git && scopedPaths.length > 0) {
|
|
214
256
|
if (before.head && after.head && before.head !== after.head) {
|
|
215
|
-
const committed = await runGit(repoPath, ["diff", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths]);
|
|
216
|
-
|
|
257
|
+
const committed = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", before.head, after.head, "--", ...scopedPaths], signal);
|
|
258
|
+
appendSection("# Changes committed during task", committed.stdout, committed.truncated);
|
|
217
259
|
}
|
|
218
260
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
if (retainedSectionBytes < sectionBudget) {
|
|
262
|
+
const base = after.head || "HEAD";
|
|
263
|
+
const working = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-color", "--binary", base, "--", ...scopedPaths], signal);
|
|
264
|
+
appendSection("# Staged and unstaged changes", working.stdout, working.truncated);
|
|
265
|
+
} else {
|
|
266
|
+
diffTruncated = true;
|
|
267
|
+
}
|
|
222
268
|
|
|
223
269
|
for (const file of changedFiles.filter((item) => item.change === "added").slice(0, 100)) {
|
|
224
|
-
|
|
270
|
+
throwIfAborted(signal);
|
|
271
|
+
if (retainedSectionBytes >= sectionBudget) {
|
|
272
|
+
diffTruncated = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
const tracked = await runGit(repoPath, ["ls-files", "--error-unmatch", "--", file.path], signal);
|
|
225
276
|
if (tracked.status === 0) continue;
|
|
226
|
-
const untracked = await runGit(repoPath, ["diff", "--no-index", "--no-color", "--binary", "--",
|
|
227
|
-
|
|
277
|
+
const untracked = await runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--no-index", "--no-color", "--binary", "--", nullDevice, file.path], signal);
|
|
278
|
+
appendSection("# Untracked file", untracked.stdout, untracked.truncated);
|
|
228
279
|
}
|
|
229
280
|
}
|
|
230
281
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
...changedFiles.map((file) => `# ${file.change}: ${file.path}`),
|
|
237
|
-
].join("\n");
|
|
238
|
-
const body = sections.join("\n\n");
|
|
239
|
-
const fullDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
|
|
282
|
+
const body = sections.join("");
|
|
283
|
+
const rawDiff = `${evidence}\n\n${body || (changedFiles.length ? "(textual patch unavailable; see changed-files.json for hash evidence)" : "(no task file changes detected)")}\n`;
|
|
284
|
+
const sanitized = sanitizeDiffEvidence(rawDiff, MAX_DIFF_BYTES);
|
|
285
|
+
const fullDiff = sanitized.content;
|
|
286
|
+
diffTruncated ||= sanitized.truncated;
|
|
240
287
|
const additions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
241
288
|
const deletions = fullDiff.split(/\r?\n/).filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
242
|
-
const fileStats = await buildFileStats(repoPath, before, after, changedFiles);
|
|
289
|
+
const fileStats = await buildFileStats(repoPath, before, after, changedFiles, signal);
|
|
243
290
|
|
|
244
291
|
return {
|
|
245
292
|
changed_files: changedFiles,
|
|
246
293
|
diff: fullDiff,
|
|
247
294
|
diff_available: changedFiles.length > 0,
|
|
248
|
-
diff_truncated:
|
|
295
|
+
diff_truncated: diffTruncated,
|
|
296
|
+
diff_redacted: sanitized.redacted,
|
|
297
|
+
diff_redaction_categories: sanitized.redaction_categories,
|
|
249
298
|
diff_size_bytes: Buffer.byteLength(fullDiff, "utf-8"),
|
|
250
299
|
additions,
|
|
251
300
|
deletions,
|
|
@@ -262,13 +311,47 @@ export async function buildChangeArtifacts(
|
|
|
262
311
|
};
|
|
263
312
|
}
|
|
264
313
|
|
|
314
|
+
export function sanitizeDiffEvidence(
|
|
315
|
+
input: string,
|
|
316
|
+
maxBytes = MAX_DIFF_BYTES,
|
|
317
|
+
): {
|
|
318
|
+
content: string;
|
|
319
|
+
truncated: boolean;
|
|
320
|
+
redacted: boolean;
|
|
321
|
+
redaction_categories: string[];
|
|
322
|
+
} {
|
|
323
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
324
|
+
throw new Error("maxBytes must be a positive integer");
|
|
325
|
+
}
|
|
326
|
+
const redaction = redactSensitiveContent(input);
|
|
327
|
+
const encodedBytes = Buffer.byteLength(redaction.content, "utf-8");
|
|
328
|
+
if (encodedBytes <= maxBytes) {
|
|
329
|
+
return {
|
|
330
|
+
content: redaction.content,
|
|
331
|
+
truncated: false,
|
|
332
|
+
redacted: redaction.redacted,
|
|
333
|
+
redaction_categories: redaction.redaction_categories,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const marker = utf8Prefix(DIFF_TRUNCATION_MARKER, maxBytes);
|
|
337
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
338
|
+
return {
|
|
339
|
+
content: utf8Prefix(redaction.content, Math.max(0, maxBytes - markerBytes)) + marker,
|
|
340
|
+
truncated: true,
|
|
341
|
+
redacted: redaction.redacted,
|
|
342
|
+
redaction_categories: redaction.redaction_categories,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
265
346
|
async function buildFileStats(
|
|
266
347
|
repoPath: string,
|
|
267
348
|
before: RepoSnapshot,
|
|
268
349
|
after: RepoSnapshot,
|
|
269
|
-
changedFiles: ChangedFile[]
|
|
350
|
+
changedFiles: ChangedFile[],
|
|
351
|
+
signal?: AbortSignal,
|
|
270
352
|
): Promise<ChangeArtifacts["file_stats"]> {
|
|
271
353
|
const results = await Promise.all(changedFiles.map(async (file) => {
|
|
354
|
+
throwIfAborted(signal);
|
|
272
355
|
let additions = 0;
|
|
273
356
|
let deletions = 0;
|
|
274
357
|
const paths = file.old_path ? [file.old_path, file.path] : [file.path];
|
|
@@ -280,7 +363,7 @@ async function buildFileStats(
|
|
|
280
363
|
}
|
|
281
364
|
ranges.push([after.head || "HEAD"]);
|
|
282
365
|
const rangeResults = await Promise.all(
|
|
283
|
-
ranges.map((range) => runGit(repoPath, ["diff", "--numstat", ...range, "--", ...paths]))
|
|
366
|
+
ranges.map((range) => runGit(repoPath, ["diff", "--no-ext-diff", "--no-textconv", "--numstat", ...range, "--", ...paths], signal))
|
|
284
367
|
);
|
|
285
368
|
for (const result of rangeResults) {
|
|
286
369
|
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
@@ -308,17 +391,31 @@ function countLines(content: string): number {
|
|
|
308
391
|
return content.split(/\r?\n/).length - (content.endsWith("\n") ? 1 : 0);
|
|
309
392
|
}
|
|
310
393
|
|
|
311
|
-
export function compareSnapshots(
|
|
312
|
-
|
|
394
|
+
export function compareSnapshots(
|
|
395
|
+
before: RepoSnapshot,
|
|
396
|
+
after: RepoSnapshot,
|
|
397
|
+
platform: NodeJS.Platform = process.platform,
|
|
398
|
+
): ChangedFile[] {
|
|
313
399
|
const changed: ChangedFile[] = [];
|
|
314
|
-
for (const path of
|
|
315
|
-
const left = before.files[path];
|
|
316
|
-
const right = after.files[path];
|
|
400
|
+
for (const { path, leftPath, rightPath, left, right } of pairSnapshotFiles(before.files, after.files, platform)) {
|
|
317
401
|
if (!left && right) {
|
|
318
402
|
changed.push(classifyChangedFile(path, "added", null, right));
|
|
319
403
|
} else if (left && !right) {
|
|
320
404
|
changed.push(classifyChangedFile(path, "deleted", left, null));
|
|
321
|
-
} else if (left.sha256
|
|
405
|
+
} else if (left && right && left.sha256 === right.sha256 && leftPath !== rightPath) {
|
|
406
|
+
// Windows lookup is case-insensitive, but a case-only Git rename is still
|
|
407
|
+
// auditable work and must not disappear from the evidence set.
|
|
408
|
+
changed.push({
|
|
409
|
+
path: rightPath!,
|
|
410
|
+
old_path: leftPath!,
|
|
411
|
+
change: "renamed",
|
|
412
|
+
before_sha256: left.sha256,
|
|
413
|
+
after_sha256: right.sha256,
|
|
414
|
+
tracked: left.tracked || right.tracked,
|
|
415
|
+
ignored: right.ignored,
|
|
416
|
+
kind: classifyPathKind(rightPath!),
|
|
417
|
+
});
|
|
418
|
+
} else if (left && right && left.sha256 !== right.sha256) {
|
|
322
419
|
changed.push(classifyChangedFile(path, "modified", left, right));
|
|
323
420
|
}
|
|
324
421
|
}
|
|
@@ -386,10 +483,12 @@ export interface ExternalDirtyFile {
|
|
|
386
483
|
export function extractExternalDirtyFiles(
|
|
387
484
|
workspaceSnapshot: RepoSnapshot,
|
|
388
485
|
repoPath: string,
|
|
389
|
-
workspaceRoot: string
|
|
486
|
+
workspaceRoot: string,
|
|
487
|
+
platform: NodeJS.Platform = process.platform,
|
|
390
488
|
): ExternalDirtyFile[] {
|
|
391
489
|
const dirtyFiles: ExternalDirtyFile[] = [];
|
|
392
|
-
const dirtyPathSet = new Set(
|
|
490
|
+
const dirtyPathSet = new Map<string, Set<string>>();
|
|
491
|
+
for (const path of workspaceSnapshot.dirty_paths) addSnapshotPath(dirtyPathSet, path, platform);
|
|
393
492
|
for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
|
|
394
493
|
const absolutePath = resolve(workspaceRoot, path);
|
|
395
494
|
const rel = relative(repoPath, absolutePath);
|
|
@@ -399,7 +498,7 @@ export function extractExternalDirtyFiles(
|
|
|
399
498
|
// 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
|
|
400
499
|
// 2. It's not tracked by git (untracked file), OR
|
|
401
500
|
// 3. It's explicitly ignored
|
|
402
|
-
const isDirty = dirtyPathSet
|
|
501
|
+
const isDirty = hasSnapshotPath(dirtyPathSet, path, platform);
|
|
403
502
|
const isUntracked = !fingerprint.tracked;
|
|
404
503
|
const isIgnored = fingerprint.ignored;
|
|
405
504
|
if (isDirty || isUntracked || isIgnored) {
|
|
@@ -422,11 +521,19 @@ export function extractExternalDirtyFiles(
|
|
|
422
521
|
*/
|
|
423
522
|
export function findNewExternalDirtyFiles(
|
|
424
523
|
baseline: ExternalDirtyFile[],
|
|
425
|
-
current: ExternalDirtyFile[]
|
|
524
|
+
current: ExternalDirtyFile[],
|
|
525
|
+
platform: NodeJS.Platform = process.platform,
|
|
426
526
|
): ExternalDirtyFile[] {
|
|
427
|
-
const baselineMap = new Map
|
|
527
|
+
const baselineMap = new Map<string, ExternalDirtyFile[]>();
|
|
528
|
+
for (const file of baseline) {
|
|
529
|
+
const key = comparableSnapshotPath(file.path, platform);
|
|
530
|
+
baselineMap.set(key, [...(baselineMap.get(key) ?? []), file]);
|
|
531
|
+
}
|
|
428
532
|
return current.filter((f) => {
|
|
429
|
-
const
|
|
533
|
+
const candidates = baselineMap.get(comparableSnapshotPath(f.path, platform)) ?? [];
|
|
534
|
+
const baselineFile = candidates.length <= 1
|
|
535
|
+
? candidates[0]
|
|
536
|
+
: candidates.find((candidate) => normalizePath(candidate.path) === normalizePath(f.path));
|
|
430
537
|
if (!baselineFile) return true; // New path — definitely new
|
|
431
538
|
// Same path but content changed during task execution
|
|
432
539
|
if (baselineFile.before_sha256 !== f.before_sha256) return true;
|
|
@@ -623,9 +730,98 @@ function normalizePath(value: string): string {
|
|
|
623
730
|
return value.replace(/\\/g, "/");
|
|
624
731
|
}
|
|
625
732
|
|
|
626
|
-
function
|
|
733
|
+
function comparableSnapshotPath(value: string, platform: NodeJS.Platform = process.platform): string {
|
|
734
|
+
const normalized = normalizePath(value);
|
|
735
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function addSnapshotPath(
|
|
739
|
+
index: Map<string, Set<string>>,
|
|
740
|
+
value: string,
|
|
741
|
+
platform: NodeJS.Platform = process.platform,
|
|
742
|
+
): void {
|
|
743
|
+
const normalized = normalizePath(value);
|
|
744
|
+
const key = comparableSnapshotPath(normalized, platform);
|
|
745
|
+
const exactPaths = index.get(key) ?? new Set<string>();
|
|
746
|
+
exactPaths.add(normalized);
|
|
747
|
+
index.set(key, exactPaths);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function hasSnapshotPath(
|
|
751
|
+
index: Map<string, Set<string>>,
|
|
752
|
+
value: string,
|
|
753
|
+
platform: NodeJS.Platform = process.platform,
|
|
754
|
+
): boolean {
|
|
755
|
+
const normalized = normalizePath(value);
|
|
756
|
+
const exactPaths = index.get(comparableSnapshotPath(normalized, platform));
|
|
757
|
+
if (!exactPaths) return false;
|
|
758
|
+
return platform !== "win32" || exactPaths.size <= 1 || exactPaths.has(normalized);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
interface SnapshotFileEntry {
|
|
762
|
+
path: string;
|
|
763
|
+
fingerprint: FileFingerprint;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function pairSnapshotFiles(
|
|
767
|
+
before: Record<string, FileFingerprint>,
|
|
768
|
+
after: Record<string, FileFingerprint>,
|
|
769
|
+
platform: NodeJS.Platform,
|
|
770
|
+
): Array<{ path: string; leftPath?: string; rightPath?: string; left?: FileFingerprint; right?: FileFingerprint }> {
|
|
771
|
+
const beforeGroups = groupSnapshotFiles(before, platform);
|
|
772
|
+
const afterGroups = groupSnapshotFiles(after, platform);
|
|
773
|
+
const pairs: Array<{ path: string; leftPath?: string; rightPath?: string; left?: FileFingerprint; right?: FileFingerprint }> = [];
|
|
774
|
+
const comparablePaths = [...new Set([...beforeGroups.keys(), ...afterGroups.keys()])].sort();
|
|
775
|
+
|
|
776
|
+
for (const comparablePath of comparablePaths) {
|
|
777
|
+
const beforeEntries = beforeGroups.get(comparablePath) ?? [];
|
|
778
|
+
const afterEntries = afterGroups.get(comparablePath) ?? [];
|
|
779
|
+
if (platform !== "win32" || (beforeEntries.length <= 1 && afterEntries.length <= 1)) {
|
|
780
|
+
pairs.push({
|
|
781
|
+
path: afterEntries[0]?.path ?? beforeEntries[0].path,
|
|
782
|
+
leftPath: beforeEntries[0]?.path,
|
|
783
|
+
rightPath: afterEntries[0]?.path,
|
|
784
|
+
left: beforeEntries[0]?.fingerprint,
|
|
785
|
+
right: afterEntries[0]?.fingerprint,
|
|
786
|
+
});
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// NTFS can opt individual directories into case-sensitive mode. If either
|
|
791
|
+
// snapshot contains a case collision, preserve every exact path instead of
|
|
792
|
+
// silently dropping one entry from the evidence.
|
|
793
|
+
const beforeExact = new Map(beforeEntries.map((entry) => [normalizePath(entry.path), entry]));
|
|
794
|
+
const afterExact = new Map(afterEntries.map((entry) => [normalizePath(entry.path), entry]));
|
|
795
|
+
const exactPaths = [...new Set([...beforeExact.keys(), ...afterExact.keys()])].sort();
|
|
796
|
+
for (const exactPath of exactPaths) {
|
|
797
|
+
pairs.push({
|
|
798
|
+
path: afterExact.get(exactPath)?.path ?? beforeExact.get(exactPath)!.path,
|
|
799
|
+
leftPath: beforeExact.get(exactPath)?.path,
|
|
800
|
+
rightPath: afterExact.get(exactPath)?.path,
|
|
801
|
+
left: beforeExact.get(exactPath)?.fingerprint,
|
|
802
|
+
right: afterExact.get(exactPath)?.fingerprint,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return pairs;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function groupSnapshotFiles(
|
|
810
|
+
files: Record<string, FileFingerprint>,
|
|
811
|
+
platform: NodeJS.Platform,
|
|
812
|
+
): Map<string, SnapshotFileEntry[]> {
|
|
813
|
+
const grouped = new Map<string, SnapshotFileEntry[]>();
|
|
814
|
+
for (const [path, fingerprint] of Object.entries(files)) {
|
|
815
|
+
const key = comparableSnapshotPath(path, platform);
|
|
816
|
+
grouped.set(key, [...(grouped.get(key) ?? []), { path, fingerprint }]);
|
|
817
|
+
}
|
|
818
|
+
return grouped;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function walkWorkspace(root: string, signal?: AbortSignal): string[] {
|
|
627
822
|
const result: string[] = [];
|
|
628
823
|
const visit = (directory: string) => {
|
|
824
|
+
throwIfAborted(signal);
|
|
629
825
|
if (result.length >= MAX_SNAPSHOT_FILES) return;
|
|
630
826
|
let entries;
|
|
631
827
|
try {
|
|
@@ -634,6 +830,7 @@ function walkWorkspace(root: string): string[] {
|
|
|
634
830
|
return;
|
|
635
831
|
}
|
|
636
832
|
for (const entry of entries) {
|
|
833
|
+
throwIfAborted(signal);
|
|
637
834
|
if (result.length >= MAX_SNAPSHOT_FILES) break;
|
|
638
835
|
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
639
836
|
const absolute = join(directory, entry.name);
|
|
@@ -645,14 +842,29 @@ function walkWorkspace(root: string): string[] {
|
|
|
645
842
|
return result;
|
|
646
843
|
}
|
|
647
844
|
|
|
648
|
-
function runGit(repoPath: string, args: string[]): Promise<{
|
|
649
|
-
|
|
650
|
-
|
|
845
|
+
function runGit(repoPath: string, args: string[], signal?: AbortSignal): Promise<{
|
|
846
|
+
status: number | null;
|
|
847
|
+
stdout: string;
|
|
848
|
+
stderr: string;
|
|
849
|
+
truncated: boolean;
|
|
850
|
+
}> {
|
|
851
|
+
throwIfAborted(signal);
|
|
852
|
+
return new Promise((resolve, reject) => {
|
|
853
|
+
const env = buildGitEnvironment(repoPath);
|
|
854
|
+
const git = resolveTrustedExecutable("git", repoPath, { pathValue: env.PATH });
|
|
855
|
+
execFile(git, args, {
|
|
651
856
|
cwd: repoPath,
|
|
652
857
|
encoding: "utf-8",
|
|
653
858
|
timeout: 30_000,
|
|
654
859
|
maxBuffer: MAX_DIFF_BYTES,
|
|
860
|
+
windowsHide: true,
|
|
861
|
+
env,
|
|
862
|
+
signal,
|
|
655
863
|
}, (err, stdout, stderr) => {
|
|
864
|
+
if (signal?.aborted) {
|
|
865
|
+
reject(abortReason(signal));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
656
868
|
let status: number | null;
|
|
657
869
|
if (err) {
|
|
658
870
|
// err.code is the exit code (number) when the process exited with a non-zero status;
|
|
@@ -661,11 +873,33 @@ function runGit(repoPath: string, args: string[]): Promise<{ status: number | nu
|
|
|
661
873
|
} else {
|
|
662
874
|
status = 0;
|
|
663
875
|
}
|
|
664
|
-
|
|
876
|
+
const truncated = Boolean(
|
|
877
|
+
err
|
|
878
|
+
&& typeof err.code === "string"
|
|
879
|
+
&& err.code.includes("MAXBUFFER"),
|
|
880
|
+
);
|
|
881
|
+
resolve({ status, stdout: stdout || "", stderr: stderr || "", truncated });
|
|
665
882
|
});
|
|
666
883
|
});
|
|
667
884
|
}
|
|
668
885
|
|
|
886
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
887
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function abortReason(signal: AbortSignal): Error {
|
|
891
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Operation aborted");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function utf8Prefix(value: string, maxBytes: number): string {
|
|
895
|
+
if (maxBytes <= 0) return "";
|
|
896
|
+
const encoded = Buffer.from(value, "utf-8");
|
|
897
|
+
if (encoded.length <= maxBytes) return value;
|
|
898
|
+
let prefix = encoded.subarray(0, maxBytes).toString("utf-8").replace(/\uFFFD$/u, "");
|
|
899
|
+
while (Buffer.byteLength(prefix, "utf-8") > maxBytes) prefix = prefix.slice(0, -1);
|
|
900
|
+
return prefix;
|
|
901
|
+
}
|
|
902
|
+
|
|
669
903
|
function hashFileAsync(filePath: string): Promise<string> {
|
|
670
904
|
return new Promise((resolve, reject) => {
|
|
671
905
|
const hash = createHash("sha256");
|
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
|
3
3
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { platform } from "node:os";
|
|
5
|
+
import { buildGitEnvironment, resolveTrustedExecutable } from "./processSecurity.js";
|
|
6
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
5
7
|
|
|
6
8
|
const isWindows = platform() === "win32";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Remove a file or directory, working around Node.js fs.rmSync issues on Windows
|
|
10
12
|
* where recursive directory deletion silently fails (Node v22+).
|
|
11
|
-
*
|
|
13
|
+
* On Windows, clears read-only attributes before one bounded native retry.
|
|
12
14
|
*/
|
|
13
15
|
function safeRemoveSync(targetPath: string): void {
|
|
14
16
|
rmSync(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
15
17
|
if (isWindows && existsSync(targetPath)) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
makeWritableSync(targetPath);
|
|
19
|
+
rmSync(targetPath, { recursive: true, force: true, maxRetries: 5, retryDelay: 150 });
|
|
20
|
+
}
|
|
21
|
+
if (existsSync(targetPath)) {
|
|
22
|
+
throw new Error(`Cleanup path still exists after bounded retries: ${targetPath}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeWritableSync(targetPath: string): void {
|
|
27
|
+
let stat: ReturnType<typeof lstatSync>;
|
|
28
|
+
try {
|
|
29
|
+
stat = lstatSync(targetPath);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (stat.isSymbolicLink()) return;
|
|
34
|
+
try { chmodSync(targetPath, stat.isDirectory() ? 0o700 : 0o600); } catch { /* best effort */ }
|
|
35
|
+
if (!stat.isDirectory()) return;
|
|
36
|
+
let entries: string[];
|
|
37
|
+
try {
|
|
38
|
+
entries = readdirSync(targetPath);
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
26
41
|
}
|
|
42
|
+
for (const entry of entries) makeWritableSync(join(targetPath, entry));
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
const EXCLUDED_DIRS = new Set([".git", ".patchwarden", ".venv", "node_modules", "samples", "docs"]);
|
|
@@ -76,7 +92,7 @@ export function runPostTaskCleanup(repoPath: string, taskDir: string): PostTaskC
|
|
|
76
92
|
});
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
|
-
|
|
95
|
+
atomicWriteJsonFileSync(join(taskDir, "post-task-cleanup.json"), report);
|
|
80
96
|
return report;
|
|
81
97
|
}
|
|
82
98
|
|
|
@@ -117,10 +133,12 @@ function walk(dir: string, visit: (path: string, isDir: boolean) => void): void
|
|
|
117
133
|
function hasTrackedGitContent(root: string, rel: string): boolean {
|
|
118
134
|
if (!isGitWorktree(root)) return false;
|
|
119
135
|
try {
|
|
120
|
-
const
|
|
136
|
+
const env = buildGitEnvironment(root);
|
|
137
|
+
const output = execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["ls-files", "--", rel], {
|
|
121
138
|
cwd: root,
|
|
122
139
|
encoding: "utf-8",
|
|
123
140
|
windowsHide: true,
|
|
141
|
+
env,
|
|
124
142
|
});
|
|
125
143
|
return output.trim().length > 0;
|
|
126
144
|
} catch {
|
|
@@ -130,19 +148,23 @@ function hasTrackedGitContent(root: string, rel: string): boolean {
|
|
|
130
148
|
|
|
131
149
|
function isIgnoredOrUntracked(root: string, rel: string): boolean {
|
|
132
150
|
if (!isGitWorktree(root)) return true;
|
|
151
|
+
const env = buildGitEnvironment(root);
|
|
152
|
+
const git = resolveTrustedExecutable("git", root, { pathValue: env.PATH });
|
|
133
153
|
try {
|
|
134
|
-
execFileSync(
|
|
154
|
+
execFileSync(git, ["check-ignore", "-q", "--", rel], {
|
|
135
155
|
cwd: root,
|
|
136
156
|
stdio: "ignore",
|
|
137
157
|
windowsHide: true,
|
|
158
|
+
env,
|
|
138
159
|
});
|
|
139
160
|
return true;
|
|
140
161
|
} catch {
|
|
141
162
|
try {
|
|
142
|
-
const output = execFileSync(
|
|
163
|
+
const output = execFileSync(git, ["ls-files", "--others", "--exclude-standard", "--", rel], {
|
|
143
164
|
cwd: root,
|
|
144
165
|
encoding: "utf-8",
|
|
145
166
|
windowsHide: true,
|
|
167
|
+
env,
|
|
146
168
|
});
|
|
147
169
|
return output.trim().length > 0 || !existsSync(resolve(root, rel));
|
|
148
170
|
} catch {
|
|
@@ -153,10 +175,12 @@ function isIgnoredOrUntracked(root: string, rel: string): boolean {
|
|
|
153
175
|
|
|
154
176
|
function isGitWorktree(root: string): boolean {
|
|
155
177
|
try {
|
|
156
|
-
|
|
178
|
+
const env = buildGitEnvironment(root);
|
|
179
|
+
return execFileSync(resolveTrustedExecutable("git", root, { pathValue: env.PATH }), ["rev-parse", "--is-inside-work-tree"], {
|
|
157
180
|
cwd: root,
|
|
158
181
|
encoding: "utf-8",
|
|
159
182
|
windowsHide: true,
|
|
183
|
+
env,
|
|
160
184
|
}).trim() === "true";
|
|
161
185
|
} catch {
|
|
162
186
|
return false;
|