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
package/src/goal/subgoalSync.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import {
|
|
12
|
+
import { mutateGoalStatus } from "./goalStore.js";
|
|
13
13
|
import { updateSubgoalStatus } from "./goalStatus.js";
|
|
14
14
|
import { logger } from "../logging.js";
|
|
15
15
|
|
|
@@ -39,13 +39,16 @@ export function syncSubgoalOnTaskDone(
|
|
|
39
39
|
if (!goalId || !subgoalId) return;
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
mutateGoalStatus(goalId, (goalStatus) => {
|
|
43
|
+
const subgoal = goalStatus.subgoals.find((entry) => entry.id === subgoalId);
|
|
44
|
+
if (!subgoal || subgoal.status !== "running") {
|
|
45
|
+
return { result: undefined };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
next: updateSubgoalStatus(goalStatus, subgoalId, "done_by_agent"),
|
|
49
|
+
result: undefined,
|
|
50
|
+
};
|
|
51
|
+
}, workspaceRoot);
|
|
49
52
|
} catch (err) {
|
|
50
53
|
// subgoal 同步失败不应影响任务完成流程,只记录到 stderr
|
|
51
54
|
logger.error(
|
|
@@ -23,17 +23,18 @@ import {
|
|
|
23
23
|
existsSync,
|
|
24
24
|
mkdirSync,
|
|
25
25
|
readFileSync,
|
|
26
|
-
renameSync,
|
|
27
|
-
writeFileSync,
|
|
28
26
|
rmSync,
|
|
29
27
|
} from "node:fs";
|
|
30
|
-
import { join } from "node:path";
|
|
28
|
+
import { join, normalize } from "node:path";
|
|
31
29
|
import { execFileSync } from "node:child_process";
|
|
32
30
|
import { randomBytes } from "node:crypto";
|
|
33
31
|
import { getConfig } from "../config.js";
|
|
34
|
-
import { guardWorkspacePath } from "../security/pathGuard.js";
|
|
32
|
+
import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
|
|
35
33
|
import { guardSensitivePath } from "../security/sensitiveGuard.js";
|
|
36
34
|
import { PatchWardenError } from "../errors.js";
|
|
35
|
+
import { buildGitEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
|
|
36
|
+
import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
|
|
37
|
+
import { withFileLockSync } from "../utils/lockedJsonFile.js";
|
|
37
38
|
|
|
38
39
|
// ── 类型定义 ──────────────────────────────────────────────────────
|
|
39
40
|
|
|
@@ -56,9 +57,12 @@ export const WorktreeDir = "_workspacetrees";
|
|
|
56
57
|
|
|
57
58
|
/** 归档目录名(discard 后保留状态),位于 .patchwarden 下,始终为安全路径。 */
|
|
58
59
|
const WORKTREE_ARCHIVE_DIR = ".patchwarden/worktree-archive";
|
|
60
|
+
const WORKTREE_LOCK_DIR = ".patchwarden/worktree-locks";
|
|
59
61
|
|
|
60
62
|
const GIT_TIMEOUT_MS = 30000;
|
|
61
63
|
const GIT_BRANCH_TIMEOUT_MS = 15000;
|
|
64
|
+
const WORKTREE_ID_MAX_LENGTH = 32;
|
|
65
|
+
const WORKTREE_ID_PATTERN = /^wt_[a-z0-9]{1,16}_[a-f0-9]{12}$/;
|
|
62
66
|
|
|
63
67
|
// ── 辅助:解析 workspaceRoot ──────────────────────────────────────
|
|
64
68
|
|
|
@@ -66,20 +70,43 @@ function resolveWorkspaceRoot(workspaceRoot?: string): string {
|
|
|
66
70
|
return workspaceRoot ?? getConfig().workspaceRoot;
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/** Accept only IDs emitted by generateWorktreeId(). */
|
|
74
|
+
export function assertValidWorktreeId(worktreeId: string): void {
|
|
75
|
+
if (
|
|
76
|
+
worktreeId.length > WORKTREE_ID_MAX_LENGTH ||
|
|
77
|
+
!WORKTREE_ID_PATTERN.test(worktreeId)
|
|
78
|
+
) {
|
|
79
|
+
throw new PatchWardenError(
|
|
80
|
+
"invalid_worktree_id",
|
|
81
|
+
"Invalid worktree id. Expected the wt_<base36 timestamp>_<12 hex chars> format.",
|
|
82
|
+
"Use the worktree_id returned by create_subgoal_task.",
|
|
83
|
+
true,
|
|
84
|
+
{ worktree_id_length: worktreeId.length }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
// ── 目录路径解析 ──────────────────────────────────────────────────
|
|
70
90
|
|
|
71
91
|
/**
|
|
72
92
|
* 返回 `<workspaceRoot>/_workspacetrees` 目录路径。不自动创建目录。
|
|
73
93
|
*/
|
|
74
94
|
export function getWorktreesDir(workspaceRoot?: string): string {
|
|
75
|
-
|
|
95
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
96
|
+
return guardPath(join(wsRoot, WorktreeDir), wsRoot, WorktreeDir);
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
/**
|
|
79
100
|
* 返回 `<getWorktreesDir()>/<worktreeId>` 路径。
|
|
80
101
|
*/
|
|
81
102
|
export function getWorktreeDir(worktreeId: string, workspaceRoot?: string): string {
|
|
82
|
-
|
|
103
|
+
assertValidWorktreeId(worktreeId);
|
|
104
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
105
|
+
return guardPath(
|
|
106
|
+
join(getWorktreesDir(wsRoot), worktreeId),
|
|
107
|
+
wsRoot,
|
|
108
|
+
WorktreeDir
|
|
109
|
+
);
|
|
83
110
|
}
|
|
84
111
|
|
|
85
112
|
// ── ID 与 branch 生成 ─────────────────────────────────────────────
|
|
@@ -105,23 +132,110 @@ function sanitizeBranchSegment(value: string): string {
|
|
|
105
132
|
return cleaned === "" ? "x" : cleaned;
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
function samePath(left: string, right: string): boolean {
|
|
136
|
+
const normalizedLeft = normalize(left);
|
|
137
|
+
const normalizedRight = normalize(right);
|
|
138
|
+
return process.platform === "win32"
|
|
139
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
140
|
+
: normalizedLeft === normalizedRight;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withWorktreeRepositoryLock<R>(workspaceRoot: string, action: () => R): R {
|
|
144
|
+
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
145
|
+
const lockDir = guardPath(join(wsRoot, WORKTREE_LOCK_DIR), wsRoot, WORKTREE_LOCK_DIR);
|
|
146
|
+
mkdirSync(lockDir, { recursive: true });
|
|
147
|
+
return withFileLockSync(join(lockDir, "repository-mutation"), action, {
|
|
148
|
+
waitMs: 0,
|
|
149
|
+
busyError: () => new PatchWardenError(
|
|
150
|
+
"worktree_busy",
|
|
151
|
+
"Another managed worktree operation is already changing this repository.",
|
|
152
|
+
"Retry after the active create, merge, or discard operation finishes.",
|
|
153
|
+
true,
|
|
154
|
+
),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function invalidWorktreeStatus(
|
|
159
|
+
worktreeId: string,
|
|
160
|
+
message: string
|
|
161
|
+
): never {
|
|
162
|
+
throw new PatchWardenError(
|
|
163
|
+
"invalid_worktree_status",
|
|
164
|
+
`Invalid worktree status for "${worktreeId}": ${message}`,
|
|
165
|
+
"Do not edit worktree_status.json manually; recreate the isolated worktree.",
|
|
166
|
+
true,
|
|
167
|
+
{ worktree_id: worktreeId }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function validateWorktreeStatus(
|
|
172
|
+
value: unknown,
|
|
173
|
+
worktreeId: string,
|
|
174
|
+
workspaceRoot: string,
|
|
175
|
+
expectedPath: string
|
|
176
|
+
): WorktreeStatus {
|
|
177
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
178
|
+
return invalidWorktreeStatus(worktreeId, "expected a JSON object");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const candidate = value as Record<string, unknown>;
|
|
182
|
+
const statusValues = new Set(["active", "merged", "discarded"]);
|
|
183
|
+
if (
|
|
184
|
+
typeof candidate.worktree_id !== "string" ||
|
|
185
|
+
typeof candidate.goal_id !== "string" ||
|
|
186
|
+
typeof candidate.subgoal_id !== "string" ||
|
|
187
|
+
typeof candidate.path !== "string" ||
|
|
188
|
+
typeof candidate.created_at !== "string" ||
|
|
189
|
+
typeof candidate.status !== "string" ||
|
|
190
|
+
!statusValues.has(candidate.status) ||
|
|
191
|
+
typeof candidate.branch !== "string"
|
|
192
|
+
) {
|
|
193
|
+
return invalidWorktreeStatus(worktreeId, "missing or invalid required fields");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (candidate.worktree_id !== worktreeId) {
|
|
197
|
+
return invalidWorktreeStatus(worktreeId, "worktree_id does not match its directory");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let guardedStatusPath: string;
|
|
201
|
+
try {
|
|
202
|
+
guardedStatusPath = guardPath(candidate.path, workspaceRoot, WorktreeDir);
|
|
203
|
+
} catch {
|
|
204
|
+
return invalidWorktreeStatus(worktreeId, "path is outside the managed worktree root");
|
|
205
|
+
}
|
|
206
|
+
if (!samePath(guardedStatusPath, expectedPath)) {
|
|
207
|
+
return invalidWorktreeStatus(worktreeId, "path does not match its directory");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const expectedBranch = `pw-${sanitizeBranchSegment(candidate.goal_id)}-${sanitizeBranchSegment(candidate.subgoal_id)}`;
|
|
211
|
+
if (candidate.branch !== expectedBranch) {
|
|
212
|
+
return invalidWorktreeStatus(worktreeId, "branch does not match goal and subgoal metadata");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
(candidate.merged_at !== undefined && typeof candidate.merged_at !== "string") ||
|
|
217
|
+
(candidate.discarded_at !== undefined && typeof candidate.discarded_at !== "string")
|
|
218
|
+
) {
|
|
219
|
+
return invalidWorktreeStatus(worktreeId, "invalid lifecycle timestamp");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return candidate as unknown as WorktreeStatus;
|
|
223
|
+
}
|
|
224
|
+
|
|
108
225
|
// ── 原子写 ────────────────────────────────────────────────────────
|
|
109
226
|
|
|
110
|
-
/**
|
|
111
|
-
* 原子写入 status 文件:先写到 `.tmp` 文件,再 renameSync。
|
|
112
|
-
* 参考 goalStore.ts writeGoalStatus 的模式。
|
|
113
|
-
*/
|
|
114
227
|
function writeStatusAtomic(statusFilePath: string, status: WorktreeStatus): void {
|
|
115
|
-
|
|
116
|
-
writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
|
|
117
|
-
renameSync(tmpPath, statusFilePath);
|
|
228
|
+
atomicWriteJsonFileSync(statusFilePath, status);
|
|
118
229
|
}
|
|
119
230
|
|
|
120
231
|
function runGit(args: string[], cwd: string, timeoutMs: number): void {
|
|
121
|
-
|
|
232
|
+
const env = buildGitEnvironment(cwd);
|
|
233
|
+
execFileSync(resolveTrustedExecutable("git", cwd, { pathValue: env.PATH }), args, {
|
|
122
234
|
cwd,
|
|
123
235
|
timeout: timeoutMs,
|
|
124
236
|
stdio: ["pipe", "pipe", "pipe"],
|
|
237
|
+
windowsHide: true,
|
|
238
|
+
env,
|
|
125
239
|
});
|
|
126
240
|
}
|
|
127
241
|
|
|
@@ -157,6 +271,7 @@ export function createWorktree(
|
|
|
157
271
|
subgoalId: string,
|
|
158
272
|
workspaceRoot: string
|
|
159
273
|
): { worktreeId: string; worktreePath: string; branch: string } {
|
|
274
|
+
return withWorktreeRepositoryLock(workspaceRoot, () => {
|
|
160
275
|
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
161
276
|
|
|
162
277
|
const worktreeId = generateWorktreeId();
|
|
@@ -222,6 +337,7 @@ export function createWorktree(
|
|
|
222
337
|
}
|
|
223
338
|
);
|
|
224
339
|
}
|
|
340
|
+
});
|
|
225
341
|
}
|
|
226
342
|
|
|
227
343
|
/**
|
|
@@ -241,12 +357,13 @@ export function readWorktreeStatus(
|
|
|
241
357
|
const statusFilePath = join(worktreePath, "worktree_status.json");
|
|
242
358
|
if (!existsSync(statusFilePath)) return null;
|
|
243
359
|
|
|
360
|
+
let parsed: unknown;
|
|
244
361
|
try {
|
|
245
|
-
|
|
246
|
-
return JSON.parse(raw) as WorktreeStatus;
|
|
362
|
+
parsed = JSON.parse(readFileSync(statusFilePath, "utf-8"));
|
|
247
363
|
} catch {
|
|
248
|
-
return
|
|
364
|
+
return invalidWorktreeStatus(worktreeId, "file is not valid JSON");
|
|
249
365
|
}
|
|
366
|
+
return validateWorktreeStatus(parsed, worktreeId, wsRoot, worktreePath);
|
|
250
367
|
}
|
|
251
368
|
|
|
252
369
|
/**
|
|
@@ -264,6 +381,7 @@ export function mergeWorktree(
|
|
|
264
381
|
worktreeId: string,
|
|
265
382
|
workspaceRoot: string
|
|
266
383
|
): { status: "merged" } {
|
|
384
|
+
return withWorktreeRepositoryLock(workspaceRoot, () => {
|
|
267
385
|
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
268
386
|
const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
|
|
269
387
|
|
|
@@ -313,6 +431,7 @@ export function mergeWorktree(
|
|
|
313
431
|
writeStatusAtomic(statusFilePath, updatedStatus);
|
|
314
432
|
|
|
315
433
|
return { status: "merged" };
|
|
434
|
+
});
|
|
316
435
|
}
|
|
317
436
|
|
|
318
437
|
/**
|
|
@@ -333,6 +452,7 @@ export function discardWorktree(
|
|
|
333
452
|
worktreeId: string,
|
|
334
453
|
workspaceRoot: string
|
|
335
454
|
): { status: "discarded" } {
|
|
455
|
+
return withWorktreeRepositoryLock(workspaceRoot, () => {
|
|
336
456
|
const wsRoot = resolveWorkspaceRoot(workspaceRoot);
|
|
337
457
|
const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
|
|
338
458
|
|
|
@@ -364,10 +484,6 @@ export function discardWorktree(
|
|
|
364
484
|
try {
|
|
365
485
|
runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_TIMEOUT_MS);
|
|
366
486
|
} catch (err) {
|
|
367
|
-
// git remove 失败时再尝试直接删目录(best effort),然后抛错
|
|
368
|
-
try {
|
|
369
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
370
|
-
} catch { /* ignore */ }
|
|
371
487
|
throw new PatchWardenError(
|
|
372
488
|
"worktree_discard_failed",
|
|
373
489
|
`Failed to remove worktree "${worktreeId}": ${gitErrorMessage(err)}`,
|
|
@@ -391,7 +507,7 @@ export function discardWorktree(
|
|
|
391
507
|
|
|
392
508
|
const archiveDir = join(wsRoot, WORKTREE_ARCHIVE_DIR);
|
|
393
509
|
guardWorkspacePath(archiveDir, wsRoot);
|
|
394
|
-
//
|
|
510
|
+
// The archive path is workspace-confined and contains only managed status metadata.
|
|
395
511
|
|
|
396
512
|
try {
|
|
397
513
|
mkdirSync(archiveDir, { recursive: true });
|
|
@@ -401,4 +517,5 @@ export function discardWorktree(
|
|
|
401
517
|
writeStatusAtomic(archiveFilePath, archivedStatus);
|
|
402
518
|
|
|
403
519
|
return { status: "discarded" };
|
|
520
|
+
});
|
|
404
521
|
}
|
package/src/httpServer.ts
CHANGED
|
@@ -16,16 +16,21 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
22
22
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
23
|
import { loadConfig, getTasksDir } from "./config.js";
|
|
24
24
|
import { registerTools } from "./tools/registry.js";
|
|
25
|
-
import { healthCheck } from "./tools/healthCheck.js";
|
|
25
|
+
import { healthCheck } from "./tools/diagnostics/healthCheck.js";
|
|
26
26
|
import { getToolCatalogSnapshot } from "./tools/registry.js";
|
|
27
27
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
28
28
|
import { logger } from "./logging.js";
|
|
29
|
+
import { firstHeaderValue, timingSafeStringEqual } from "./security/secretComparison.js";
|
|
30
|
+
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
31
|
+
import { atomicWriteJsonFileSync } from "./utils/atomicFile.js";
|
|
32
|
+
import { mutateTaskStatus } from "./runner/taskStatusStore.js";
|
|
33
|
+
import { isTrustedLoopbackHostHeader } from "./security/loopbackHost.js";
|
|
29
34
|
|
|
30
35
|
// ── Bootstrap ─────────────────────────────────────────────────────
|
|
31
36
|
|
|
@@ -44,6 +49,16 @@ logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed
|
|
|
44
49
|
const httpCfg = config.http || {};
|
|
45
50
|
const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
|
|
46
51
|
const ownerToken = process.env[ownerTokenEnv] || "";
|
|
52
|
+
const MAX_ADMIN_BODY_BYTES = 64 * 1024;
|
|
53
|
+
const MAX_ACCEPTANCE_NOTES_CHARS = 10_000;
|
|
54
|
+
const REVIEWABLE_TASK_STATUSES = new Set([
|
|
55
|
+
"done_by_agent",
|
|
56
|
+
"done",
|
|
57
|
+
"accepted",
|
|
58
|
+
"rejected",
|
|
59
|
+
"needs_fix",
|
|
60
|
+
"blocked",
|
|
61
|
+
]);
|
|
47
62
|
|
|
48
63
|
if (ownerToken) {
|
|
49
64
|
logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
|
|
@@ -54,15 +69,15 @@ if (ownerToken) {
|
|
|
54
69
|
function checkOwnerToken(req: IncomingMessage): boolean {
|
|
55
70
|
if (!ownerToken) return true; // no token configured — allow all
|
|
56
71
|
|
|
57
|
-
const authHeader = req.headers["authorization"]
|
|
58
|
-
const customHeader = req.headers["x-patchwarden-token"]
|
|
72
|
+
const authHeader = firstHeaderValue(req.headers["authorization"]);
|
|
73
|
+
const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
|
|
59
74
|
|
|
60
75
|
if (authHeader.startsWith("Bearer ")) {
|
|
61
|
-
return authHeader.slice(7)
|
|
76
|
+
return timingSafeStringEqual(authHeader.slice(7), ownerToken);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
if (
|
|
65
|
-
return customHeader
|
|
79
|
+
if (customHeader.length > 0) {
|
|
80
|
+
return timingSafeStringEqual(customHeader, ownerToken);
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
return false;
|
|
@@ -82,11 +97,22 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
|
|
|
82
97
|
throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
|
|
83
98
|
}
|
|
84
99
|
let notes = "";
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
if (body.trim()) {
|
|
101
|
+
let parsed: unknown;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(body);
|
|
104
|
+
} catch {
|
|
105
|
+
throw httpError(400, "Acceptance body must be valid JSON.");
|
|
106
|
+
}
|
|
107
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
108
|
+
throw httpError(400, "Acceptance body must be a JSON object.");
|
|
109
|
+
}
|
|
110
|
+
const record = parsed as Record<string, unknown>;
|
|
111
|
+
const rawNotes = record.notes ?? record.reason ?? "";
|
|
112
|
+
if (typeof rawNotes !== "string") {
|
|
113
|
+
throw httpError(400, "Acceptance notes or reason must be a string.");
|
|
114
|
+
}
|
|
115
|
+
notes = redactSensitiveContent(rawNotes).content.slice(0, MAX_ACCEPTANCE_NOTES_CHARS);
|
|
90
116
|
}
|
|
91
117
|
const acceptance = {
|
|
92
118
|
status,
|
|
@@ -94,23 +120,69 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
|
|
|
94
120
|
reviewer: "human",
|
|
95
121
|
notes,
|
|
96
122
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// Also update the task's status.json with acceptance_status
|
|
123
|
+
// Commit the review artifact and status annotation while holding the same
|
|
124
|
+
// status lock used by the runner, cancel, and audit paths.
|
|
100
125
|
const statusFile = join(taskDir, "status.json");
|
|
101
|
-
if (existsSync(statusFile)) {
|
|
102
|
-
|
|
103
|
-
const current = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
104
|
-
current.acceptance_status = status;
|
|
105
|
-
current.acceptance_reviewed_at = acceptance.reviewed_at;
|
|
106
|
-
writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
|
|
107
|
-
} catch {
|
|
108
|
-
// status.json update is best-effort
|
|
109
|
-
}
|
|
126
|
+
if (!existsSync(statusFile)) {
|
|
127
|
+
throw httpError(409, `Task "${taskId}" has no status.json to review.`);
|
|
110
128
|
}
|
|
129
|
+
mutateTaskStatus(statusFile, (current) => {
|
|
130
|
+
if (!REVIEWABLE_TASK_STATUSES.has(String(current.status || ""))) {
|
|
131
|
+
throw httpError(409, `Task "${taskId}" is not in a reviewable terminal state.`);
|
|
132
|
+
}
|
|
133
|
+
atomicWriteJsonFileSync(filePath, acceptance);
|
|
134
|
+
const next = {
|
|
135
|
+
...current,
|
|
136
|
+
acceptance_status: status,
|
|
137
|
+
acceptance_reviewed_at: acceptance.reviewed_at,
|
|
138
|
+
updated_at: new Date().toISOString(),
|
|
139
|
+
};
|
|
140
|
+
return { next, result: next };
|
|
141
|
+
});
|
|
111
142
|
return acceptance;
|
|
112
143
|
}
|
|
113
144
|
|
|
145
|
+
function httpError(statusCode: number, message: string): Error & { statusCode: number } {
|
|
146
|
+
return Object.assign(new Error(message), { statusCode });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function readBoundedRequestBody(req: IncomingMessage, maxBytes: number): Promise<string> {
|
|
150
|
+
const declaredLength = Number(firstHeaderValue(req.headers["content-length"]));
|
|
151
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
152
|
+
req.resume();
|
|
153
|
+
return Promise.reject(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
|
|
154
|
+
}
|
|
155
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
156
|
+
const chunks: Buffer[] = [];
|
|
157
|
+
let total = 0;
|
|
158
|
+
let settled = false;
|
|
159
|
+
req.on("data", (chunk: Buffer | string) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
162
|
+
total += buffer.length;
|
|
163
|
+
if (total > maxBytes) {
|
|
164
|
+
settled = true;
|
|
165
|
+
req.resume();
|
|
166
|
+
rejectBody(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
chunks.push(buffer);
|
|
170
|
+
});
|
|
171
|
+
req.on("end", () => {
|
|
172
|
+
if (!settled) {
|
|
173
|
+
settled = true;
|
|
174
|
+
resolveBody(Buffer.concat(chunks).toString("utf-8"));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
req.on("error", (error) => {
|
|
178
|
+
if (!settled) {
|
|
179
|
+
settled = true;
|
|
180
|
+
rejectBody(error);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
114
186
|
function readAcceptance(taskId: string): object {
|
|
115
187
|
const filePath = getAcceptancePath(taskId);
|
|
116
188
|
if (!existsSync(filePath)) {
|
|
@@ -181,6 +253,16 @@ function parseAdminUrl(url: string) {
|
|
|
181
253
|
// ── HTTP server ───────────────────────────────────────────────────
|
|
182
254
|
|
|
183
255
|
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
256
|
+
if (!isTrustedLoopbackHostHeader(req.headers.host, port)) {
|
|
257
|
+
res.writeHead(421, {
|
|
258
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
259
|
+
"Cache-Control": "no-store",
|
|
260
|
+
"X-Content-Type-Options": "nosniff",
|
|
261
|
+
});
|
|
262
|
+
res.end(JSON.stringify({ error: "Untrusted Host header" }));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
184
266
|
// Health check endpoints
|
|
185
267
|
if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
|
|
186
268
|
const health = healthCheck(getToolCatalogSnapshot());
|
|
@@ -206,9 +288,7 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
|
|
|
206
288
|
return;
|
|
207
289
|
}
|
|
208
290
|
if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
|
|
209
|
-
const
|
|
210
|
-
for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }
|
|
211
|
-
const body = Buffer.concat(chunks).toString("utf-8");
|
|
291
|
+
const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
|
|
212
292
|
const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
|
|
213
293
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
214
294
|
res.end(JSON.stringify(acceptance, null, 2));
|
|
@@ -216,10 +296,13 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
|
|
|
216
296
|
}
|
|
217
297
|
res.writeHead(405, { "Content-Type": "application/json" });
|
|
218
298
|
res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
|
|
219
|
-
} catch (err:
|
|
220
|
-
const statusCode = err
|
|
299
|
+
} catch (err: unknown) {
|
|
300
|
+
const statusCode = err && typeof err === "object" && "statusCode" in err
|
|
301
|
+
? Number((err as { statusCode?: unknown }).statusCode) || 500
|
|
302
|
+
: 500;
|
|
303
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
221
304
|
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
222
|
-
res.end(JSON.stringify({ error:
|
|
305
|
+
res.end(JSON.stringify({ error: message }));
|
|
223
306
|
}
|
|
224
307
|
return;
|
|
225
308
|
}
|
package/src/logging.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
5
|
-
import { stableJsonStringify } from "./
|
|
5
|
+
import { stableJsonStringify } from "./utils/stableJson.js";
|
|
6
6
|
import { getConfig } from "./config.js";
|
|
7
|
+
import { appendBoundedTextFileSync } from "./utils/boundedFile.js";
|
|
8
|
+
|
|
9
|
+
const MAX_INVOCATION_LOG_BYTES = 5 * 1024 * 1024;
|
|
7
10
|
|
|
8
11
|
// ── Types ─────────────────────────────────────────────────────────
|
|
9
12
|
|
|
@@ -234,7 +237,11 @@ export function logToolInvocation(
|
|
|
234
237
|
|
|
235
238
|
mkdirSync(logsDir, { recursive: true });
|
|
236
239
|
const logFilePath = join(logsDir, "invocation.log");
|
|
237
|
-
|
|
240
|
+
appendBoundedTextFileSync(
|
|
241
|
+
logFilePath,
|
|
242
|
+
JSON.stringify(entry) + "\n",
|
|
243
|
+
MAX_INVOCATION_LOG_BYTES,
|
|
244
|
+
);
|
|
238
245
|
} catch (err) {
|
|
239
246
|
// 日志失败不应阻断主流程,仅向 stderr 记录错误
|
|
240
247
|
try {
|
|
@@ -334,10 +334,13 @@ function pathMatchesPattern(relPath: string, pattern: string): boolean {
|
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
function parseGithubRepo(repository: unknown): string | null {
|
|
337
|
+
const repositoryObject = repository && typeof repository === "object" && !Array.isArray(repository)
|
|
338
|
+
? repository as Record<string, unknown>
|
|
339
|
+
: null;
|
|
337
340
|
const raw = typeof repository === "string"
|
|
338
341
|
? repository
|
|
339
|
-
:
|
|
340
|
-
?
|
|
342
|
+
: typeof repositoryObject?.url === "string"
|
|
343
|
+
? repositoryObject.url
|
|
341
344
|
: "";
|
|
342
345
|
const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
|
|
343
346
|
return match ? match[1] : null;
|