patchwarden 1.5.1 → 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 +152 -25
- package/README.md +120 -23
- package/dist/assessments/agentAssessor.js +21 -12
- package/dist/assessments/assessmentStore.js +42 -34
- 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 +46 -1
- package/dist/control/routes/status.js +72 -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 +63 -30
- package/dist/control/server.js +36 -365
- package/dist/control/shared.d.ts +25 -1
- package/dist/control/shared.js +64 -11
- 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 +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.js +62 -52
- 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 +1 -1
- package/dist/runner/runTask.js +115 -87
- 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} +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 +256 -48
- 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 +24 -23
- 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} +1 -1
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
- 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 +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
- package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
- 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.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} +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/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} +58 -30
- 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 +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 +17 -11
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +39 -29
- 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 +91 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +10 -10
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +27 -3
- 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/{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/assessmentStore.ts +52 -40
- 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 +97 -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 +69 -33
- package/src/control/server.ts +36 -342
- package/src/control/shared.ts +77 -11
- 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 +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +73 -59
- 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 +138 -91
- package/src/runner/simpleProcess.ts +66 -31
- 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 +281 -51
- 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 +25 -24
- 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} +99 -32
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
- 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 +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- 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} +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} +69 -34
- 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 +12 -0
- package/ui/pages/dashboard.html +90 -147
- package/ui/pages/direct-sessions.html +123 -26
- 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 +12 -0
- package/ui/pages/tasks.html +54 -8
- 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/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/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/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/{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
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { allowedEnvironmentValues, buildChildEnvironment, prepareShellFreeCommand, redactProcessOutput, resolveTrustedExecutable, SecureProcessLogCapture, } from "./processSecurity.js";
|
|
3
3
|
const GRACEFUL_KILL_MS = 2000;
|
|
4
4
|
const DEFAULT_MAX_STDOUT = 524288;
|
|
5
5
|
const DEFAULT_MAX_STDERR = 131072;
|
|
6
6
|
export function runSimpleProcessSync(options) {
|
|
7
7
|
const maxStdout = options.maxStdoutBytes ?? DEFAULT_MAX_STDOUT;
|
|
8
8
|
const maxStderr = options.maxStderrBytes ?? DEFAULT_MAX_STDERR;
|
|
9
|
+
const exactRedactionValues = allowedEnvironmentValues(options.environmentVariableNames);
|
|
10
|
+
const logCapture = new SecureProcessLogCapture([options.stdoutPath, options.stderrPath], options.maxLogBytes);
|
|
9
11
|
let result;
|
|
10
12
|
try {
|
|
11
|
-
|
|
13
|
+
const env = buildChildEnvironment({
|
|
14
|
+
cwd: options.cwd,
|
|
15
|
+
allowedNames: options.environmentVariableNames,
|
|
16
|
+
blockedNames: options.blockedEnvironmentVariableNames,
|
|
17
|
+
});
|
|
18
|
+
const prepared = prepareShellFreeCommand(options.command, options.args, options.cwd, { pathValue: env.PATH });
|
|
19
|
+
const command = resolveTrustedExecutable(prepared.command, options.cwd, { pathValue: env.PATH });
|
|
20
|
+
result = spawnSync(command, prepared.args, {
|
|
12
21
|
cwd: options.cwd,
|
|
13
22
|
stdio: ["ignore", "pipe", "pipe"],
|
|
14
23
|
timeout: options.timeoutMs,
|
|
15
24
|
windowsHide: true,
|
|
16
25
|
maxBuffer: maxStdout + 1024,
|
|
17
26
|
encoding: "utf-8",
|
|
27
|
+
env,
|
|
18
28
|
});
|
|
19
29
|
}
|
|
20
30
|
catch (error) {
|
|
@@ -28,23 +38,16 @@ export function runSimpleProcessSync(options) {
|
|
|
28
38
|
stderrTruncated: false,
|
|
29
39
|
};
|
|
30
40
|
}
|
|
31
|
-
const
|
|
32
|
-
const
|
|
41
|
+
const rawStdout = result.stdout || "";
|
|
42
|
+
const rawStderr = result.stderr || "";
|
|
43
|
+
const stdout = redactProcessOutput(rawStdout, exactRedactionValues);
|
|
44
|
+
const stderr = redactProcessOutput(rawStderr, exactRedactionValues);
|
|
33
45
|
const timedOut = Boolean(result.signal === "SIGTERM" || result.signal === "SIGKILL" ||
|
|
34
46
|
(result.signal !== null && result.status === null) ||
|
|
35
|
-
(result.error && result.error
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
catch { } // best-effort log write; failure doesn't affect result
|
|
41
|
-
}
|
|
42
|
-
if (options.stderrPath && stderr) {
|
|
43
|
-
try {
|
|
44
|
-
writeFileSyncAppend(options.stderrPath, stderr);
|
|
45
|
-
}
|
|
46
|
-
catch { } // best-effort log write; failure doesn't affect result
|
|
47
|
-
}
|
|
47
|
+
(result.error && errorCode(result.error) === "ETIMEDOUT"));
|
|
48
|
+
logCapture.append(options.stdoutPath, rawStdout);
|
|
49
|
+
logCapture.append(options.stderrPath, rawStderr);
|
|
50
|
+
logCapture.flush(exactRedactionValues);
|
|
48
51
|
return {
|
|
49
52
|
exitCode: result.status,
|
|
50
53
|
stdout: stdout.length > maxStdout ? stdout.slice(0, maxStdout) : stdout,
|
|
@@ -52,21 +55,30 @@ export function runSimpleProcessSync(options) {
|
|
|
52
55
|
// If timed out, don't report spawnError — it's a timeout, not a spawn failure
|
|
53
56
|
spawnError: timedOut ? null : (result.error ? result.error.message : null),
|
|
54
57
|
timedOut,
|
|
55
|
-
stdoutTruncated: stdout.length > maxStdout,
|
|
56
|
-
stderrTruncated: stderr.length > maxStderr,
|
|
58
|
+
stdoutTruncated: rawStdout.length > maxStdout || stdout.length > maxStdout,
|
|
59
|
+
stderrTruncated: rawStderr.length > maxStderr || stderr.length > maxStderr,
|
|
57
60
|
};
|
|
58
61
|
}
|
|
59
62
|
export async function runSimpleProcess(options) {
|
|
60
63
|
const maxStdout = options.maxStdoutBytes ?? DEFAULT_MAX_STDOUT;
|
|
61
64
|
const maxStderr = options.maxStderrBytes ?? DEFAULT_MAX_STDERR;
|
|
62
|
-
const
|
|
65
|
+
const exactRedactionValues = allowedEnvironmentValues(options.environmentVariableNames);
|
|
66
|
+
const logCapture = new SecureProcessLogCapture([options.stdoutPath, options.stderrPath], options.maxLogBytes);
|
|
63
67
|
let child;
|
|
64
68
|
try {
|
|
65
|
-
|
|
69
|
+
const env = buildChildEnvironment({
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
allowedNames: options.environmentVariableNames,
|
|
72
|
+
blockedNames: options.blockedEnvironmentVariableNames,
|
|
73
|
+
});
|
|
74
|
+
const prepared = prepareShellFreeCommand(options.command, options.args, options.cwd, { pathValue: env.PATH });
|
|
75
|
+
const command = resolveTrustedExecutable(prepared.command, options.cwd, { pathValue: env.PATH });
|
|
76
|
+
child = spawn(command, prepared.args, {
|
|
66
77
|
cwd: options.cwd,
|
|
67
78
|
stdio: ["ignore", "pipe", "pipe"],
|
|
68
79
|
detached: process.platform !== "win32",
|
|
69
80
|
windowsHide: true,
|
|
81
|
+
env,
|
|
70
82
|
});
|
|
71
83
|
}
|
|
72
84
|
catch (error) {
|
|
@@ -80,8 +92,6 @@ export async function runSimpleProcess(options) {
|
|
|
80
92
|
stderrTruncated: false,
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
|
-
const stdoutStream = options.stdoutPath ? createWriteStream(options.stdoutPath, { flags: "a" }) : null;
|
|
84
|
-
const stderrStream = options.stderrPath ? createWriteStream(options.stderrPath, { flags: "a" }) : null;
|
|
85
95
|
let stdoutBuf = Buffer.alloc(0);
|
|
86
96
|
let stderrBuf = Buffer.alloc(0);
|
|
87
97
|
let stdoutTruncated = false;
|
|
@@ -105,7 +115,7 @@ export async function runSimpleProcess(options) {
|
|
|
105
115
|
}
|
|
106
116
|
};
|
|
107
117
|
child.stdout?.on("data", (chunk) => {
|
|
108
|
-
|
|
118
|
+
logCapture.append(options.stdoutPath, chunk);
|
|
109
119
|
if (stdoutBuf.length < maxStdout) {
|
|
110
120
|
const remaining = maxStdout - stdoutBuf.length;
|
|
111
121
|
if (chunk.length <= remaining) {
|
|
@@ -121,7 +131,7 @@ export async function runSimpleProcess(options) {
|
|
|
121
131
|
}
|
|
122
132
|
});
|
|
123
133
|
child.stderr?.on("data", (chunk) => {
|
|
124
|
-
|
|
134
|
+
logCapture.append(options.stderrPath, chunk);
|
|
125
135
|
if (stderrBuf.length < maxStderr) {
|
|
126
136
|
const remaining = maxStderr - stderrBuf.length;
|
|
127
137
|
if (chunk.length <= remaining) {
|
|
@@ -146,7 +156,6 @@ export async function runSimpleProcess(options) {
|
|
|
146
156
|
resolveExit(code);
|
|
147
157
|
};
|
|
148
158
|
child.once("close", (code) => finish(code));
|
|
149
|
-
child.once("exit", (code) => finish(code));
|
|
150
159
|
child.once("error", (error) => {
|
|
151
160
|
spawnError = error.message;
|
|
152
161
|
finish(null);
|
|
@@ -155,16 +164,17 @@ export async function runSimpleProcess(options) {
|
|
|
155
164
|
clearTimeout(timeoutTimer);
|
|
156
165
|
if (forceTimer)
|
|
157
166
|
clearTimeout(forceTimer);
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
logCapture.flush(exactRedactionValues);
|
|
168
|
+
const stdout = redactProcessOutput(stdoutBuf.toString("utf-8"), exactRedactionValues);
|
|
169
|
+
const stderr = redactProcessOutput(stderrBuf.toString("utf-8"), exactRedactionValues);
|
|
160
170
|
return {
|
|
161
171
|
exitCode,
|
|
162
|
-
stdout:
|
|
163
|
-
stderr:
|
|
172
|
+
stdout: stdout.length > maxStdout ? stdout.slice(0, maxStdout) : stdout,
|
|
173
|
+
stderr: stderr.length > maxStderr ? stderr.slice(0, maxStderr) : stderr,
|
|
164
174
|
spawnError,
|
|
165
175
|
timedOut,
|
|
166
|
-
stdoutTruncated,
|
|
167
|
-
stderrTruncated,
|
|
176
|
+
stdoutTruncated: stdoutTruncated || stdout.length > maxStdout,
|
|
177
|
+
stderrTruncated: stderrTruncated || stderr.length > maxStderr,
|
|
168
178
|
};
|
|
169
179
|
}
|
|
170
180
|
function gracefulKill(child) {
|
|
@@ -181,10 +191,13 @@ function forceKill(child) {
|
|
|
181
191
|
return;
|
|
182
192
|
try {
|
|
183
193
|
if (process.platform === "win32") {
|
|
184
|
-
const
|
|
194
|
+
const systemRoot = process.env.SystemRoot || process.env.WINDIR || "C:\\Windows";
|
|
195
|
+
const taskkill = resolveTrustedExecutable(`${systemRoot}\\System32\\taskkill.exe`, process.cwd());
|
|
196
|
+
const result = spawnSync(taskkill, ["/PID", String(child.pid), "/T", "/F"], {
|
|
185
197
|
stdio: "ignore",
|
|
186
198
|
timeout: 5000,
|
|
187
199
|
windowsHide: true,
|
|
200
|
+
env: buildChildEnvironment({ cwd: process.cwd() }),
|
|
188
201
|
});
|
|
189
202
|
if (result.status !== 0)
|
|
190
203
|
child.kill("SIGKILL");
|
|
@@ -200,7 +213,8 @@ function forceKill(child) {
|
|
|
200
213
|
catch { } // cleanup failure is safe to ignore
|
|
201
214
|
}
|
|
202
215
|
}
|
|
203
|
-
function
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
function errorCode(error) {
|
|
217
|
+
return error && typeof error === "object" && "code" in error
|
|
218
|
+
? String(error.code || "")
|
|
219
|
+
: null;
|
|
206
220
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
2
|
+
import { atomicWriteFileSync } from "../utils/atomicFile.js";
|
|
3
3
|
const PHASES = [
|
|
4
4
|
{ phase: "queued", label: "Queued" },
|
|
5
5
|
{ phase: "preparing", label: "Preparing repository snapshot" },
|
|
@@ -32,5 +32,5 @@ export function writeTaskProgress(taskDir, phase, options) {
|
|
|
32
32
|
lines.push(`Current command: ${options.currentCommand}`);
|
|
33
33
|
if (options?.note)
|
|
34
34
|
lines.push("", `Note: ${options.note}`);
|
|
35
|
-
|
|
35
|
+
atomicWriteFileSync(join(taskDir, "progress.md"), `${lines.join("\n")}\n`);
|
|
36
36
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
3
4
|
export function readTaskRuntime(taskDir) {
|
|
4
5
|
const runtimeFile = join(taskDir, "runtime.json");
|
|
5
6
|
if (!existsSync(runtimeFile))
|
|
@@ -20,6 +21,6 @@ export function writeTaskRuntime(taskDir, patch) {
|
|
|
20
21
|
...current,
|
|
21
22
|
...patch,
|
|
22
23
|
};
|
|
23
|
-
|
|
24
|
+
atomicWriteJsonFileSync(join(taskDir, "runtime.json"), next);
|
|
24
25
|
return next;
|
|
25
26
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type LockedJsonMutation } from "../utils/lockedJsonFile.js";
|
|
2
|
+
export type TaskStatusRecord = Record<string, unknown>;
|
|
3
|
+
export type TaskStatusMutation<T> = LockedJsonMutation<TaskStatusRecord, T>;
|
|
4
|
+
export interface TaskClaimResult {
|
|
5
|
+
claimed: boolean;
|
|
6
|
+
status: TaskStatusRecord;
|
|
7
|
+
}
|
|
8
|
+
export declare function readTaskStatusFile(statusFile: string): TaskStatusRecord;
|
|
9
|
+
export declare function mutateTaskStatus<T>(statusFile: string, mutation: (current: TaskStatusRecord) => TaskStatusMutation<T>): T;
|
|
10
|
+
export declare function claimPendingTask(statusFile: string, patch: TaskStatusRecord): TaskClaimResult;
|
|
11
|
+
export declare function updateTaskStatusFile(statusFile: string, patch: TaskStatusRecord): TaskStatusRecord;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PatchWardenError } from "../errors.js";
|
|
2
|
+
import { mutateLockedJsonFileSync, readJsonObjectFileSync, } from "../utils/lockedJsonFile.js";
|
|
3
|
+
export function readTaskStatusFile(statusFile) {
|
|
4
|
+
return readJsonObjectFileSync(statusFile);
|
|
5
|
+
}
|
|
6
|
+
export function mutateTaskStatus(statusFile, mutation) {
|
|
7
|
+
return mutateLockedJsonFileSync(statusFile, mutation, {
|
|
8
|
+
busyError: () => new PatchWardenError("task_status_busy", "Task status is currently being updated by another PatchWarden process.", "Retry the operation after the current task status update completes.", true),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function claimPendingTask(statusFile, patch) {
|
|
12
|
+
return mutateTaskStatus(statusFile, (current) => {
|
|
13
|
+
if (current.status !== "pending")
|
|
14
|
+
return { result: { claimed: false, status: current } };
|
|
15
|
+
const next = { ...current, ...patch, updated_at: new Date().toISOString() };
|
|
16
|
+
return { next, result: { claimed: true, status: next } };
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function updateTaskStatusFile(statusFile, patch) {
|
|
20
|
+
return mutateTaskStatus(statusFile, (current) => {
|
|
21
|
+
const next = { ...current, ...patch, updated_at: new Date().toISOString() };
|
|
22
|
+
return { next, result: next };
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/runner/watch.d.ts
CHANGED
|
@@ -16,4 +16,17 @@
|
|
|
16
16
|
* Run: node dist/runner/watch.js
|
|
17
17
|
* or: npm run watch
|
|
18
18
|
*/
|
|
19
|
-
export {
|
|
19
|
+
export declare class WatcherAlreadyRunningError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Acquire the watcher lock at `lockFilePath`. If a lock file exists and its
|
|
24
|
+
* recorded PID is still alive (per `process.kill(pid, 0)`), throws
|
|
25
|
+
* {@link WatcherAlreadyRunningError}. Otherwise overwrites the lock atomically.
|
|
26
|
+
*
|
|
27
|
+
* Exported for unit testing; `lockFilePath` defaults to the module-level
|
|
28
|
+
* {@link WATCHER_LOCK_FILE} but tests pass a temp path.
|
|
29
|
+
*/
|
|
30
|
+
export declare function acquireWatcherLock(lockFilePath?: string): void;
|
|
31
|
+
export declare function releaseWatcherLock(lockFilePath?: string, instanceId?: string, pid?: number): boolean;
|
|
32
|
+
export declare function createNonOverlappingRunner(operation: () => Promise<void>, onSkipped?: () => void): () => Promise<boolean>;
|
package/dist/runner/watch.js
CHANGED
|
@@ -16,15 +16,21 @@
|
|
|
16
16
|
* Run: node dist/runner/watch.js
|
|
17
17
|
* or: npm run watch
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
20
20
|
import { resolve, join, dirname } from "node:path";
|
|
21
|
+
import { pathToFileURL } from "node:url";
|
|
21
22
|
import { loadConfig, getConfig, getTasksDir, resolveWorkspaceRoot } from "../config.js";
|
|
22
23
|
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
23
24
|
import { guardAgentCommand, guardTestCommand } from "../security/commandGuard.js";
|
|
24
25
|
import { validateAssessmentFreshness } from "../assessments/assessmentStore.js";
|
|
25
26
|
import { captureRepoSnapshot } from "./changeCapture.js";
|
|
26
|
-
import {
|
|
27
|
+
import { mutateTaskStatus } from "./taskStatusStore.js";
|
|
27
28
|
import { logger } from "../logging.js";
|
|
29
|
+
import { redactSensitiveContent } from "../security/contentRedaction.js";
|
|
30
|
+
import { atomicWriteFileSync } from "../utils/atomicFile.js";
|
|
31
|
+
// Note: `runTask` is imported lazily inside `tick()` via `await import("./runTask.js")`.
|
|
32
|
+
// This keeps `import { acquireWatcherLock }` side-effect-free for unit tests and
|
|
33
|
+
// avoids eagerly pulling in the full tools/dispatch graph at module load.
|
|
28
34
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
29
35
|
loadConfig();
|
|
30
36
|
const config = getConfig();
|
|
@@ -32,34 +38,160 @@ const tasksDir = getTasksDir(config);
|
|
|
32
38
|
const wsRoot = resolveWorkspaceRoot(config);
|
|
33
39
|
const POLL_INTERVAL_MS = 4000;
|
|
34
40
|
const WATCHER_HEARTBEAT_FILE = join(dirname(tasksDir), "watcher-heartbeat.json");
|
|
41
|
+
const WATCHER_LOCK_FILE = join(dirname(tasksDir), "watcher.lock");
|
|
35
42
|
const WATCHER_STARTED_AT = new Date().toISOString();
|
|
36
43
|
const WATCHER_INSTANCE_ID = process.env.PATCHWARDEN_WATCHER_INSTANCE_ID || `standalone-${process.pid}-${Date.now()}`;
|
|
37
44
|
const WATCHER_LAUNCHER_PID = Number.isInteger(Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID))
|
|
38
45
|
? Number(process.env.PATCHWARDEN_WATCHER_LAUNCHER_PID)
|
|
39
46
|
: null;
|
|
40
|
-
mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
|
|
41
|
-
logger.info(`[watcher] Workspace: ${wsRoot}`);
|
|
42
|
-
logger.info(`[watcher] Tasks: ${tasksDir}`);
|
|
43
|
-
logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
|
44
|
-
logger.info(`[watcher] Press Ctrl+C to stop`);
|
|
45
47
|
// Track executed tasks to prevent re-execution
|
|
46
48
|
const executedTasks = new Set();
|
|
49
|
+
let consecutiveTickFailures = 0;
|
|
50
|
+
// ── Single-instance lock ─────────────────────────────────────────
|
|
51
|
+
// watch.ts is a long-running poller; without a lock, two watchers started
|
|
52
|
+
// against the same workspace would race on the same pending tasks. The lock
|
|
53
|
+
// is best-effort: it detects a live PID via process.kill(pid, 0) and refuses
|
|
54
|
+
// to start when one is found. A dead PID (crashed watcher) is taken over.
|
|
55
|
+
export class WatcherAlreadyRunningError extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "WatcherAlreadyRunningError";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function errorCode(error) {
|
|
62
|
+
return error && typeof error === "object" && "code" in error
|
|
63
|
+
? String(error.code || "")
|
|
64
|
+
: null;
|
|
65
|
+
}
|
|
66
|
+
function readWatcherLock(lockFilePath) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(readFileSync(lockFilePath, "utf-8"));
|
|
69
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
70
|
+
? parsed
|
|
71
|
+
: null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function processIsAlive(pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
// EPERM means the process exists but the current user cannot signal it.
|
|
84
|
+
return errorCode(error) === "EPERM";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function runningWatcherError(lockData) {
|
|
88
|
+
return new WatcherAlreadyRunningError(`[watcher] Already running (pid=${lockData?.pid ?? "unknown"}, instance=${lockData?.instance_id ?? "unknown"}). Exiting.`);
|
|
89
|
+
}
|
|
90
|
+
function createWatcherLockExclusive(lockFilePath, content) {
|
|
91
|
+
const descriptor = openSync(lockFilePath, "wx", 0o600);
|
|
92
|
+
let writeError = null;
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(descriptor, content, "utf-8");
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
writeError = error;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
closeSync(descriptor);
|
|
101
|
+
}
|
|
102
|
+
if (writeError) {
|
|
103
|
+
try {
|
|
104
|
+
unlinkSync(lockFilePath);
|
|
105
|
+
}
|
|
106
|
+
catch { /* best effort after a partial write */ }
|
|
107
|
+
throw writeError;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Acquire the watcher lock at `lockFilePath`. If a lock file exists and its
|
|
112
|
+
* recorded PID is still alive (per `process.kill(pid, 0)`), throws
|
|
113
|
+
* {@link WatcherAlreadyRunningError}. Otherwise overwrites the lock atomically.
|
|
114
|
+
*
|
|
115
|
+
* Exported for unit testing; `lockFilePath` defaults to the module-level
|
|
116
|
+
* {@link WATCHER_LOCK_FILE} but tests pass a temp path.
|
|
117
|
+
*/
|
|
118
|
+
export function acquireWatcherLock(lockFilePath = WATCHER_LOCK_FILE) {
|
|
119
|
+
const lockContent = JSON.stringify({
|
|
120
|
+
pid: process.pid,
|
|
121
|
+
instance_id: WATCHER_INSTANCE_ID,
|
|
122
|
+
started_at: WATCHER_STARTED_AT,
|
|
123
|
+
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
124
|
+
}, null, 2);
|
|
125
|
+
try {
|
|
126
|
+
createWatcherLockExclusive(lockFilePath, lockContent);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (errorCode(error) !== "EEXIST")
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
const existing = readWatcherLock(lockFilePath);
|
|
134
|
+
if (typeof existing?.pid === "number" && processIsAlive(existing.pid)) {
|
|
135
|
+
throw runningWatcherError(existing);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
unlinkSync(lockFilePath);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (errorCode(error) !== "ENOENT")
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
createWatcherLockExclusive(lockFilePath, lockContent);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (errorCode(error) === "EEXIST") {
|
|
149
|
+
throw runningWatcherError(readWatcherLock(lockFilePath));
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function releaseWatcherLock(lockFilePath = WATCHER_LOCK_FILE, instanceId = WATCHER_INSTANCE_ID, pid = process.pid) {
|
|
155
|
+
const existing = readWatcherLock(lockFilePath);
|
|
156
|
+
if (existing?.pid !== pid || existing.instance_id !== instanceId)
|
|
157
|
+
return false;
|
|
158
|
+
try {
|
|
159
|
+
unlinkSync(lockFilePath);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (errorCode(error) === "ENOENT")
|
|
164
|
+
return false;
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function createNonOverlappingRunner(operation, onSkipped = () => undefined) {
|
|
169
|
+
let running = false;
|
|
170
|
+
return async () => {
|
|
171
|
+
if (running) {
|
|
172
|
+
onSkipped();
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
running = true;
|
|
176
|
+
try {
|
|
177
|
+
await operation();
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
running = false;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
47
185
|
// ── Main loop ─────────────────────────────────────────────────────
|
|
48
186
|
async function tick() {
|
|
49
187
|
try {
|
|
50
|
-
|
|
51
|
-
writeFileSync(heartbeatTemporary, JSON.stringify({
|
|
52
|
-
status: "running",
|
|
53
|
-
pid: process.pid,
|
|
54
|
-
started_at: WATCHER_STARTED_AT,
|
|
55
|
-
last_heartbeat_at: new Date().toISOString(),
|
|
56
|
-
instance_id: WATCHER_INSTANCE_ID,
|
|
57
|
-
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
58
|
-
}, null, 2), "utf-8");
|
|
59
|
-
renameSync(heartbeatTemporary, WATCHER_HEARTBEAT_FILE);
|
|
188
|
+
writeWatcherHeartbeat("running");
|
|
60
189
|
// Ensure tasks directory exists
|
|
61
|
-
if (!existsSync(tasksDir))
|
|
190
|
+
if (!existsSync(tasksDir)) {
|
|
191
|
+
consecutiveTickFailures = 0;
|
|
192
|
+
writeWatcherHeartbeat("running");
|
|
62
193
|
return;
|
|
194
|
+
}
|
|
63
195
|
const entries = readdirSync(tasksDir, { withFileTypes: true });
|
|
64
196
|
const taskDirs = entries.filter((e) => e.isDirectory());
|
|
65
197
|
for (const entry of taskDirs) {
|
|
@@ -73,21 +205,34 @@ async function tick() {
|
|
|
73
205
|
continue;
|
|
74
206
|
let statusData;
|
|
75
207
|
try {
|
|
76
|
-
|
|
208
|
+
const parsed = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
209
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
210
|
+
throw new Error("status.json must contain an object");
|
|
211
|
+
}
|
|
212
|
+
statusData = parsed;
|
|
77
213
|
}
|
|
78
|
-
catch {
|
|
79
|
-
|
|
214
|
+
catch (error) {
|
|
215
|
+
logger.warn("[watcher] Skipping unreadable task status", {
|
|
216
|
+
task_id: taskId,
|
|
217
|
+
error: error instanceof Error ? error.message : String(error),
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
80
220
|
}
|
|
81
221
|
if (statusData.status !== "pending")
|
|
82
222
|
continue;
|
|
83
223
|
// ── Pre-flight safety checks ──
|
|
84
224
|
try {
|
|
85
225
|
// Check repo_path
|
|
86
|
-
const
|
|
226
|
+
const repoPath = typeof statusData.resolved_repo_path === "string"
|
|
227
|
+
? statusData.resolved_repo_path
|
|
228
|
+
: typeof statusData.repo_path === "string"
|
|
229
|
+
? statusData.repo_path
|
|
230
|
+
: wsRoot;
|
|
231
|
+
const resolvedRepoPath = guardWorkspacePath(repoPath, wsRoot);
|
|
87
232
|
// Check agent
|
|
88
|
-
guardAgentCommand(statusData.agent, config);
|
|
233
|
+
guardAgentCommand(typeof statusData.agent === "string" ? statusData.agent : "", config);
|
|
89
234
|
// Check test_command
|
|
90
|
-
if (statusData.test_command) {
|
|
235
|
+
if (typeof statusData.test_command === "string" && statusData.test_command) {
|
|
91
236
|
guardTestCommand(statusData.test_command, config, resolvedRepoPath);
|
|
92
237
|
}
|
|
93
238
|
if (Array.isArray(statusData.verify_commands)) {
|
|
@@ -96,25 +241,31 @@ async function tick() {
|
|
|
96
241
|
}
|
|
97
242
|
}
|
|
98
243
|
// Assessment freshness revalidation
|
|
99
|
-
if (statusData.assessment_id) {
|
|
244
|
+
if (typeof statusData.assessment_id === "string" && statusData.assessment_id) {
|
|
100
245
|
const preExecSnapshot = await captureRepoSnapshot(resolvedRepoPath);
|
|
101
|
-
const validation = validateAssessmentFreshness(
|
|
246
|
+
const validation = validateAssessmentFreshness(statusData.assessment_id, preExecSnapshot);
|
|
102
247
|
if (!validation.valid) {
|
|
103
248
|
throw new Error(`assessment validation failed: ${validation.failure_reason}`);
|
|
104
249
|
}
|
|
105
250
|
}
|
|
106
251
|
}
|
|
107
252
|
catch (err) {
|
|
108
|
-
const errMsg = `[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}
|
|
253
|
+
const errMsg = redactSensitiveContent(`[watcher] Safety check failed for ${taskId}: ${err instanceof Error ? err.message : String(err)}`).content;
|
|
109
254
|
logger.error(errMsg);
|
|
110
255
|
// Write error and mark as failed so it doesn't get re-picked
|
|
111
256
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
257
|
+
atomicWriteFileSync(join(taskDir, "error.log"), errMsg);
|
|
258
|
+
mutateTaskStatus(statusFile, (current) => {
|
|
259
|
+
if (current.status !== "pending")
|
|
260
|
+
return { result: undefined };
|
|
261
|
+
const next = {
|
|
262
|
+
...current,
|
|
263
|
+
status: "failed",
|
|
264
|
+
error: errMsg,
|
|
265
|
+
updated_at: new Date().toISOString(),
|
|
266
|
+
};
|
|
267
|
+
return { next, result: undefined };
|
|
268
|
+
});
|
|
118
269
|
}
|
|
119
270
|
catch (err) {
|
|
120
271
|
logger.warn("watcher error-log write failed", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -126,6 +277,7 @@ async function tick() {
|
|
|
126
277
|
logger.info(`[watcher] Executing: ${taskId}`);
|
|
127
278
|
executedTasks.add(taskId);
|
|
128
279
|
try {
|
|
280
|
+
const { runTask } = await import("./runTask.js");
|
|
129
281
|
const result = await runTask(taskId);
|
|
130
282
|
logger.info(`[watcher] ${taskId} → ${result.status}`);
|
|
131
283
|
}
|
|
@@ -133,22 +285,78 @@ async function tick() {
|
|
|
133
285
|
logger.error(`[watcher] ${taskId} → error: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
286
|
}
|
|
135
287
|
}
|
|
288
|
+
consecutiveTickFailures = 0;
|
|
289
|
+
writeWatcherHeartbeat("running");
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
consecutiveTickFailures += 1;
|
|
293
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
294
|
+
logger.error(`[watcher] Tick error (${consecutiveTickFailures} consecutive): ${message}`);
|
|
295
|
+
try {
|
|
296
|
+
writeWatcherHeartbeat(consecutiveTickFailures >= 3 ? "degraded" : "running", message);
|
|
297
|
+
}
|
|
298
|
+
catch (heartbeatError) {
|
|
299
|
+
logger.error("[watcher] Failed to persist degraded heartbeat", {
|
|
300
|
+
error: heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function writeWatcherHeartbeat(status, lastError) {
|
|
306
|
+
atomicWriteFileSync(WATCHER_HEARTBEAT_FILE, JSON.stringify({
|
|
307
|
+
status,
|
|
308
|
+
pid: process.pid,
|
|
309
|
+
started_at: WATCHER_STARTED_AT,
|
|
310
|
+
last_heartbeat_at: new Date().toISOString(),
|
|
311
|
+
instance_id: WATCHER_INSTANCE_ID,
|
|
312
|
+
launcher_pid: WATCHER_LAUNCHER_PID,
|
|
313
|
+
consecutive_failures: consecutiveTickFailures,
|
|
314
|
+
last_error: lastError ? lastError.slice(0, 500) : null,
|
|
315
|
+
}, null, 2));
|
|
316
|
+
}
|
|
317
|
+
// ── Start (only when executed as a script) ─────────────────────────
|
|
318
|
+
// The bootstrap below has process-wide side effects (mkdir, signal handlers,
|
|
319
|
+
// polling interval, lock acquisition). Guarding it with a main-module check
|
|
320
|
+
// keeps `import { acquireWatcherLock }` safe for unit tests — without it,
|
|
321
|
+
// importing this module would start polling the real tasks dir and register
|
|
322
|
+
// SIGINT/SIGTERM handlers that interfere with the test runner.
|
|
323
|
+
const isMainModule = !!process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
324
|
+
if (isMainModule) {
|
|
325
|
+
mkdirSync(dirname(WATCHER_HEARTBEAT_FILE), { recursive: true });
|
|
326
|
+
logger.info(`[watcher] Workspace: ${wsRoot}`);
|
|
327
|
+
logger.info(`[watcher] Tasks: ${tasksDir}`);
|
|
328
|
+
logger.info(`[watcher] Polling every ${POLL_INTERVAL_MS / 1000}s`);
|
|
329
|
+
logger.info(`[watcher] Press Ctrl+C to stop`);
|
|
330
|
+
try {
|
|
331
|
+
acquireWatcherLock(WATCHER_LOCK_FILE);
|
|
136
332
|
}
|
|
137
333
|
catch (err) {
|
|
138
|
-
|
|
334
|
+
if (err instanceof WatcherAlreadyRunningError) {
|
|
335
|
+
logger.error(err.message);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
throw err;
|
|
339
|
+
}
|
|
340
|
+
logger.info("[watcher] Started");
|
|
341
|
+
const scheduledTick = createNonOverlappingRunner(tick, () => {
|
|
342
|
+
logger.warn("[watcher] Previous tick is still running; skipping overlapping poll");
|
|
343
|
+
});
|
|
344
|
+
setInterval(() => { void scheduledTick(); }, POLL_INTERVAL_MS);
|
|
345
|
+
// Run first tick immediately
|
|
346
|
+
void scheduledTick();
|
|
347
|
+
// Graceful shutdown
|
|
348
|
+
function shutdown() {
|
|
349
|
+
logger.info("[watcher] Stopped");
|
|
350
|
+
try {
|
|
351
|
+
releaseWatcherLock(WATCHER_LOCK_FILE);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
logger.warn("[watcher] Failed to release watcher lock", {
|
|
355
|
+
error: error instanceof Error ? error.message : String(error),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
process.exit(0);
|
|
139
359
|
}
|
|
360
|
+
process.once("SIGINT", shutdown);
|
|
361
|
+
process.once("SIGTERM", shutdown);
|
|
140
362
|
}
|
|
141
|
-
// ── Start ─────────────────────────────────────────────────────────
|
|
142
|
-
logger.info("[watcher] Started");
|
|
143
|
-
setInterval(tick, POLL_INTERVAL_MS);
|
|
144
|
-
// Run first tick immediately
|
|
145
|
-
tick();
|
|
146
|
-
// Graceful shutdown
|
|
147
|
-
process.on("SIGINT", () => {
|
|
148
|
-
logger.info("[watcher] Stopped");
|
|
149
|
-
process.exit(0);
|
|
150
|
-
});
|
|
151
|
-
process.on("SIGTERM", () => {
|
|
152
|
-
logger.info("[watcher] Stopped");
|
|
153
|
-
process.exit(0);
|
|
154
|
-
});
|