patchwarden 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PatchWarden.cmd +32 -2
- package/README.en.md +194 -26
- package/README.md +160 -22
- package/dist/assessments/agentAssessor.d.ts +1 -1
- package/dist/assessments/agentAssessor.js +24 -15
- package/dist/assessments/assessmentStore.d.ts +1 -1
- package/dist/assessments/assessmentStore.js +44 -36
- package/dist/assessments/confirmCli.js +5 -4
- package/dist/config.d.ts +22 -0
- package/dist/config.js +98 -11
- package/dist/control/fileManager.d.ts +2 -0
- package/dist/control/fileManager.js +39 -0
- package/dist/control/middleware/auth.d.ts +16 -0
- package/dist/control/middleware/auth.js +17 -0
- package/dist/control/middleware/static.d.ts +3 -0
- package/dist/control/middleware/static.js +78 -0
- package/dist/control/routeTable.d.ts +24 -0
- package/dist/control/routeTable.js +263 -0
- package/dist/control/routes/audit.d.ts +15 -0
- package/dist/control/routes/audit.js +287 -0
- package/dist/control/routes/evidence.d.ts +4 -0
- package/dist/control/routes/evidence.js +96 -0
- package/dist/control/routes/lineage.d.ts +3 -0
- package/dist/control/routes/lineage.js +71 -0
- package/dist/control/routes/policy.d.ts +3 -0
- package/dist/control/routes/policy.js +81 -0
- package/dist/control/routes/process.d.ts +12 -0
- package/dist/control/routes/process.js +487 -0
- package/dist/control/routes/sessions.d.ts +30 -0
- package/dist/control/routes/sessions.js +287 -0
- package/dist/control/routes/status.d.ts +51 -0
- package/dist/control/routes/status.js +307 -0
- package/dist/control/routes/taskActions.d.ts +21 -0
- package/dist/control/routes/taskActions.js +197 -0
- package/dist/control/routes/tasks.d.ts +24 -0
- package/dist/control/routes/tasks.js +309 -0
- package/dist/control/routes/workspace.d.ts +15 -0
- package/dist/control/routes/workspace.js +220 -0
- package/dist/control/runtime.d.ts +92 -0
- package/dist/control/runtime.js +425 -0
- package/dist/control/server.d.ts +13 -0
- package/dist/control/server.js +150 -0
- package/dist/control/shared.d.ts +59 -0
- package/dist/control/shared.js +341 -0
- package/dist/controlCenter.d.ts +6 -0
- package/dist/controlCenter.js +7 -2197
- package/dist/direct/directAudit.js +234 -225
- package/dist/direct/directGuards.d.ts +2 -1
- package/dist/direct/directGuards.js +72 -37
- package/dist/direct/directPatch.d.ts +6 -1
- package/dist/direct/directPatch.js +55 -8
- package/dist/direct/directSessionStore.d.ts +16 -6
- package/dist/direct/directSessionStore.js +127 -35
- package/dist/direct/directVerification.js +8 -16
- package/dist/doctor.d.ts +18 -1
- package/dist/doctor.js +614 -373
- package/dist/goal/acceptanceEngine.d.ts +1 -1
- package/dist/goal/goalProgress.js +75 -69
- package/dist/goal/goalReport.d.ts +54 -0
- package/dist/goal/goalReport.js +197 -0
- package/dist/goal/goalStatus.d.ts +8 -0
- package/dist/goal/goalStatus.js +10 -3
- package/dist/goal/goalStore.d.ts +9 -2
- package/dist/goal/goalStore.js +152 -45
- package/dist/goal/handoffExport.js +6 -6
- package/dist/goal/specKitImport.d.ts +63 -0
- package/dist/goal/specKitImport.js +221 -0
- package/dist/goal/subgoalSync.js +13 -10
- package/dist/goal/worktreeManager.d.ts +2 -0
- package/dist/goal/worktreeManager.js +209 -136
- package/dist/httpServer.js +126 -44
- package/dist/index.js +7 -4
- package/dist/logging.d.ts +7 -1
- package/dist/logging.js +13 -3
- package/dist/policy/projectPolicy.js +5 -2
- package/dist/release/releaseGate.d.ts +1 -1
- package/dist/release/releaseGate.js +78 -25
- package/dist/runner/agentInvocation.d.ts +14 -0
- package/dist/runner/agentInvocation.js +96 -4
- package/dist/runner/changeCapture.d.ts +14 -6
- package/dist/runner/changeCapture.js +287 -81
- package/dist/runner/cli.js +7 -6
- package/dist/runner/postTaskCleanup.js +60 -7
- package/dist/runner/processSecurity.d.ts +56 -0
- package/dist/runner/processSecurity.js +373 -0
- package/dist/runner/runTask.d.ts +1 -1
- package/dist/runner/runTask.js +339 -287
- package/dist/runner/simpleProcess.d.ts +3 -0
- package/dist/runner/simpleProcess.js +52 -38
- package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
- package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
- package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
- package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
- package/dist/runner/taskStatusStore.d.ts +11 -0
- package/dist/runner/taskStatusStore.js +24 -0
- package/dist/runner/watch.d.ts +14 -1
- package/dist/runner/watch.js +265 -54
- package/dist/security/contentRedaction.d.ts +6 -0
- package/dist/security/contentRedaction.js +24 -2
- package/dist/security/discoveryTokenStore.d.ts +3 -2
- package/dist/security/discoveryTokenStore.js +17 -2
- package/dist/security/loopbackHost.d.ts +2 -0
- package/dist/security/loopbackHost.js +16 -0
- package/dist/security/pathGuard.js +32 -17
- package/dist/security/planGuard.js +2 -1
- package/dist/security/runtimeGuard.js +9 -6
- package/dist/security/secretComparison.d.ts +3 -0
- package/dist/security/secretComparison.js +11 -0
- package/dist/security/sensitiveGuard.d.ts +5 -0
- package/dist/security/sensitiveGuard.js +27 -8
- package/dist/security/toolInvocationGuard.d.ts +2 -2
- package/dist/security/toolInvocationGuard.js +1 -1
- package/dist/security/workspaceRootGuard.d.ts +8 -0
- package/dist/security/workspaceRootGuard.js +47 -0
- package/dist/smoke-test.js +280 -273
- package/dist/tools/catalog/index.d.ts +4 -0
- package/dist/tools/catalog/index.js +4 -0
- package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
- package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
- package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
- package/dist/tools/definitions/toolDefs.d.ts +18 -0
- package/dist/tools/definitions/toolDefs.js +1177 -0
- package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
- package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
- package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
- package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
- package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
- package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
- package/dist/tools/diagnostics/index.d.ts +6 -0
- package/dist/tools/diagnostics/index.js +6 -0
- package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
- package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
- package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
- package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
- package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
- package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
- package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
- package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
- package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
- package/dist/tools/direct/finalizeDirectSession.js +86 -0
- package/dist/tools/direct/index.d.ts +3 -0
- package/dist/tools/direct/index.js +3 -0
- package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
- package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
- package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
- package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
- package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
- package/dist/tools/discovery/index.d.ts +3 -0
- package/dist/tools/discovery/index.js +3 -0
- package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
- package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
- package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
- package/dist/tools/dispatch/coreDispatch.js +283 -0
- package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
- package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
- package/dist/tools/dispatch/directDispatch.d.ts +8 -0
- package/dist/tools/dispatch/directDispatch.js +116 -0
- package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
- package/dist/tools/dispatch/goalDispatch.js +92 -0
- package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
- package/dist/tools/dispatch/releaseDispatch.js +46 -0
- package/dist/tools/dispatch/types.d.ts +23 -0
- package/dist/tools/dispatch/types.js +15 -0
- package/dist/tools/dispatch/validation.d.ts +7 -0
- package/dist/tools/dispatch/validation.js +71 -0
- package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
- package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
- package/dist/tools/goals/goalSubgoalTask.js +118 -0
- package/dist/tools/goals/index.d.ts +3 -0
- package/dist/tools/goals/index.js +3 -0
- package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
- package/dist/tools/registry.d.ts +10 -14
- package/dist/tools/registry.js +47 -1651
- package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
- package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
- package/dist/tools/release/index.d.ts +2 -0
- package/dist/tools/release/index.js +2 -0
- package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
- package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
- package/dist/tools/tasks/cancelTask.d.ts +11 -0
- package/dist/tools/tasks/cancelTask.js +85 -0
- package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
- package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
- package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
- package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
- package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
- package/dist/tools/tasks/evidencePack.js +387 -0
- package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
- package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
- package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
- package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
- package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
- package/dist/tools/tasks/index.d.ts +18 -0
- package/dist/tools/tasks/index.js +18 -0
- package/dist/tools/tasks/killTask.d.ts +1 -0
- package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
- package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
- package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
- package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
- package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
- package/dist/tools/tasks/retryTask.js +60 -0
- package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
- package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
- package/dist/tools/tasks/runVerification.js +38 -0
- package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
- package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
- package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
- package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
- package/dist/tools/workspace/applyPatch.js +53 -0
- package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
- package/dist/tools/workspace/index.d.ts +10 -0
- package/dist/tools/workspace/index.js +10 -0
- package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
- package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
- package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
- package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
- package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
- package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
- package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
- package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
- package/dist/tools/workspace/syncFile.js +134 -0
- package/dist/utils/atomicFile.d.ts +7 -0
- package/dist/utils/atomicFile.js +48 -0
- package/dist/utils/boundedFile.d.ts +8 -0
- package/dist/utils/boundedFile.js +93 -0
- package/dist/utils/lockedJsonFile.d.ts +15 -0
- package/dist/utils/lockedJsonFile.js +224 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +26 -0
- package/dist/utils/stableJson.d.ts +2 -0
- package/dist/utils/stableJson.js +12 -0
- package/dist/version.d.ts +3 -2
- package/dist/version.js +3 -2
- package/dist/watcherStatus.d.ts +2 -2
- package/dist/watcherStatus.js +18 -0
- package/docs/CODE_WIKI.md +1648 -0
- package/docs/agentseal-integration.md +150 -0
- package/docs/architecture.md +23 -0
- package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
- package/docs/assets/patchwarden-oss-demo.gif +0 -0
- package/docs/control-center/README.md +4 -4
- package/docs/dashboard-overview.md +86 -0
- package/docs/demo.md +8 -0
- package/docs/desktop-app.md +175 -0
- package/docs/direct-session-workflow.md +98 -0
- package/docs/evidence-pack-schema.md +215 -0
- package/docs/lineage-evidence-pack-workflow.md +127 -0
- package/docs/mcp-inspector-testing.md +200 -0
- package/docs/open-source-application.md +168 -0
- package/docs/opencode-worker.md +151 -0
- package/docs/openhands-worker.md +181 -0
- package/docs/release-checklist.md +1 -1
- package/docs/release-evidence.md +82 -0
- package/docs/spec-kit-integration.md +131 -0
- package/docs/task-safe-review-workflow.md +98 -0
- package/docs/threat-model.md +97 -0
- package/docs/user-feedback.md +40 -0
- package/docs/why-patchwarden.md +110 -0
- package/examples/config.example.json +4 -2
- package/package.json +28 -11
- package/scripts/README.md +15 -6
- package/scripts/build.js +11 -0
- package/scripts/checks/brand-check.js +3 -0
- package/scripts/checks/build-output-check.js +37 -0
- package/scripts/checks/control-center-smoke.js +447 -6
- package/scripts/checks/control-smoke.js +19 -8
- package/scripts/checks/http-mcp-smoke.js +82 -4
- package/scripts/checks/lifecycle-smoke.js +39 -33
- package/scripts/checks/mcp-manifest-check.js +1 -1
- package/scripts/checks/mcp-smoke.js +30 -4
- package/scripts/checks/package-manifest-check.js +20 -7
- package/scripts/checks/unit-tests.js +3 -5
- package/scripts/checks/watcher-supervisor-smoke.js +9 -0
- package/scripts/control/get-patchwarden-health.ps1 +1 -1
- package/scripts/control/manage-patchwarden.ps1 +69 -4
- package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
- package/scripts/control/run-background-supervisor.ps1 +41 -0
- package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
- package/scripts/generate-demo-gif.py +320 -0
- package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
- package/scripts/launchers/PatchWarden-Control.cmd +6 -0
- package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
- package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
- package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
- package/scripts/lib/clean-generated-output.js +11 -0
- package/scripts/lib/file-discovery.js +12 -0
- package/scripts/release/desktop-preflight.js +211 -0
- package/scripts/release/pack-clean.js +59 -14
- package/src/assessments/agentAssessor.ts +29 -15
- package/src/assessments/assessmentStore.ts +55 -43
- package/src/assessments/confirmCli.ts +5 -4
- package/src/config.ts +105 -11
- package/src/control/fileManager.ts +40 -0
- package/src/control/middleware/auth.ts +35 -0
- package/src/control/middleware/static.ts +84 -0
- package/src/control/routeTable.ts +301 -0
- package/src/control/routes/audit.ts +329 -0
- package/src/control/routes/evidence.ts +107 -0
- package/src/control/routes/lineage.ts +92 -0
- package/src/control/routes/policy.ts +81 -0
- package/src/control/routes/process.ts +505 -0
- package/src/control/routes/sessions.ts +314 -0
- package/src/control/routes/status.ts +405 -0
- package/src/control/routes/taskActions.ts +214 -0
- package/src/control/routes/tasks.ts +331 -0
- package/src/control/routes/workspace.ts +234 -0
- package/src/control/runtime.ts +508 -0
- package/src/control/server.ts +165 -0
- package/src/control/shared.ts +360 -0
- package/src/controlCenter.ts +7 -2347
- package/src/direct/directAudit.ts +17 -9
- package/src/direct/directGuards.ts +100 -51
- package/src/direct/directPatch.ts +95 -8
- package/src/direct/directSessionStore.ts +211 -55
- package/src/direct/directVerification.ts +9 -23
- package/src/doctor.ts +766 -492
- package/src/goal/acceptanceEngine.ts +1 -1
- package/src/goal/goalProgress.ts +17 -13
- package/src/goal/goalReport.ts +263 -0
- package/src/goal/goalStatus.ts +25 -3
- package/src/goal/goalStore.ts +241 -54
- package/src/goal/handoffExport.ts +6 -6
- package/src/goal/specKitImport.ts +355 -0
- package/src/goal/subgoalSync.ts +15 -10
- package/src/goal/worktreeManager.ts +139 -22
- package/src/httpServer.ts +130 -44
- package/src/index.ts +7 -4
- package/src/logging.ts +20 -4
- package/src/policy/projectPolicy.ts +5 -2
- package/src/release/releaseGate.ts +74 -27
- package/src/runner/agentInvocation.ts +126 -4
- package/src/runner/changeCapture.ts +342 -80
- package/src/runner/cli.ts +7 -6
- package/src/runner/postTaskCleanup.ts +55 -7
- package/src/runner/processSecurity.ts +433 -0
- package/src/runner/runTask.ts +441 -292
- package/src/runner/simpleProcess.ts +68 -33
- package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
- package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
- package/src/runner/taskStatusStore.ts +54 -0
- package/src/runner/watch.ts +290 -57
- package/src/security/contentRedaction.ts +31 -2
- package/src/security/discoveryTokenStore.ts +18 -4
- package/src/security/loopbackHost.ts +19 -0
- package/src/security/pathGuard.ts +35 -16
- package/src/security/planGuard.ts +2 -1
- package/src/security/runtimeGuard.ts +9 -6
- package/src/security/secretComparison.ts +13 -0
- package/src/security/sensitiveGuard.ts +27 -8
- package/src/security/toolInvocationGuard.ts +2 -2
- package/src/security/workspaceRootGuard.ts +53 -0
- package/src/smoke-test.ts +276 -273
- package/src/tools/catalog/index.ts +4 -0
- package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
- package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
- package/src/tools/definitions/toolDefs.ts +1264 -0
- package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
- package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
- package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
- package/src/tools/diagnostics/index.ts +6 -0
- package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
- package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
- package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
- package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
- package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
- package/src/tools/direct/index.ts +3 -0
- package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
- package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
- package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
- package/src/tools/discovery/index.ts +3 -0
- package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
- package/src/tools/dispatch/coreDispatch.ts +379 -0
- package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
- package/src/tools/dispatch/directDispatch.ts +168 -0
- package/src/tools/dispatch/goalDispatch.ts +128 -0
- package/src/tools/dispatch/releaseDispatch.ts +66 -0
- package/src/tools/dispatch/types.ts +24 -0
- package/src/tools/dispatch/validation.ts +78 -0
- package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
- package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
- package/src/tools/goals/index.ts +3 -0
- package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
- package/src/tools/registry.ts +54 -1858
- package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
- package/src/tools/release/index.ts +2 -0
- package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
- package/src/tools/tasks/cancelTask.ts +109 -0
- package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
- package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
- package/src/tools/tasks/evidencePack.ts +504 -0
- package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
- package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
- package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
- package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
- package/src/tools/tasks/index.ts +18 -0
- package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
- package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
- package/src/tools/tasks/retryTask.ts +69 -0
- package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
- package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
- package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
- package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
- package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
- package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
- package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
- package/src/tools/workspace/index.ts +10 -0
- package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
- package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
- package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
- package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
- package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
- package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
- package/src/tools/workspace/syncFile.ts +292 -0
- package/src/utils/atomicFile.ts +58 -0
- package/src/utils/boundedFile.ts +116 -0
- package/src/utils/lockedJsonFile.ts +251 -0
- package/src/utils/platform.ts +31 -0
- package/src/utils/stableJson.ts +12 -0
- package/src/version.ts +3 -2
- package/src/watcherStatus.ts +19 -1
- package/ui/desktop-bootstrap.js +11 -0
- package/ui/desktop-bridge.js +36 -0
- package/ui/desktop.css +160 -0
- package/ui/getting-started.js +93 -0
- package/ui/i18n.js +484 -0
- package/ui/log-parser.js +30 -0
- package/ui/pages/audit.html +204 -3
- package/ui/pages/dashboard.html +937 -79
- package/ui/pages/direct-sessions.html +609 -60
- package/ui/pages/getting-started.html +43 -0
- package/ui/pages/logs.html +105 -22
- package/ui/pages/settings.html +101 -0
- package/ui/pages/task-detail.html +468 -438
- package/ui/pages/tasks.html +648 -65
- package/ui/pages/workspace.html +38 -1
- package/ui/settings.js +245 -0
- package/PatchWarden-Control.cmd +0 -6
- package/Restart-PatchWarden-Control.cmd +0 -6
- package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
- package/dist/test/unit/acceptance-engine.test.js +0 -228
- package/dist/test/unit/agent-router.test.d.ts +0 -1
- package/dist/test/unit/agent-router.test.js +0 -287
- package/dist/test/unit/android-doctor.test.d.ts +0 -1
- package/dist/test/unit/android-doctor.test.js +0 -118
- package/dist/test/unit/audit-checks.test.d.ts +0 -1
- package/dist/test/unit/audit-checks.test.js +0 -350
- package/dist/test/unit/chinese-path.test.d.ts +0 -1
- package/dist/test/unit/chinese-path.test.js +0 -91
- package/dist/test/unit/command-guard.test.d.ts +0 -1
- package/dist/test/unit/command-guard.test.js +0 -160
- package/dist/test/unit/diagnose-task.test.d.ts +0 -1
- package/dist/test/unit/diagnose-task.test.js +0 -457
- package/dist/test/unit/direct-guards.test.d.ts +0 -1
- package/dist/test/unit/direct-guards.test.js +0 -213
- package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
- package/dist/test/unit/discovery-token-store.test.js +0 -139
- package/dist/test/unit/evidence-pack.test.d.ts +0 -1
- package/dist/test/unit/evidence-pack.test.js +0 -130
- package/dist/test/unit/goal-graph.test.d.ts +0 -1
- package/dist/test/unit/goal-graph.test.js +0 -298
- package/dist/test/unit/goal-progress.test.d.ts +0 -1
- package/dist/test/unit/goal-progress.test.js +0 -381
- package/dist/test/unit/goal-status.test.d.ts +0 -1
- package/dist/test/unit/goal-status.test.js +0 -215
- package/dist/test/unit/goal-store.test.d.ts +0 -1
- package/dist/test/unit/goal-store.test.js +0 -253
- package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
- package/dist/test/unit/goal-subgoal-task.test.js +0 -55
- package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
- package/dist/test/unit/goal-tools-registry.test.js +0 -190
- package/dist/test/unit/handoff-export.test.d.ts +0 -1
- package/dist/test/unit/handoff-export.test.js +0 -263
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
- package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
- package/dist/test/unit/logging.test.d.ts +0 -1
- package/dist/test/unit/logging.test.js +0 -397
- package/dist/test/unit/path-guard.test.d.ts +0 -1
- package/dist/test/unit/path-guard.test.js +0 -109
- package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
- package/dist/test/unit/post-task-cleanup.test.js +0 -48
- package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
- package/dist/test/unit/project-policy-release-mode.test.js +0 -125
- package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
- package/dist/test/unit/reconcile-tasks.test.js +0 -456
- package/dist/test/unit/release-gate.test.d.ts +0 -1
- package/dist/test/unit/release-gate.test.js +0 -242
- package/dist/test/unit/run-task-loop.test.d.ts +0 -1
- package/dist/test/unit/run-task-loop.test.js +0 -380
- package/dist/test/unit/safe-status.test.d.ts +0 -1
- package/dist/test/unit/safe-status.test.js +0 -165
- package/dist/test/unit/safe-views.test.d.ts +0 -1
- package/dist/test/unit/safe-views.test.js +0 -171
- package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
- package/dist/test/unit/schema-drift-check.test.js +0 -176
- package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
- package/dist/test/unit/sensitive-guard.test.js +0 -104
- package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
- package/dist/test/unit/subgoal-sync.test.js +0 -183
- package/dist/test/unit/sync-file.test.d.ts +0 -1
- package/dist/test/unit/sync-file.test.js +0 -154
- package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
- package/dist/test/unit/tool-invocation-guard.test.js +0 -432
- package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
- package/dist/test/unit/tool-usage-stats.test.js +0 -300
- package/dist/test/unit/toolSearch.test.d.ts +0 -1
- package/dist/test/unit/toolSearch.test.js +0 -571
- package/dist/test/unit/watcher-status.test.d.ts +0 -1
- package/dist/test/unit/watcher-status.test.js +0 -169
- package/dist/test/unit/worktree-manager.test.d.ts +0 -1
- package/dist/test/unit/worktree-manager.test.js +0 -176
- package/dist/tools/applyPatch.js +0 -41
- package/dist/tools/cancelTask.d.ts +0 -30
- package/dist/tools/cancelTask.js +0 -64
- package/dist/tools/evidencePack.js +0 -168
- package/dist/tools/finalizeDirectSession.js +0 -84
- package/dist/tools/goalSubgoalTask.js +0 -110
- package/dist/tools/killTask.d.ts +0 -15
- package/dist/tools/retryTask.js +0 -45
- package/dist/tools/runVerification.js +0 -36
- package/dist/tools/syncFile.js +0 -65
- package/docs/control-center/control-center-daily-driver.md +0 -211
- package/docs/control-center/control-center-mvp.md +0 -205
- package/docs/control-center/control-center-phase2.md +0 -159
- package/docs/release-v0.6.0.md +0 -71
- package/docs/release-v0.6.1.md +0 -75
- package/docs/release-v0.6.4.md +0 -45
- package/src/test/unit/acceptance-engine.test.ts +0 -261
- package/src/test/unit/agent-router.test.ts +0 -342
- package/src/test/unit/android-doctor.test.ts +0 -158
- package/src/test/unit/audit-checks.test.ts +0 -567
- package/src/test/unit/chinese-path.test.ts +0 -106
- package/src/test/unit/command-guard.test.ts +0 -221
- package/src/test/unit/diagnose-task.test.ts +0 -544
- package/src/test/unit/direct-guards.test.ts +0 -297
- package/src/test/unit/discovery-token-store.test.ts +0 -181
- package/src/test/unit/evidence-pack.test.ts +0 -142
- package/src/test/unit/goal-graph.test.ts +0 -347
- package/src/test/unit/goal-progress.test.ts +0 -538
- package/src/test/unit/goal-status.test.ts +0 -270
- package/src/test/unit/goal-store.test.ts +0 -318
- package/src/test/unit/goal-subgoal-task.test.ts +0 -72
- package/src/test/unit/goal-tools-registry.test.ts +0 -243
- package/src/test/unit/handoff-export.test.ts +0 -295
- package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
- package/src/test/unit/logging.test.ts +0 -497
- package/src/test/unit/path-guard.test.ts +0 -150
- package/src/test/unit/post-task-cleanup.test.ts +0 -53
- package/src/test/unit/project-policy-release-mode.test.ts +0 -156
- package/src/test/unit/reconcile-tasks.test.ts +0 -551
- package/src/test/unit/release-gate.test.ts +0 -314
- package/src/test/unit/run-task-loop.test.ts +0 -425
- package/src/test/unit/safe-status.test.ts +0 -187
- package/src/test/unit/safe-views.test.ts +0 -184
- package/src/test/unit/schema-drift-check.test.ts +0 -260
- package/src/test/unit/sensitive-guard.test.ts +0 -124
- package/src/test/unit/subgoal-sync.test.ts +0 -236
- package/src/test/unit/sync-file.test.ts +0 -231
- package/src/test/unit/tool-invocation-guard.test.ts +0 -542
- package/src/test/unit/tool-usage-stats.test.ts +0 -384
- package/src/test/unit/toolSearch.test.ts +0 -756
- package/src/test/unit/watcher-status.test.ts +0 -190
- package/src/test/unit/worktree-manager.test.ts +0 -247
- package/src/tools/cancelTask.ts +0 -70
- package/src/tools/evidencePack.ts +0 -205
- package/src/tools/retryTask.ts +0 -53
- package/src/tools/syncFile.ts +0 -122
- /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
- /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
- /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
- /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
- /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
- /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
- /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
- /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
- /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
- /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
- /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
- /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
- /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
- /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
- /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
- /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
- /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
- /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
- /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
- /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
- /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
- /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
- /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
- /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
- /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
- /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
- /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
- /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
- /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
- /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
package/src/doctor.ts
CHANGED
|
@@ -10,560 +10,834 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
12
12
|
import { isAbsolute, resolve, normalize, join } from "node:path";
|
|
13
|
-
import { execSync } from "node:child_process";
|
|
14
13
|
import { createServer } from "node:net";
|
|
15
|
-
import {
|
|
14
|
+
import { getConfig, type PatchWardenConfig } from "./config.js";
|
|
16
15
|
import { guardPath, guardWorkspacePath } from "./security/pathGuard.js";
|
|
17
16
|
import { isSensitivePath } from "./security/sensitiveGuard.js";
|
|
18
17
|
import { guardPlanContent } from "./security/planGuard.js";
|
|
19
|
-
import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
|
|
18
|
+
import { TASK_READ_ONLY_FILES } from "./tools/tasks/getTaskFile.js";
|
|
20
19
|
import { getToolDefs } from "./tools/registry.js";
|
|
21
|
-
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
22
|
-
import { buildToolRegistry } from "./tools/toolRegistry.js";
|
|
23
|
-
import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
|
|
20
|
+
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/catalog/toolCatalog.js";
|
|
21
|
+
import { buildToolRegistry } from "./tools/catalog/toolRegistry.js";
|
|
22
|
+
import { runAllSchemaDriftChecks } from "./tools/diagnostics/schemaDriftCheck.js";
|
|
24
23
|
import { runReleaseGateCheck } from "./release/releaseGate.js";
|
|
25
24
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
25
|
+
import { logger } from "./logging.js";
|
|
26
|
+
import { unsafeWorkspaceRootLabel } from "./security/workspaceRootGuard.js";
|
|
27
|
+
import { runSimpleProcessSync } from "./runner/simpleProcess.js";
|
|
28
|
+
import { resolveAgentLaunch } from "./runner/agentInvocation.js";
|
|
26
29
|
|
|
27
|
-
// ──
|
|
30
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
let warn = 0;
|
|
31
|
-
let fail = 0;
|
|
32
|
-
const results: string[] = [];
|
|
32
|
+
export type DoctorCheckLevel = "OK" | "WARN" | "FAIL";
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} else {
|
|
39
|
-
results.push(`[FAIL] ${name}${detail ? " — " + detail : ""}`);
|
|
40
|
-
fail++;
|
|
41
|
-
}
|
|
34
|
+
export interface DoctorCheckResult {
|
|
35
|
+
level: DoctorCheckLevel;
|
|
36
|
+
message: string;
|
|
37
|
+
details?: string;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} else {
|
|
49
|
-
results.push(`[WARN] ${name}${detail ? " — " + detail : ""}`);
|
|
50
|
-
warn++;
|
|
51
|
-
}
|
|
40
|
+
export interface DoctorCheck {
|
|
41
|
+
id: string;
|
|
42
|
+
description: string;
|
|
43
|
+
run(context: DoctorContext): DoctorCheckResult[] | Promise<DoctorCheckResult[]>;
|
|
52
44
|
}
|
|
53
45
|
|
|
54
|
-
|
|
46
|
+
export interface DoctorContext {
|
|
47
|
+
config: PatchWardenConfig | null;
|
|
48
|
+
configError: string | null;
|
|
49
|
+
allowDefaultConfig: boolean;
|
|
50
|
+
configPathUsed: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function cmd(command: string, args: readonly string[] = []): string {
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
const result = runSimpleProcessSync({
|
|
58
|
+
command,
|
|
59
|
+
args: [...args],
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
timeoutMs: 5000,
|
|
62
|
+
maxStdoutBytes: 64 * 1024,
|
|
63
|
+
maxStderrBytes: 64 * 1024,
|
|
64
|
+
});
|
|
65
|
+
if (result.exitCode !== 0) return "";
|
|
66
|
+
return `${result.stdout}${result.stderr}`.trim();
|
|
61
67
|
} catch {
|
|
62
68
|
return "";
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
async function main() {
|
|
69
|
-
|
|
70
|
-
console.log("PatchWarden Doctor\n");
|
|
71
|
-
const allowDefaultConfig = process.argv.includes("--allow-default-config");
|
|
72
|
-
|
|
73
|
-
// 1. Node version
|
|
74
|
-
const nodeVer = process.version;
|
|
75
|
-
const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
|
|
76
|
-
check("Node.js version", nodeMajor >= 18,
|
|
77
|
-
nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`);
|
|
78
|
-
|
|
79
|
-
// 2. npm
|
|
80
|
-
const npmVer = cmd("npm --version");
|
|
81
|
-
check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
|
|
82
|
-
|
|
83
|
-
// Local-only medium-risk confirmation entrypoint. It must exist as a package
|
|
84
|
-
// binary but must not be exposed as an MCP tool.
|
|
85
|
-
try {
|
|
86
|
-
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
87
|
-
check(
|
|
88
|
-
"patchwarden-confirm package binary",
|
|
89
|
-
packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js",
|
|
90
|
-
packageJson.bin?.["patchwarden-confirm"] || "missing"
|
|
91
|
-
);
|
|
92
|
-
check(
|
|
93
|
-
"patchwarden-confirm compiled entrypoint",
|
|
94
|
-
existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")),
|
|
95
|
-
"run npm.cmd run build"
|
|
96
|
-
);
|
|
97
|
-
} catch (error) {
|
|
98
|
-
check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
|
|
72
|
+
function okResult(message: string, details?: string): DoctorCheckResult {
|
|
73
|
+
return { level: "OK", message, details };
|
|
99
74
|
}
|
|
100
75
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
warnCheck("Git available", gitVer !== "",
|
|
104
|
-
gitVer || "git not found — runner git.diff will not work");
|
|
105
|
-
|
|
106
|
-
// 4. Config file exists
|
|
107
|
-
const configPaths = [
|
|
108
|
-
resolve(process.cwd(), "patchwarden.config.json"),
|
|
109
|
-
process.env.PATCHWARDEN_CONFIG || "",
|
|
110
|
-
].filter(Boolean);
|
|
111
|
-
|
|
112
|
-
let configPathUsed = "";
|
|
113
|
-
for (const p of configPaths) {
|
|
114
|
-
if (existsSync(p)) { configPathUsed = p; break; }
|
|
76
|
+
function warnResult(message: string, details?: string): DoctorCheckResult {
|
|
77
|
+
return { level: "WARN", message, details };
|
|
115
78
|
}
|
|
116
79
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
: 'Create one: cp examples/config.example.json patchwarden.config.json';
|
|
120
|
-
if (allowDefaultConfig) {
|
|
121
|
-
warnCheck("Config file exists", configPathUsed !== "", configDetail);
|
|
122
|
-
} else {
|
|
123
|
-
check("Config file exists", configPathUsed !== "", configDetail);
|
|
80
|
+
function failResult(message: string, details?: string): DoctorCheckResult {
|
|
81
|
+
return { level: "FAIL", message, details };
|
|
124
82
|
}
|
|
125
83
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
ok++;
|
|
130
|
-
} else {
|
|
131
|
-
results.push(`[OK] PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)`);
|
|
132
|
-
ok++;
|
|
84
|
+
/** Mirrors original check(): OK if condition true, FAIL otherwise. */
|
|
85
|
+
function checkResult(name: string, condition: boolean, detail?: string): DoctorCheckResult {
|
|
86
|
+
return condition ? okResult(name, detail) : failResult(name, detail);
|
|
133
87
|
}
|
|
134
88
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
config = getConfig();
|
|
139
|
-
check("Config parseable", true, `workspaceRoot: ${config.workspaceRoot}`);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
check("Config parseable", false, err instanceof Error ? err.message : String(err));
|
|
89
|
+
/** Mirrors original warnCheck(): OK if condition true, WARN otherwise. */
|
|
90
|
+
function warnCheckResult(name: string, condition: boolean, detail?: string): DoctorCheckResult {
|
|
91
|
+
return condition ? okResult(name, detail) : warnResult(name, detail);
|
|
142
92
|
}
|
|
143
93
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
check("workspaceRoot exists", exists, ws);
|
|
149
|
-
|
|
150
|
-
let isDir = false;
|
|
151
|
-
try { isDir = statSync(ws).isDirectory(); } catch {}
|
|
152
|
-
check("workspaceRoot is directory", isDir, ws);
|
|
153
|
-
|
|
154
|
-
// Danger checks
|
|
155
|
-
const dangerousRoots = [
|
|
156
|
-
{ pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
|
|
157
|
-
{ pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
|
|
158
|
-
{ pattern: /\\Desktop$/, label: "Desktop" },
|
|
159
|
-
{ pattern: /\\Downloads$/, label: "Downloads" },
|
|
160
|
-
{ pattern: /\\Documents$/, label: "Documents" },
|
|
161
|
-
];
|
|
162
|
-
|
|
163
|
-
for (const { pattern, label } of dangerousRoots) {
|
|
164
|
-
const matches = pattern.test(ws);
|
|
165
|
-
if (matches) {
|
|
166
|
-
results.push(`[WARN] workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`);
|
|
167
|
-
warn++;
|
|
168
|
-
}
|
|
94
|
+
function formatResult(result: DoctorCheckResult): string {
|
|
95
|
+
const tag = result.level === "OK" ? "[OK] " : result.level === "WARN" ? "[WARN] " : "[FAIL] ";
|
|
96
|
+
if (result.level !== "OK" && result.details) {
|
|
97
|
+
return `${tag}${result.message} — ${result.details}`;
|
|
169
98
|
}
|
|
99
|
+
return `${tag}${result.message}`;
|
|
170
100
|
}
|
|
171
101
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const relativeRepo = guardWorkspacePath(".", config.workspaceRoot);
|
|
194
|
-
const absoluteRepo = guardWorkspacePath(config.workspaceRoot, config.workspaceRoot);
|
|
195
|
-
check("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo);
|
|
196
|
-
} catch (error) {
|
|
197
|
-
check("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error));
|
|
198
|
-
}
|
|
199
|
-
}
|
|
102
|
+
// ── Checks ─────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const checkNodeVersion: DoctorCheck = {
|
|
105
|
+
id: "node-version",
|
|
106
|
+
description: "Node.js version",
|
|
107
|
+
run() {
|
|
108
|
+
const nodeVer = process.version;
|
|
109
|
+
const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
|
|
110
|
+
const detail = nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`;
|
|
111
|
+
return [checkResult("Node.js version", nodeMajor >= 18, detail)];
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const checkNpmAvailable: DoctorCheck = {
|
|
116
|
+
id: "npm-available",
|
|
117
|
+
description: "npm available",
|
|
118
|
+
run() {
|
|
119
|
+
const npmVer = cmd(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"]);
|
|
120
|
+
return [checkResult("npm available", npmVer !== "", npmVer || "npm not found in PATH")];
|
|
121
|
+
},
|
|
122
|
+
};
|
|
200
123
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
124
|
+
// Local-only medium-risk confirmation entrypoint. It must exist as a package
|
|
125
|
+
// binary but must not be exposed as an MCP tool.
|
|
126
|
+
const checkConfirmPackage: DoctorCheck = {
|
|
127
|
+
id: "confirm-package",
|
|
128
|
+
description: "patchwarden-confirm package",
|
|
129
|
+
run() {
|
|
130
|
+
try {
|
|
131
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
132
|
+
return [
|
|
133
|
+
checkResult(
|
|
134
|
+
"patchwarden-confirm package binary",
|
|
135
|
+
packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js",
|
|
136
|
+
packageJson.bin?.["patchwarden-confirm"] || "missing",
|
|
137
|
+
),
|
|
138
|
+
checkResult(
|
|
139
|
+
"patchwarden-confirm compiled entrypoint",
|
|
140
|
+
existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")),
|
|
141
|
+
"run npm.cmd run build",
|
|
142
|
+
),
|
|
143
|
+
];
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return [checkResult("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error))];
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const checkGitAvailable: DoctorCheck = {
|
|
151
|
+
id: "git-available",
|
|
152
|
+
description: "Git available",
|
|
153
|
+
run() {
|
|
154
|
+
const gitVer = cmd("git", ["--version"]);
|
|
155
|
+
return [warnCheckResult("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work")];
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const checkConfigFile: DoctorCheck = {
|
|
160
|
+
id: "config-file",
|
|
161
|
+
description: "Config file exists",
|
|
162
|
+
run(context) {
|
|
163
|
+
const configDetail = context.configPathUsed
|
|
164
|
+
? context.configPathUsed
|
|
165
|
+
: 'Create one: cp examples/config.example.json patchwarden.config.json';
|
|
166
|
+
if (context.allowDefaultConfig) {
|
|
167
|
+
return [warnCheckResult("Config file exists", context.configPathUsed !== "", configDetail)];
|
|
168
|
+
}
|
|
169
|
+
return [checkResult("Config file exists", context.configPathUsed !== "", configDetail)];
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const checkPatchwardenConfigEnv: DoctorCheck = {
|
|
174
|
+
id: "patchwarden-config-env",
|
|
175
|
+
description: "PATCHWARDEN_CONFIG env",
|
|
176
|
+
run() {
|
|
177
|
+
if (process.env.PATCHWARDEN_CONFIG) {
|
|
178
|
+
return [okResult(`PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`)];
|
|
179
|
+
}
|
|
180
|
+
return [okResult("PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)")];
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const checkConfigParseable: DoctorCheck = {
|
|
185
|
+
id: "config-parseable",
|
|
186
|
+
description: "Config parseable",
|
|
187
|
+
run(context) {
|
|
188
|
+
if (context.config) {
|
|
189
|
+
return [checkResult("Config parseable", true, `workspaceRoot: ${context.config.workspaceRoot}`)];
|
|
190
|
+
}
|
|
191
|
+
return [checkResult("Config parseable", false, context.configError || "unknown error")];
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const checkWorkspaceRoot: DoctorCheck = {
|
|
196
|
+
id: "workspace-root",
|
|
197
|
+
description: "workspaceRoot checks",
|
|
198
|
+
run(context) {
|
|
199
|
+
if (!context.config) return [];
|
|
200
|
+
const results: DoctorCheckResult[] = [];
|
|
201
|
+
const ws = normalize(resolve(context.config.workspaceRoot));
|
|
202
|
+
const exists = existsSync(ws);
|
|
203
|
+
results.push(checkResult("workspaceRoot exists", exists, ws));
|
|
204
|
+
|
|
205
|
+
let isDir = false;
|
|
206
|
+
try { isDir = statSync(ws).isDirectory(); } catch {}
|
|
207
|
+
results.push(checkResult("workspaceRoot is directory", isDir, ws));
|
|
208
|
+
|
|
209
|
+
const unsafeLabel = unsafeWorkspaceRootLabel(ws);
|
|
210
|
+
if (unsafeLabel) {
|
|
211
|
+
results.push(warnResult(`workspaceRoot is ${unsafeLabel}: ${ws} — consider narrowing to a project directory`));
|
|
212
|
+
}
|
|
213
|
+
return results;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const checkPathGuard: DoctorCheck = {
|
|
218
|
+
id: "path-guard",
|
|
219
|
+
description: "Path guard test",
|
|
220
|
+
run(context) {
|
|
221
|
+
if (!context.config) return [];
|
|
222
|
+
const results: DoctorCheckResult[] = [];
|
|
223
|
+
try {
|
|
224
|
+
guardPath("test-file.txt", context.config.workspaceRoot);
|
|
225
|
+
results.push(okResult("pathGuard allows workspace-internal path"));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
results.push(failResult(`pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`));
|
|
228
|
+
}
|
|
213
229
|
|
|
214
|
-
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
fail++;
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
|
|
224
|
-
results.push("[FAIL] save_plan security rule did not block credential access");
|
|
225
|
-
fail++;
|
|
226
|
-
} catch {
|
|
227
|
-
results.push("[OK] save_plan security rules loaded");
|
|
228
|
-
ok++;
|
|
229
|
-
}
|
|
230
|
+
try {
|
|
231
|
+
guardPath("../outside", context.config.workspaceRoot);
|
|
232
|
+
results.push(failResult("pathGuard should have blocked ../escape"));
|
|
233
|
+
} catch {
|
|
234
|
+
results.push(okResult("pathGuard blocks ../ path escape"));
|
|
235
|
+
}
|
|
230
236
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
237
|
+
try {
|
|
238
|
+
const relativeRepo = guardWorkspacePath(".", context.config.workspaceRoot);
|
|
239
|
+
const absoluteRepo = guardWorkspacePath(context.config.workspaceRoot, context.config.workspaceRoot);
|
|
240
|
+
results.push(checkResult("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
results.push(checkResult("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error)));
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const checkSensitiveGuard: DoctorCheck = {
|
|
249
|
+
id: "sensitive-guard",
|
|
250
|
+
description: "Sensitive file guard test",
|
|
251
|
+
run() {
|
|
252
|
+
const results: DoctorCheckResult[] = [];
|
|
253
|
+
const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
|
|
254
|
+
for (const sp of sensitivePaths) {
|
|
255
|
+
const blocked = isSensitivePath(sp);
|
|
256
|
+
if (blocked) {
|
|
257
|
+
results.push(okResult(`sensitiveGuard blocks "${sp}"`));
|
|
258
|
+
} else {
|
|
259
|
+
results.push(failResult(`sensitiveGuard does NOT block "${sp}"`));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return results;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const checkSavePlanRules: DoctorCheck = {
|
|
267
|
+
id: "save-plan-rules",
|
|
268
|
+
description: "save_plan guard test",
|
|
269
|
+
run() {
|
|
270
|
+
const results: DoctorCheckResult[] = [];
|
|
271
|
+
try {
|
|
272
|
+
guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
|
|
273
|
+
results.push(okResult("save_plan allows normal development plans"));
|
|
274
|
+
} catch {
|
|
275
|
+
results.push(failResult("save_plan incorrectly blocks a normal development plan"));
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
|
|
279
|
+
results.push(failResult("save_plan security rule did not block credential access"));
|
|
280
|
+
} catch {
|
|
281
|
+
results.push(okResult("save_plan security rules loaded"));
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const checkTaskArtifactAllowlist: DoctorCheck = {
|
|
288
|
+
id: "task-artifact-allowlist",
|
|
289
|
+
description: "Read-only task artifact allowlist",
|
|
290
|
+
run() {
|
|
291
|
+
const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
|
|
292
|
+
return [
|
|
293
|
+
checkResult(
|
|
294
|
+
"Read-only task artifact allowlist",
|
|
295
|
+
requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)),
|
|
296
|
+
requiredReadOnlyFiles.join(", "),
|
|
297
|
+
),
|
|
298
|
+
];
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const checkServerVersion: DoctorCheck = {
|
|
303
|
+
id: "server-version",
|
|
304
|
+
description: "Server version and manifest preflight",
|
|
305
|
+
run() {
|
|
306
|
+
const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
|
307
|
+
return [
|
|
308
|
+
checkResult(
|
|
309
|
+
"Server version matches package.json",
|
|
310
|
+
packageJson.version === PATCHWARDEN_VERSION,
|
|
311
|
+
`${PATCHWARDEN_VERSION} vs ${packageJson.version}`,
|
|
312
|
+
),
|
|
313
|
+
process.env.PATCHWARDEN_DESKTOP_RUNTIME === "1"
|
|
314
|
+
? okResult("Manifest preflight covered by compiled desktop runtime checks")
|
|
315
|
+
: checkResult(
|
|
316
|
+
"Manifest preflight script exists",
|
|
317
|
+
existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")),
|
|
318
|
+
"scripts/checks/mcp-manifest-check.js",
|
|
319
|
+
),
|
|
320
|
+
];
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const checkToolProfiles: DoctorCheck = {
|
|
325
|
+
id: "tool-profiles",
|
|
326
|
+
description: "Tool profile and schema drift checks",
|
|
327
|
+
run(context) {
|
|
328
|
+
const results: DoctorCheckResult[] = [];
|
|
329
|
+
const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
330
|
+
try {
|
|
331
|
+
process.env.PATCHWARDEN_TOOL_PROFILE = "full";
|
|
332
|
+
const fullTools = getToolDefs();
|
|
333
|
+
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", context.config?.enableDirectProfile);
|
|
334
|
+
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
|
|
335
|
+
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
|
|
336
|
+
results.push(checkResult("Full tool profile exposes 66 tools", fullTools.length === 66, `${fullTools.length} tools`));
|
|
337
|
+
results.push(
|
|
338
|
+
checkResult(
|
|
339
|
+
`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`,
|
|
340
|
+
JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES),
|
|
341
|
+
coreTools.map((tool) => tool.name).join(", "),
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
results.push(
|
|
345
|
+
checkResult(
|
|
346
|
+
"Core task schemas expose inline_plan, verify_commands, and wait aliases",
|
|
347
|
+
Boolean(
|
|
348
|
+
createSchema?.properties?.inline_plan &&
|
|
349
|
+
createSchema?.properties?.verify_commands &&
|
|
350
|
+
waitSchema?.properties?.timeout_seconds &&
|
|
351
|
+
waitSchema?.properties?.wait_seconds,
|
|
352
|
+
),
|
|
353
|
+
),
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// chatgpt_direct profile checks
|
|
357
|
+
const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
|
|
358
|
+
results.push(
|
|
359
|
+
checkResult(
|
|
360
|
+
"chatgpt_direct disabled exposes only health_check (degraded mode)",
|
|
361
|
+
directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check",
|
|
362
|
+
`${directDisabledTools.map((t) => t.name).join(", ")}`,
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (context.config?.enableDirectProfile) {
|
|
367
|
+
const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
|
|
368
|
+
results.push(
|
|
369
|
+
checkResult(
|
|
370
|
+
`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`,
|
|
371
|
+
JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES),
|
|
372
|
+
directEnabledTools.map((tool) => tool.name).join(", "),
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
} else {
|
|
376
|
+
results.push(okResult("chatgpt_direct enabled check skipped (enableDirectProfile=false)"));
|
|
377
|
+
}
|
|
286
378
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
379
|
+
// Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
|
|
380
|
+
const driftRegistry = buildToolRegistry(fullTools);
|
|
381
|
+
const driftToolDefs = new Map<string, { inputSchema: unknown }>();
|
|
382
|
+
for (const tool of fullTools) {
|
|
383
|
+
driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
|
|
384
|
+
}
|
|
385
|
+
const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
|
|
386
|
+
if (driftResult.ok) {
|
|
387
|
+
results.push(okResult("Schema drift check — no drift detected"));
|
|
388
|
+
} else {
|
|
389
|
+
for (const w of driftResult.warnings) {
|
|
390
|
+
results.push(warnResult(`Schema drift: ${w}`));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} finally {
|
|
394
|
+
if (previousProfile === undefined) delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
395
|
+
else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
|
|
303
396
|
}
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
|
|
308
|
-
}
|
|
397
|
+
return results;
|
|
398
|
+
},
|
|
399
|
+
};
|
|
309
400
|
|
|
310
|
-
//
|
|
401
|
+
// Release gate module loadable (v1.0.0) — module integrity only.
|
|
311
402
|
// Does NOT execute local_ready (would recurse into doctor:ci).
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
403
|
+
const checkReleaseGate: DoctorCheck = {
|
|
404
|
+
id: "release-gate",
|
|
405
|
+
description: "Release gate module loadable",
|
|
406
|
+
run() {
|
|
407
|
+
try {
|
|
408
|
+
const releaseGateReady = typeof runReleaseGateCheck === "function";
|
|
409
|
+
return [
|
|
410
|
+
checkResult(
|
|
411
|
+
"Release gate module loadable",
|
|
412
|
+
releaseGateReady,
|
|
413
|
+
releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid",
|
|
414
|
+
),
|
|
415
|
+
];
|
|
416
|
+
} catch (error) {
|
|
417
|
+
return [checkResult("Release gate module loadable", false, error instanceof Error ? error.message : String(error))];
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const checkHttpPort: DoctorCheck = {
|
|
423
|
+
id: "http-port",
|
|
424
|
+
description: "HTTP port check",
|
|
425
|
+
async run(context) {
|
|
426
|
+
const httpPort = context.config?.http?.port || context.config?.httpPort || 7331;
|
|
427
|
+
try {
|
|
428
|
+
const server = createServer();
|
|
429
|
+
await new Promise<void>((resolvePort, rejectPort) => {
|
|
430
|
+
server.once("error", rejectPort);
|
|
431
|
+
server.listen(httpPort, "127.0.0.1", () => {
|
|
432
|
+
server.close();
|
|
433
|
+
resolvePort();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
return [okResult(`HTTP port ${httpPort} is free`)];
|
|
437
|
+
} catch {
|
|
438
|
+
return [warnResult(`HTTP port ${httpPort} is in use — change http.port in config`)];
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const checkDistFiles: DoctorCheck = {
|
|
444
|
+
id: "dist-files",
|
|
445
|
+
description: "dist file checks",
|
|
446
|
+
run() {
|
|
447
|
+
const distChecks = [
|
|
448
|
+
{ file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
|
|
449
|
+
{ file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
|
|
450
|
+
{ file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
|
|
451
|
+
];
|
|
452
|
+
const results: DoctorCheckResult[] = [];
|
|
453
|
+
for (const { file, label, cmd: buildCmd } of distChecks) {
|
|
454
|
+
const exists = existsSync(resolve(process.cwd(), file));
|
|
455
|
+
results.push(checkResult(`${label} exists`, exists, exists ? file : `Missing — run: ${buildCmd}`));
|
|
456
|
+
}
|
|
457
|
+
return results;
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const checkToolModules: DoctorCheck = {
|
|
462
|
+
id: "tool-modules",
|
|
463
|
+
description: "Tool module checks",
|
|
464
|
+
run() {
|
|
465
|
+
const newTools = [
|
|
466
|
+
{ name: "listTasks", subdir: "tasks" },
|
|
467
|
+
{ name: "listAgents", subdir: "workspace" },
|
|
468
|
+
{ name: "cancelTask", subdir: "tasks" },
|
|
469
|
+
{ name: "killTask", subdir: "tasks" },
|
|
470
|
+
{ name: "retryTask", subdir: "tasks" },
|
|
471
|
+
{ name: "getTaskProgress", subdir: "tasks" },
|
|
472
|
+
{ name: "getTaskSummary", subdir: "tasks" },
|
|
473
|
+
{ name: "waitForTask", subdir: "tasks" },
|
|
474
|
+
{ name: "getTaskStdoutTail", subdir: "tasks" },
|
|
475
|
+
{ name: "healthCheck", subdir: "diagnostics" },
|
|
476
|
+
{ name: "auditTask", subdir: "diagnostics" },
|
|
477
|
+
];
|
|
478
|
+
const results: DoctorCheckResult[] = [];
|
|
479
|
+
for (const t of newTools) {
|
|
480
|
+
const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
|
|
481
|
+
const exists = existsSync(compiled);
|
|
482
|
+
results.push(checkResult(`Tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
|
|
483
|
+
}
|
|
484
|
+
return results;
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const checkDirectToolModules: DoctorCheck = {
|
|
489
|
+
id: "direct-tool-modules",
|
|
490
|
+
description: "Direct tool module checks",
|
|
491
|
+
run() {
|
|
492
|
+
const directToolModules = [
|
|
493
|
+
{ name: "createDirectSession", subdir: "direct" },
|
|
494
|
+
{ name: "searchWorkspace", subdir: "workspace" },
|
|
495
|
+
{ name: "applyPatch", subdir: "workspace" },
|
|
496
|
+
{ name: "runVerification", subdir: "tasks" },
|
|
497
|
+
{ name: "finalizeDirectSession", subdir: "direct" },
|
|
498
|
+
{ name: "auditSession", subdir: "diagnostics" },
|
|
499
|
+
];
|
|
500
|
+
const results: DoctorCheckResult[] = [];
|
|
501
|
+
for (const t of directToolModules) {
|
|
502
|
+
const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
|
|
503
|
+
const exists = existsSync(compiled);
|
|
504
|
+
results.push(checkResult(`Direct tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
|
|
505
|
+
}
|
|
506
|
+
return results;
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
const checkDirectSupportModules: DoctorCheck = {
|
|
511
|
+
id: "direct-support-modules",
|
|
512
|
+
description: "Direct support module checks",
|
|
513
|
+
run() {
|
|
514
|
+
const directSupportModules = [
|
|
515
|
+
"directSessionStore",
|
|
516
|
+
"directGuards",
|
|
517
|
+
"directPatch",
|
|
518
|
+
"directVerification",
|
|
519
|
+
"directAudit",
|
|
520
|
+
];
|
|
521
|
+
const results: DoctorCheckResult[] = [];
|
|
522
|
+
for (const t of directSupportModules) {
|
|
523
|
+
const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
|
|
524
|
+
const exists = existsSync(compiled);
|
|
525
|
+
results.push(checkResult(`Direct support module: ${t}`, exists, exists ? "compiled" : "missing"));
|
|
526
|
+
}
|
|
527
|
+
return results;
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const checkTaskDirectoryWritable: DoctorCheck = {
|
|
532
|
+
id: "task-directory-writable",
|
|
533
|
+
description: "Task directory and workspaceRoot writable",
|
|
534
|
+
run(context) {
|
|
535
|
+
if (!context.config) return [];
|
|
536
|
+
const results: DoctorCheckResult[] = [];
|
|
537
|
+
const tasksDir = resolve(context.config.workspaceRoot, context.config.tasksDir);
|
|
538
|
+
try {
|
|
539
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
540
|
+
const testFile = join(tasksDir, ".doctor-write-test");
|
|
541
|
+
writeFileSync(testFile, "ok", "utf-8");
|
|
542
|
+
rmSync(testFile);
|
|
543
|
+
results.push(checkResult("Task directory writable", true, tasksDir));
|
|
544
|
+
|
|
545
|
+
const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
|
|
546
|
+
mkdirSync(sampleTaskDir, { recursive: true });
|
|
547
|
+
const sampleStatus = join(sampleTaskDir, "status.json");
|
|
548
|
+
writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
|
|
549
|
+
const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
|
|
550
|
+
rmSync(sampleTaskDir, { recursive: true, force: true });
|
|
551
|
+
results.push(checkResult("Example task directory read/write", sampleReadable, sampleTaskDir));
|
|
552
|
+
} catch {
|
|
553
|
+
results.push(warnCheckResult("Task directory writable", false, tasksDir));
|
|
554
|
+
}
|
|
340
555
|
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
556
|
+
// workspaceRoot writable
|
|
557
|
+
try {
|
|
558
|
+
const testFile = resolve(context.config.workspaceRoot, ".doctor-write-test");
|
|
559
|
+
writeFileSync(testFile, "ok", "utf-8");
|
|
560
|
+
rmSync(testFile);
|
|
561
|
+
results.push(checkResult("workspaceRoot writable", true, context.config.workspaceRoot));
|
|
562
|
+
} catch {
|
|
563
|
+
results.push(warnCheckResult("workspaceRoot writable", false, context.config.workspaceRoot));
|
|
564
|
+
}
|
|
565
|
+
return results;
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const checkWatcherStaleThreshold: DoctorCheck = {
|
|
570
|
+
id: "watcher-stale-threshold",
|
|
571
|
+
description: "Watcher stale threshold is valid",
|
|
572
|
+
run(context) {
|
|
573
|
+
if (!context.config) return [];
|
|
574
|
+
return [
|
|
575
|
+
checkResult(
|
|
576
|
+
"Watcher stale threshold is valid",
|
|
577
|
+
context.config.watcherStaleSeconds >= 5 && context.config.watcherStaleSeconds <= 3600,
|
|
578
|
+
`${context.config.watcherStaleSeconds}s`,
|
|
579
|
+
),
|
|
580
|
+
];
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const checkAllowedTestCommandsIncludesNpmTest: DoctorCheck = {
|
|
585
|
+
id: "allowed-test-commands-npm-test",
|
|
586
|
+
description: "allowedTestCommands includes npm test",
|
|
587
|
+
run(context) {
|
|
588
|
+
if (!context.config) return [];
|
|
589
|
+
const hasNpmTest = context.config.allowedTestCommands.some((c: string) => c === "npm test" || c === "npm run test");
|
|
590
|
+
return [warnCheckResult("allowedTestCommands includes npm test", hasNpmTest, hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands")];
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const checkTaskTimeoutDefaults: DoctorCheck = {
|
|
595
|
+
id: "task-timeout-defaults",
|
|
596
|
+
description: "Task timeout defaults are valid",
|
|
597
|
+
run(context) {
|
|
598
|
+
if (!context.config) return [];
|
|
599
|
+
return [
|
|
600
|
+
checkResult(
|
|
601
|
+
"Task timeout defaults are valid",
|
|
602
|
+
context.config.defaultTaskTimeoutSeconds > 0 && context.config.defaultTaskTimeoutSeconds <= context.config.maxTaskTimeoutSeconds,
|
|
603
|
+
`default ${context.config.defaultTaskTimeoutSeconds}s, max ${context.config.maxTaskTimeoutSeconds}s`,
|
|
604
|
+
),
|
|
605
|
+
];
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const checkAgentCommands: DoctorCheck = {
|
|
610
|
+
id: "agent-commands",
|
|
611
|
+
description: "Agent command check",
|
|
612
|
+
run(context) {
|
|
613
|
+
if (!context.config) return [];
|
|
614
|
+
const results: DoctorCheckResult[] = [];
|
|
615
|
+
for (const [name, agentCfg] of Object.entries(context.config.agents)) {
|
|
616
|
+
const cmdName = agentCfg.command;
|
|
617
|
+
const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
|
|
618
|
+
if (looksLikePath) {
|
|
619
|
+
const agentExists = existsSync(cmdName);
|
|
620
|
+
results.push(warnCheckResult(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`));
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
// Use an argumentized lookup utility; never interpolate config into a shell.
|
|
624
|
+
const isWin = process.platform === "win32";
|
|
625
|
+
let found = "";
|
|
626
|
+
if (isWin) {
|
|
627
|
+
try {
|
|
628
|
+
found = resolveAgentLaunch(
|
|
629
|
+
name,
|
|
630
|
+
cmdName,
|
|
631
|
+
process.platform,
|
|
632
|
+
process.env.PATH || "",
|
|
633
|
+
existsSync,
|
|
634
|
+
context.config.workspaceRoot,
|
|
635
|
+
typeof agentCfg.adapter === "string" ? agentCfg.adapter : name,
|
|
636
|
+
).command;
|
|
637
|
+
} catch {
|
|
638
|
+
found = "";
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
found = cmd("which", [cmdName]);
|
|
642
|
+
}
|
|
643
|
+
results.push(warnCheckResult(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`));
|
|
644
|
+
}
|
|
645
|
+
return results;
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const checkAllowedTestCommandsSafety: DoctorCheck = {
|
|
650
|
+
id: "allowed-test-commands-safety",
|
|
651
|
+
description: "allowedTestCommands safety check",
|
|
652
|
+
run(context) {
|
|
653
|
+
if (!context.config) return [];
|
|
654
|
+
const results: DoctorCheckResult[] = [];
|
|
655
|
+
const testCmds = context.config.allowedTestCommands || [];
|
|
656
|
+
results.push(checkResult("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured"));
|
|
657
|
+
|
|
658
|
+
const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
|
|
659
|
+
for (const cmdStr of testCmds) {
|
|
660
|
+
for (const danger of dangerous) {
|
|
661
|
+
if (cmdStr.toLowerCase().includes(danger)) {
|
|
662
|
+
results.push(warnResult(`allowedTestCommands contains dangerous pattern: "${cmdStr}"`));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return results;
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// Direct profile config checks
|
|
671
|
+
const checkDirectProfileConfig: DoctorCheck = {
|
|
672
|
+
id: "direct-profile-config",
|
|
673
|
+
description: "Direct profile config checks",
|
|
674
|
+
run(context) {
|
|
675
|
+
if (!context.config) return [];
|
|
676
|
+
const results: DoctorCheckResult[] = [];
|
|
677
|
+
const directCmds = context.config.directAllowedCommands || [];
|
|
678
|
+
results.push(warnCheckResult("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured"));
|
|
679
|
+
|
|
680
|
+
// npm run doctor should NOT be in default Direct whitelist
|
|
681
|
+
const hasDoctor = directCmds.some((c: string) => c === "npm run doctor");
|
|
682
|
+
results.push(checkResult("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present"));
|
|
683
|
+
|
|
684
|
+
results.push(checkResult("directSessionTtlSeconds is valid", context.config.directSessionTtlSeconds >= 60 && context.config.directSessionTtlSeconds <= 86400, `${context.config.directSessionTtlSeconds}s`));
|
|
685
|
+
|
|
686
|
+
results.push(checkResult("directMaxPatchBytes is positive", context.config.directMaxPatchBytes > 0, `${context.config.directMaxPatchBytes}`));
|
|
687
|
+
|
|
688
|
+
results.push(checkResult("directMaxFileBytes is positive", context.config.directMaxFileBytes > 0, `${context.config.directMaxFileBytes}`));
|
|
689
|
+
return results;
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Tunnel example files check
|
|
694
|
+
const checkTunnelExamples: DoctorCheck = {
|
|
695
|
+
id: "tunnel-examples",
|
|
696
|
+
description: "Tunnel example files check",
|
|
697
|
+
run() {
|
|
698
|
+
const tunnelFiles = [
|
|
699
|
+
"examples/openai-tunnel/README.md",
|
|
700
|
+
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
701
|
+
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
702
|
+
"scripts/mcp/patchwarden-mcp-direct.cmd",
|
|
703
|
+
"PatchWarden.cmd",
|
|
704
|
+
"scripts/control/manage-patchwarden.ps1",
|
|
705
|
+
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
706
|
+
];
|
|
707
|
+
const results: DoctorCheckResult[] = [];
|
|
708
|
+
for (const tf of tunnelFiles) {
|
|
709
|
+
const full = resolve(process.cwd(), tf);
|
|
710
|
+
const exists = existsSync(full);
|
|
711
|
+
results.push(checkResult(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing"));
|
|
712
|
+
|
|
713
|
+
// Check for leaked secrets in example files
|
|
714
|
+
if (exists) {
|
|
715
|
+
const content = readFileSync(full, "utf-8");
|
|
716
|
+
// Only flag actual key-value assignments, not comments/mentions
|
|
717
|
+
const leaked = /(?:api_key|sk-[a-zA-Z0-9]{10,}|token\s*[:=]\s*\S{4,}|secret\s*[:=]\s*\S{4,}|password\s*[:=]\s*\S{4,})/gi.test(
|
|
718
|
+
// Strip comment lines first
|
|
719
|
+
content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n"),
|
|
720
|
+
);
|
|
721
|
+
if (leaked) {
|
|
722
|
+
results.push(warnResult(`${tf} may contain secrets`));
|
|
723
|
+
} else {
|
|
724
|
+
results.push(okResult(`${tf} — no real secrets`));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return results;
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// ── Check registry ─────────────────────────────────────────────────
|
|
733
|
+
|
|
734
|
+
const checks: DoctorCheck[] = [
|
|
735
|
+
checkNodeVersion,
|
|
736
|
+
checkNpmAvailable,
|
|
737
|
+
checkConfirmPackage,
|
|
738
|
+
checkGitAvailable,
|
|
739
|
+
checkConfigFile,
|
|
740
|
+
checkPatchwardenConfigEnv,
|
|
741
|
+
checkConfigParseable,
|
|
742
|
+
checkWorkspaceRoot,
|
|
743
|
+
checkPathGuard,
|
|
744
|
+
checkSensitiveGuard,
|
|
745
|
+
checkSavePlanRules,
|
|
746
|
+
checkTaskArtifactAllowlist,
|
|
747
|
+
checkServerVersion,
|
|
748
|
+
checkToolProfiles,
|
|
749
|
+
checkReleaseGate,
|
|
750
|
+
checkHttpPort,
|
|
751
|
+
checkDistFiles,
|
|
752
|
+
checkToolModules,
|
|
753
|
+
checkDirectToolModules,
|
|
754
|
+
checkDirectSupportModules,
|
|
755
|
+
checkTaskDirectoryWritable,
|
|
756
|
+
checkWatcherStaleThreshold,
|
|
757
|
+
checkAllowedTestCommandsIncludesNpmTest,
|
|
758
|
+
checkTaskTimeoutDefaults,
|
|
759
|
+
checkAgentCommands,
|
|
760
|
+
checkAllowedTestCommandsSafety,
|
|
761
|
+
checkDirectProfileConfig,
|
|
762
|
+
checkTunnelExamples,
|
|
346
763
|
];
|
|
347
|
-
for (const { file, label, cmd: buildCmd } of distChecks) {
|
|
348
|
-
check(`${label} exists`, existsSync(resolve(process.cwd(), file)),
|
|
349
|
-
existsSync(resolve(process.cwd(), file)) ? file : `Missing — run: ${buildCmd}`);
|
|
350
|
-
}
|
|
351
764
|
|
|
352
|
-
//
|
|
353
|
-
const newTools = [
|
|
354
|
-
"listTasks",
|
|
355
|
-
"listAgents",
|
|
356
|
-
"cancelTask",
|
|
357
|
-
"killTask",
|
|
358
|
-
"retryTask",
|
|
359
|
-
"getTaskProgress",
|
|
360
|
-
"getTaskSummary",
|
|
361
|
-
"waitForTask",
|
|
362
|
-
"getTaskStdoutTail",
|
|
363
|
-
"healthCheck",
|
|
364
|
-
"auditTask",
|
|
365
|
-
];
|
|
366
|
-
for (const t of newTools) {
|
|
367
|
-
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
368
|
-
check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
369
|
-
}
|
|
765
|
+
// ── Context preparation ────────────────────────────────────────────
|
|
370
766
|
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
"createDirectSession",
|
|
374
|
-
"searchWorkspace",
|
|
375
|
-
"applyPatch",
|
|
376
|
-
"runVerification",
|
|
377
|
-
"finalizeDirectSession",
|
|
378
|
-
"auditSession",
|
|
379
|
-
];
|
|
380
|
-
for (const t of directToolModules) {
|
|
381
|
-
const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
|
|
382
|
-
check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
383
|
-
}
|
|
767
|
+
function prepareContext(): DoctorContext {
|
|
768
|
+
const allowDefaultConfig = process.argv.includes("--allow-default-config");
|
|
384
769
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"directPatch",
|
|
390
|
-
"directVerification",
|
|
391
|
-
"directAudit",
|
|
392
|
-
];
|
|
393
|
-
for (const t of directSupportModules) {
|
|
394
|
-
const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
|
|
395
|
-
check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
|
|
396
|
-
}
|
|
770
|
+
const configPaths = [
|
|
771
|
+
resolve(process.cwd(), "patchwarden.config.json"),
|
|
772
|
+
process.env.PATCHWARDEN_CONFIG || "",
|
|
773
|
+
].filter(Boolean);
|
|
397
774
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
try {
|
|
402
|
-
mkdirSync(tasksDir, { recursive: true });
|
|
403
|
-
const testFile = join(tasksDir, ".doctor-write-test");
|
|
404
|
-
writeFileSync(testFile, "ok", "utf-8");
|
|
405
|
-
rmSync(testFile);
|
|
406
|
-
check("Task directory writable", true, tasksDir);
|
|
407
|
-
|
|
408
|
-
const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
|
|
409
|
-
mkdirSync(sampleTaskDir, { recursive: true });
|
|
410
|
-
const sampleStatus = join(sampleTaskDir, "status.json");
|
|
411
|
-
writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
|
|
412
|
-
const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
|
|
413
|
-
rmSync(sampleTaskDir, { recursive: true, force: true });
|
|
414
|
-
check("Example task directory read/write", sampleReadable, sampleTaskDir);
|
|
415
|
-
} catch {
|
|
416
|
-
warnCheck("Task directory writable", false, tasksDir);
|
|
775
|
+
let configPathUsed = "";
|
|
776
|
+
for (const p of configPaths) {
|
|
777
|
+
if (existsSync(p)) { configPathUsed = p; break; }
|
|
417
778
|
}
|
|
418
779
|
|
|
419
|
-
//
|
|
780
|
+
// Load config (may fail)
|
|
781
|
+
let config: PatchWardenConfig | null = null;
|
|
782
|
+
let configError: string | null = null;
|
|
420
783
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
check("workspaceRoot writable", true, config.workspaceRoot);
|
|
425
|
-
} catch {
|
|
426
|
-
warnCheck("workspaceRoot writable", false, config.workspaceRoot);
|
|
784
|
+
config = getConfig();
|
|
785
|
+
} catch (err) {
|
|
786
|
+
configError = err instanceof Error ? err.message : String(err);
|
|
427
787
|
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (config) {
|
|
431
|
-
check("Watcher stale threshold is valid",
|
|
432
|
-
config.watcherStaleSeconds >= 5 && config.watcherStaleSeconds <= 3600,
|
|
433
|
-
`${config.watcherStaleSeconds}s`);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// allowedTestCommands has npm test
|
|
437
|
-
if (config) {
|
|
438
|
-
const hasNpmTest = config.allowedTestCommands.some((c: string) => c === "npm test" || c === "npm run test");
|
|
439
|
-
warnCheck("allowedTestCommands includes npm test", hasNpmTest,
|
|
440
|
-
hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands");
|
|
441
|
-
}
|
|
442
788
|
|
|
443
|
-
|
|
444
|
-
check("Task timeout defaults are valid",
|
|
445
|
-
config.defaultTaskTimeoutSeconds > 0 && config.defaultTaskTimeoutSeconds <= config.maxTaskTimeoutSeconds,
|
|
446
|
-
`default ${config.defaultTaskTimeoutSeconds}s, max ${config.maxTaskTimeoutSeconds}s`);
|
|
789
|
+
return { config, configError, allowDefaultConfig, configPathUsed };
|
|
447
790
|
}
|
|
448
791
|
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
for (const [name, agentCfg] of Object.entries(agents) as [string, any][]) {
|
|
453
|
-
const cmdName = agentCfg.command;
|
|
454
|
-
const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
|
|
455
|
-
if (looksLikePath) {
|
|
456
|
-
const agentExists = existsSync(cmdName);
|
|
457
|
-
warnCheck(`Agent "${name}" command available`, agentExists,
|
|
458
|
-
agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`);
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
// Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
|
|
462
|
-
const isWin = process.platform === "win32";
|
|
463
|
-
const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
|
|
464
|
-
const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
|
|
465
|
-
const found = cmd(lookupCmd) || cmd(fallbackCmd);
|
|
466
|
-
warnCheck(`Agent "${name}" command available`, found !== "",
|
|
467
|
-
found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
792
|
+
// ══════════════════════════════════════════════════════════════════
|
|
793
|
+
// Main
|
|
794
|
+
// ══════════════════════════════════════════════════════════════════
|
|
470
795
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
warn++;
|
|
483
|
-
}
|
|
796
|
+
async function main() {
|
|
797
|
+
console.log("PatchWarden Doctor\n");
|
|
798
|
+
const context = prepareContext();
|
|
799
|
+
|
|
800
|
+
const allResults: DoctorCheckResult[] = [];
|
|
801
|
+
for (const check of checks) {
|
|
802
|
+
try {
|
|
803
|
+
const results = await check.run(context);
|
|
804
|
+
allResults.push(...results);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
allResults.push(failResult(check.description, err instanceof Error ? err.message : String(err)));
|
|
484
807
|
}
|
|
485
808
|
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 14b. Direct profile config checks
|
|
489
|
-
if (config) {
|
|
490
|
-
const directCmds = config.directAllowedCommands || [];
|
|
491
|
-
warnCheck("directAllowedCommands is non-empty", directCmds.length > 0,
|
|
492
|
-
directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
|
|
493
|
-
|
|
494
|
-
// npm run doctor should NOT be in default Direct whitelist
|
|
495
|
-
const hasDoctor = directCmds.some((c: string) => c === "npm run doctor");
|
|
496
|
-
check("directAllowedCommands does not include npm run doctor", !hasDoctor,
|
|
497
|
-
hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
|
|
498
809
|
|
|
499
|
-
|
|
500
|
-
config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400,
|
|
501
|
-
`${config.directSessionTtlSeconds}s`);
|
|
810
|
+
console.log(allResults.map(formatResult).join("\n"));
|
|
502
811
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
// 15. Tunnel example files check
|
|
511
|
-
const tunnelFiles = [
|
|
512
|
-
"examples/openai-tunnel/README.md",
|
|
513
|
-
"examples/openai-tunnel/tunnel-client.example.yaml",
|
|
514
|
-
"examples/openai-tunnel/chatgpt-test-prompt.md",
|
|
515
|
-
"scripts/mcp/patchwarden-mcp-direct.cmd",
|
|
516
|
-
"PatchWarden.cmd",
|
|
517
|
-
"scripts/control/manage-patchwarden.ps1",
|
|
518
|
-
"scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
|
|
519
|
-
];
|
|
520
|
-
for (const tf of tunnelFiles) {
|
|
521
|
-
const full = resolve(process.cwd(), tf);
|
|
522
|
-
const exists = existsSync(full);
|
|
523
|
-
check(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing");
|
|
524
|
-
|
|
525
|
-
// Check for leaked secrets in example files
|
|
526
|
-
if (exists) {
|
|
527
|
-
const content = readFileSync(full, "utf-8");
|
|
528
|
-
// Only flag actual key-value assignments, not comments/mentions
|
|
529
|
-
const leaked = /(?:api_key|sk-[a-zA-Z0-9]{10,}|token\s*[:=]\s*\S{4,}|secret\s*[:=]\s*\S{4,}|password\s*[:=]\s*\S{4,})/gi.test(
|
|
530
|
-
// Strip comment lines first
|
|
531
|
-
content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n")
|
|
532
|
-
);
|
|
533
|
-
if (leaked) {
|
|
534
|
-
results.push(`[WARN] ${tf} may contain secrets`);
|
|
535
|
-
warn++;
|
|
536
|
-
} else {
|
|
537
|
-
results.push(`[OK] ${tf} — no real secrets`);
|
|
538
|
-
ok++;
|
|
539
|
-
}
|
|
812
|
+
let ok = 0;
|
|
813
|
+
let warn = 0;
|
|
814
|
+
let fail = 0;
|
|
815
|
+
for (const r of allResults) {
|
|
816
|
+
if (r.level === "OK") ok++;
|
|
817
|
+
else if (r.level === "WARN") warn++;
|
|
818
|
+
else fail++;
|
|
540
819
|
}
|
|
541
|
-
}
|
|
542
820
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
console.log(
|
|
549
|
-
console.log(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
process.exit(0);
|
|
559
|
-
} else {
|
|
560
|
-
console.log("\n✅ All checks passed.");
|
|
561
|
-
process.exit(0);
|
|
821
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
822
|
+
console.log(`OK: ${ok} WARN: ${warn} FAIL: ${fail}`);
|
|
823
|
+
console.log(`${"=".repeat(50)}`);
|
|
824
|
+
|
|
825
|
+
if (fail > 0) {
|
|
826
|
+
console.log("\n❌ Doctor found issues that need attention.");
|
|
827
|
+
console.log(" Fix FAIL items before using PatchWarden.");
|
|
828
|
+
process.exit(1);
|
|
829
|
+
} else if (warn > 0) {
|
|
830
|
+
console.log("\n⚠️ Doctor found warnings — review before production use.");
|
|
831
|
+
process.exit(0);
|
|
832
|
+
} else {
|
|
833
|
+
console.log("\n✅ All checks passed.");
|
|
834
|
+
process.exit(0);
|
|
835
|
+
}
|
|
562
836
|
}
|
|
563
837
|
|
|
564
|
-
} // end async main
|
|
565
|
-
|
|
566
838
|
main().catch((err) => {
|
|
567
|
-
|
|
839
|
+
logger.fatal("Doctor crashed", {
|
|
840
|
+
error: err instanceof Error ? err.message : String(err),
|
|
841
|
+
});
|
|
568
842
|
process.exit(1);
|
|
569
843
|
});
|