patchwarden 1.1.0 → 1.5.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.
Files changed (258) hide show
  1. package/README.en.md +83 -8
  2. package/README.md +78 -10
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2001
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/direct/directSessionStore.d.ts +2 -0
  44. package/dist/direct/directVerification.js +7 -0
  45. package/dist/doctor.d.ts +18 -1
  46. package/dist/doctor.js +579 -348
  47. package/dist/goal/goalReport.d.ts +54 -0
  48. package/dist/goal/goalReport.js +204 -0
  49. package/dist/goal/goalStatus.d.ts +6 -0
  50. package/dist/goal/specKitImport.d.ts +63 -0
  51. package/dist/goal/specKitImport.js +220 -0
  52. package/dist/goal/subgoalSync.js +2 -1
  53. package/dist/httpServer.js +15 -12
  54. package/dist/index.js +7 -4
  55. package/dist/logging.d.ts +7 -1
  56. package/dist/logging.js +8 -0
  57. package/dist/policy/projectPolicy.d.ts +55 -0
  58. package/dist/policy/projectPolicy.js +286 -0
  59. package/dist/runner/changeCapture.d.ts +3 -3
  60. package/dist/runner/changeCapture.js +63 -39
  61. package/dist/runner/cli.js +7 -6
  62. package/dist/runner/postTaskCleanup.js +26 -2
  63. package/dist/runner/runTask.js +245 -221
  64. package/dist/runner/simpleProcess.js +4 -4
  65. package/dist/runner/watch.js +17 -14
  66. package/dist/security/contentRedaction.d.ts +6 -0
  67. package/dist/security/contentRedaction.js +22 -0
  68. package/dist/smoke-test.js +261 -255
  69. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  70. package/dist/test/unit/apply-patch.test.js +225 -0
  71. package/dist/test/unit/create-task.test.d.ts +1 -0
  72. package/dist/test/unit/create-task.test.js +197 -0
  73. package/dist/test/unit/direct-guards.test.js +124 -9
  74. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  75. package/dist/test/unit/evidence-pack.test.js +224 -0
  76. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  77. package/dist/test/unit/get-task-status.test.js +174 -0
  78. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  79. package/dist/test/unit/get-task-summary.test.js +146 -0
  80. package/dist/test/unit/goal-report.test.d.ts +1 -0
  81. package/dist/test/unit/goal-report.test.js +159 -0
  82. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  83. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  84. package/dist/test/unit/path-guard.test.js +24 -0
  85. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  86. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  87. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  88. package/dist/test/unit/run-task-loop.test.js +380 -0
  89. package/dist/test/unit/schema-drift-check.test.js +10 -9
  90. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  91. package/dist/test/unit/spec-kit-import.test.js +341 -0
  92. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  93. package/dist/test/unit/wait-for-task.test.js +144 -0
  94. package/dist/tools/auditTask.d.ts +8 -63
  95. package/dist/tools/auditTask.js +12 -8
  96. package/dist/tools/createDirectSession.d.ts +1 -1
  97. package/dist/tools/createDirectSession.js +2 -2
  98. package/dist/tools/createTask.d.ts +2 -2
  99. package/dist/tools/createTask.js +4 -4
  100. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  101. package/dist/tools/dispatch/coreDispatch.js +282 -0
  102. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  103. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  104. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  105. package/dist/tools/dispatch/directDispatch.js +115 -0
  106. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  107. package/dist/tools/dispatch/goalDispatch.js +91 -0
  108. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  109. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  110. package/dist/tools/dispatch/types.d.ts +23 -0
  111. package/dist/tools/dispatch/types.js +15 -0
  112. package/dist/tools/evidencePack.d.ts +45 -0
  113. package/dist/tools/evidencePack.js +375 -0
  114. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  115. package/dist/tools/finalizeDirectSession.js +3 -3
  116. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  117. package/dist/tools/goalSubgoalTask.js +2 -2
  118. package/dist/tools/healthCheck.js +3 -3
  119. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  120. package/dist/tools/recommendAgentForTask.js +56 -0
  121. package/dist/tools/registry.d.ts +3 -3
  122. package/dist/tools/registry.js +333 -402
  123. package/dist/tools/releaseMode.d.ts +50 -0
  124. package/dist/tools/releaseMode.js +370 -0
  125. package/dist/tools/retryTask.d.ts +2 -2
  126. package/dist/tools/retryTask.js +2 -2
  127. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  128. package/dist/tools/runDirectVerificationBundle.js +64 -0
  129. package/dist/tools/runTaskLoop.d.ts +57 -0
  130. package/dist/tools/runTaskLoop.js +417 -0
  131. package/dist/tools/runVerification.d.ts +4 -0
  132. package/dist/tools/runVerification.js +4 -0
  133. package/dist/tools/safeViews.d.ts +8 -2
  134. package/dist/tools/safeViews.js +4 -2
  135. package/dist/tools/taskLineage.d.ts +91 -0
  136. package/dist/tools/taskLineage.js +175 -0
  137. package/dist/tools/toolCatalog.d.ts +2 -2
  138. package/dist/tools/toolCatalog.js +6 -0
  139. package/dist/tools/toolRegistry.js +132 -0
  140. package/dist/version.d.ts +2 -2
  141. package/dist/version.js +2 -2
  142. package/docs/CODE_WIKI.md +983 -0
  143. package/docs/agentseal-integration.md +150 -0
  144. package/docs/architecture.md +63 -0
  145. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  146. package/docs/chatgpt-usage.md +31 -0
  147. package/docs/control-center/README.md +9 -0
  148. package/docs/dashboard-overview.md +86 -0
  149. package/docs/demo.md +8 -0
  150. package/docs/direct-session-workflow.md +98 -0
  151. package/docs/evidence-pack-schema.md +215 -0
  152. package/docs/execution-plan-2026-07-09.md +315 -0
  153. package/docs/lineage-evidence-pack-workflow.md +127 -0
  154. package/docs/mcp-inspector-testing.md +200 -0
  155. package/docs/open-source-application.md +162 -0
  156. package/docs/opencode-worker.md +151 -0
  157. package/docs/openhands-worker.md +181 -0
  158. package/docs/release-evidence.md +72 -0
  159. package/docs/roadmap-execution-and-acceptance.md +365 -0
  160. package/docs/spec-kit-integration.md +131 -0
  161. package/docs/task-safe-review-workflow.md +98 -0
  162. package/docs/threat-model.md +79 -0
  163. package/docs/user-feedback.md +40 -0
  164. package/docs/why-patchwarden.md +110 -0
  165. package/package.json +3 -3
  166. package/scripts/checks/control-center-smoke.js +443 -0
  167. package/scripts/checks/control-smoke.js +2 -2
  168. package/scripts/checks/lifecycle-smoke.js +29 -23
  169. package/scripts/checks/mcp-manifest-check.js +12 -0
  170. package/scripts/checks/mcp-smoke.js +33 -7
  171. package/scripts/checks/watcher-supervisor-smoke.js +1 -1
  172. package/scripts/generate-demo-gif.py +320 -0
  173. package/src/assessments/agentAssessor.ts +3 -3
  174. package/src/assessments/assessmentStore.ts +2 -2
  175. package/src/assessments/confirmCli.ts +5 -4
  176. package/src/config.ts +37 -0
  177. package/src/control/middleware/auth.ts +17 -0
  178. package/src/control/middleware/static.ts +71 -0
  179. package/src/control/routes/audit.ts +321 -0
  180. package/src/control/routes/evidence.ts +107 -0
  181. package/src/control/routes/lineage.ts +92 -0
  182. package/src/control/routes/policy.ts +81 -0
  183. package/src/control/routes/process.ts +204 -0
  184. package/src/control/routes/sessions.ts +251 -0
  185. package/src/control/routes/status.ts +325 -0
  186. package/src/control/routes/taskActions.ts +248 -0
  187. package/src/control/routes/tasks.ts +323 -0
  188. package/src/control/routes/workspace.ts +203 -0
  189. package/src/control/runtime.ts +472 -0
  190. package/src/control/server.ts +471 -0
  191. package/src/control/shared.ts +294 -0
  192. package/src/controlCenter.ts +7 -2150
  193. package/src/direct/directGuards.ts +28 -7
  194. package/src/direct/directSessionStore.ts +2 -0
  195. package/src/direct/directVerification.ts +7 -0
  196. package/src/doctor.ts +741 -481
  197. package/src/goal/goalReport.ts +271 -0
  198. package/src/goal/goalStatus.ts +6 -0
  199. package/src/goal/specKitImport.ts +355 -0
  200. package/src/goal/subgoalSync.ts +4 -2
  201. package/src/httpServer.ts +17 -14
  202. package/src/index.ts +7 -4
  203. package/src/logging.ts +10 -1
  204. package/src/policy/projectPolicy.ts +344 -0
  205. package/src/runner/changeCapture.ts +70 -42
  206. package/src/runner/cli.ts +7 -6
  207. package/src/runner/postTaskCleanup.ts +26 -2
  208. package/src/runner/runTask.ts +325 -223
  209. package/src/runner/simpleProcess.ts +4 -4
  210. package/src/runner/watch.ts +17 -14
  211. package/src/security/contentRedaction.ts +29 -0
  212. package/src/smoke-test.ts +254 -252
  213. package/src/test/unit/apply-patch.test.ts +293 -0
  214. package/src/test/unit/create-task.test.ts +255 -0
  215. package/src/test/unit/direct-guards.test.ts +178 -8
  216. package/src/test/unit/evidence-pack.test.ts +251 -0
  217. package/src/test/unit/get-task-status.test.ts +203 -0
  218. package/src/test/unit/get-task-summary.test.ts +173 -0
  219. package/src/test/unit/goal-report.test.ts +189 -0
  220. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  221. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  222. package/src/test/unit/path-guard.test.ts +30 -0
  223. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  224. package/src/test/unit/run-task-loop.test.ts +425 -0
  225. package/src/test/unit/schema-drift-check.test.ts +11 -9
  226. package/src/test/unit/spec-kit-import.test.ts +429 -0
  227. package/src/test/unit/wait-for-task.test.ts +176 -0
  228. package/src/tools/auditTask.ts +99 -59
  229. package/src/tools/createDirectSession.ts +3 -3
  230. package/src/tools/createTask.ts +7 -7
  231. package/src/tools/dispatch/coreDispatch.ts +374 -0
  232. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  233. package/src/tools/dispatch/directDispatch.ts +167 -0
  234. package/src/tools/dispatch/goalDispatch.ts +127 -0
  235. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  236. package/src/tools/dispatch/types.ts +24 -0
  237. package/src/tools/evidencePack.ts +490 -0
  238. package/src/tools/finalizeDirectSession.ts +4 -4
  239. package/src/tools/goalSubgoalTask.ts +2 -2
  240. package/src/tools/healthCheck.ts +3 -3
  241. package/src/tools/listWorkspace.ts +71 -71
  242. package/src/tools/recommendAgentForTask.ts +79 -0
  243. package/src/tools/registry.ts +352 -509
  244. package/src/tools/releaseMode.ts +450 -0
  245. package/src/tools/retryTask.ts +2 -2
  246. package/src/tools/runDirectVerificationBundle.ts +98 -0
  247. package/src/tools/runTaskLoop.ts +526 -0
  248. package/src/tools/runVerification.ts +8 -0
  249. package/src/tools/safeViews.ts +4 -2
  250. package/src/tools/taskLineage.ts +300 -0
  251. package/src/tools/toolCatalog.ts +6 -0
  252. package/src/tools/toolRegistry.ts +132 -0
  253. package/src/version.ts +2 -2
  254. package/ui/pages/audit.html +192 -3
  255. package/ui/pages/dashboard.html +1070 -14
  256. package/ui/pages/direct-sessions.html +505 -53
  257. package/ui/pages/task-detail.html +456 -438
  258. package/ui/pages/tasks.html +598 -61
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * Shared tool registry for PatchWarden MCP server.
3
3
  * Used by both stdio (index.ts) and HTTP (httpServer.ts) transports.
4
4
  */
@@ -9,57 +9,8 @@ import {
9
9
  ListToolsRequestSchema,
10
10
  } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { getAllConfiguredTestCommands, getAllConfiguredDirectCommands, getConfig } from "../config.js";
12
- import { savePlan } from "../tools/savePlan.js";
13
- import { getPlan } from "../tools/getPlan.js";
14
- import { createTask } from "../tools/createTask.js";
15
- import { getTaskStatus } from "../tools/getTaskStatus.js";
16
- import { getResult, getResultJson, getDiff, getTestLog, getTaskLogTail } from "../tools/taskOutputs.js";
17
- import { listWorkspace } from "../tools/listWorkspace.js";
18
- import { readWorkspaceFile } from "../tools/readWorkspaceFile.js";
19
- import { listTasks } from "../tools/listTasks.js";
20
- import { cancelTask } from "../tools/cancelTask.js";
21
- import { killTask } from "../tools/killTask.js";
22
- import { retryTask } from "../tools/retryTask.js";
23
- import { getTaskStdoutTail } from "../tools/getTaskStdoutTail.js";
24
- import { getTaskProgress } from "../tools/getTaskProgress.js";
25
- import { listAgents } from "../tools/listAgents.js";
26
- import { healthCheck } from "../tools/healthCheck.js";
27
- import { getTaskSummary } from "../tools/getTaskSummary.js";
28
- import { waitForTask } from "../tools/waitForTask.js";
29
12
  import { errorPayload, PatchWardenError } from "../errors.js";
30
- import { auditTask } from "../tools/auditTask.js";
31
- import { safeStatus } from "../tools/safeStatus.js";
32
- import {
33
- safeAudit,
34
- safeAuditDirectSession,
35
- safeDiffSummary,
36
- safeDirectSummary,
37
- safeFinalizeDirectSession,
38
- safeResult,
39
- safeTestSummary,
40
- } from "../tools/safeViews.js";
41
- import { diagnoseTask } from "../tools/diagnoseTask.js";
42
- import { reconcileTasks } from "../tools/reconcileTasks.js";
43
- import { discoverTools } from "../tools/discoverTools.js";
44
- import { explainTool } from "../tools/explainTool.js";
45
- import { invokeDiscoveredTool } from "./invokeDiscoveredTool.js";
46
13
  import { logger } from "../logging.js";
47
- import { runTask } from "../runner/runTask.js";
48
- import { createDirectSession } from "../tools/createDirectSession.js";
49
- import { searchWorkspace } from "../tools/searchWorkspace.js";
50
- import { applyPatch } from "../tools/applyPatch.js";
51
- import { runVerification } from "../tools/runVerification.js";
52
- import { finalizeDirectSession } from "../tools/finalizeDirectSession.js";
53
- import { auditSession } from "../tools/auditSession.js";
54
- import { syncFile } from "../tools/syncFile.js";
55
- import { createGoal, listGoals, readGoal, readGoalStatus } from "../goal/goalStore.js";
56
- import { suggestNextSubgoal } from "../goal/goalGraph.js";
57
- import { exportHandoff } from "../goal/handoffExport.js";
58
- import { acceptSubgoal, rejectSubgoal, summarizeGoalProgress } from "../goal/goalProgress.js";
59
- import { createSubgoalTask } from "./goalSubgoalTask.js";
60
- import { checkReleaseGate } from "./checkReleaseGate.js";
61
- import { mergeWorktreeTool } from "./mergeWorktree.js";
62
- import { discardWorktreeTool } from "./discardWorktree.js";
63
14
  import { TASK_TEMPLATE_NAMES } from "./taskTemplates.js";
64
15
  import {
65
16
  buildToolCatalogSnapshot,
@@ -68,6 +19,12 @@ import {
68
19
  selectToolsForProfile,
69
20
  type ToolCatalogSnapshot,
70
21
  } from "./toolCatalog.js";
22
+ import { coreHandlers, runTaskHandler } from "./dispatch/coreDispatch.js";
23
+ import { goalHandlers } from "./dispatch/goalDispatch.js";
24
+ import { directHandlers } from "./dispatch/directDispatch.js";
25
+ import { releaseHandlers } from "./dispatch/releaseDispatch.js";
26
+ import { diagnosticHandlers } from "./dispatch/diagnosticDispatch.js";
27
+ import type { ToolHandlerMap } from "./dispatch/types.js";
71
28
 
72
29
  // ── Tool definitions ──────────────────────────────────────────────
73
30
 
@@ -84,10 +41,12 @@ export interface ToolDef {
84
41
  export function getToolDefs(): ToolDef[] {
85
42
  const config = getConfig();
86
43
  const agentNames = Object.keys(config.agents).sort();
44
+ const routableAgentNames = [...agentNames, "auto"];
87
45
  const agentDescription = agentNames.length > 0
88
- ? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}`
46
+ ? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}. run_task_loop also accepts "auto" for bounded routing.`
89
47
  : "Configured local agent name. No agents are currently configured.";
90
48
  const testCommands = getAllConfiguredTestCommands(config).sort();
49
+ const directCommands = getAllConfiguredDirectCommands(config);
91
50
  const tools: ToolDef[] = [
92
51
  {
93
52
  name: "save_plan",
@@ -201,6 +160,178 @@ export function getToolDefs(): ToolDef[] {
201
160
  required: [],
202
161
  },
203
162
  },
163
+ {
164
+ name: "run_task_loop",
165
+ description:
166
+ "Run a guarded PatchWarden task loop by composing create_task, wait_for_task, safe summaries, and audit_task. It does not bypass the watcher, command allow-list, workspace confinement, or confirmation boundaries. Returns only bounded structured lineage and final status.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: {
170
+ repo_path: {
171
+ type: "string",
172
+ description: "Required repository path inside workspaceRoot. No implicit workspace-root fallback is allowed.",
173
+ },
174
+ goal: { type: "string", description: "Task goal to execute through the guarded loop." },
175
+ verify_commands: {
176
+ type: "array",
177
+ minItems: 1,
178
+ maxItems: 20,
179
+ items: {
180
+ type: "string",
181
+ ...(testCommands.length > 0 ? { enum: testCommands } : {}),
182
+ },
183
+ description: "Exact-match verification commands. The loop reuses create_task validation and will not run commands outside the allow-list.",
184
+ },
185
+ agent: {
186
+ type: "string",
187
+ description: agentDescription,
188
+ ...(agentNames.length > 0 ? { enum: routableAgentNames } : {}),
189
+ },
190
+ template: {
191
+ type: "string",
192
+ enum: ["inspect_only", "feature_small", "release_check"],
193
+ default: "feature_small",
194
+ description: "Initial guarded task template. Follow-up repair tasks use fix_tests automatically when verification fails.",
195
+ },
196
+ max_iterations: {
197
+ type: "integer",
198
+ minimum: 1,
199
+ maximum: 5,
200
+ default: 3,
201
+ description: "Maximum total main/fix task attempts.",
202
+ },
203
+ task_timeout_seconds: {
204
+ type: "integer",
205
+ minimum: 1,
206
+ maximum: config.maxTaskTimeoutSeconds,
207
+ default: config.defaultTaskTimeoutSeconds,
208
+ description: `Per-task wait budget in seconds (default ${config.defaultTaskTimeoutSeconds}, max ${config.maxTaskTimeoutSeconds}).`,
209
+ },
210
+ auto_fix_tests: {
211
+ type: "boolean",
212
+ default: true,
213
+ description: "When true, create a fix_tests follow-up task after failed_verification until max_iterations is reached.",
214
+ },
215
+ auto_cleanup_artifacts: {
216
+ type: "boolean",
217
+ default: true,
218
+ description: "Records cleanup intent in lineage. Low-risk cleanup remains handled by runTask post-task cleanup.",
219
+ },
220
+ stop_on_high_risk: {
221
+ type: "boolean",
222
+ default: true,
223
+ description: "When true, stop immediately on non-policy high-risk audit evidence. Scope, policy, sensitive-path, release/publish, and confirmation boundaries always stop regardless of this flag.",
224
+ },
225
+ direct_verify: {
226
+ type: "boolean",
227
+ default: false,
228
+ description: "When true, run an independent Direct verification session after the guarded task/audit succeeds. Direct never patches files in this loop.",
229
+ },
230
+ direct_verify_commands: {
231
+ type: "array",
232
+ minItems: 1,
233
+ maxItems: 20,
234
+ items: {
235
+ type: "string",
236
+ ...(directCommands.length > 0 ? { enum: directCommands } : {}),
237
+ },
238
+ description: "Optional Direct verification commands. Defaults to verify_commands and still must pass the Direct command allow-list.",
239
+ },
240
+ direct_verify_timeout_seconds: {
241
+ type: "integer",
242
+ minimum: 1,
243
+ maximum: Math.min(config.maxTaskTimeoutSeconds, config.directSessionTtlSeconds),
244
+ default: 120,
245
+ description: "Per-command timeout for Direct verification.",
246
+ },
247
+ scope_files: {
248
+ type: "array",
249
+ maxItems: 50,
250
+ items: { type: "string" },
251
+ description: "Optional bounded file scope used only for agent routing hints.",
252
+ },
253
+ isolation_mode: {
254
+ type: "string",
255
+ enum: ["current_repo", "worktree"],
256
+ default: "current_repo",
257
+ description: "Use current_repo by default. worktree creates an isolated git worktree for the task but never auto-merges it.",
258
+ },
259
+ worktree_base_branch: {
260
+ type: "string",
261
+ description: "Optional base-branch label recorded in lineage. v1.5 does not auto-checkout or merge this branch.",
262
+ },
263
+ worktree_cleanup: {
264
+ type: "string",
265
+ enum: ["keep", "archive", "delete_ignored_only"],
266
+ default: "keep",
267
+ description: "Cleanup intent recorded in lineage. v1.5 keeps worktrees by default and does not auto-delete them.",
268
+ },
269
+ },
270
+ required: ["repo_path", "goal", "verify_commands"],
271
+ },
272
+ },
273
+ {
274
+ name: "recommend_agent_for_task",
275
+ description:
276
+ "Return a bounded read-only agent routing recommendation for a repo-scoped task. Does not start an agent, create a task, or read logs.",
277
+ inputSchema: {
278
+ type: "object",
279
+ properties: {
280
+ repo_path: { type: "string", description: "Repository path inside workspaceRoot." },
281
+ goal: { type: "string", description: "Task goal used for routing hints." },
282
+ scope_files: {
283
+ type: "array",
284
+ maxItems: 50,
285
+ items: { type: "string" },
286
+ description: "Optional bounded list of files or directories expected to be in scope.",
287
+ },
288
+ template: { type: "string", description: "Optional task template hint." },
289
+ risk_hint: { type: "string", description: "Optional compact risk hint text." },
290
+ },
291
+ required: ["repo_path", "goal"],
292
+ },
293
+ },
294
+ {
295
+ name: "get_task_lineage",
296
+ description:
297
+ "Read a bounded safe summary for a run_task_loop lineage. Does not return full logs, diffs, stdout, stderr, or markdown artifacts.",
298
+ inputSchema: {
299
+ type: "object",
300
+ properties: {
301
+ lineage_id: { type: "string", description: "Lineage ID returned by run_task_loop." },
302
+ max_items: { type: "integer", minimum: 1, maximum: 50, default: 8, description: "Maximum rounds/tasks/warnings to return." },
303
+ },
304
+ required: ["lineage_id"],
305
+ },
306
+ },
307
+ {
308
+ name: "export_task_evidence_pack",
309
+ description:
310
+ "Export a bounded evidence pack for a run_task_loop lineage. Writes evidence.json and EVIDENCE.md without stdout, stderr, full logs, full diff, or sensitive file contents.",
311
+ inputSchema: {
312
+ type: "object",
313
+ properties: {
314
+ lineage_id: { type: "string", description: "Lineage ID returned by run_task_loop." },
315
+ max_items: { type: "integer", minimum: 1, maximum: 50, default: 12, description: "Maximum rounds/tasks/warnings to include." },
316
+ },
317
+ required: ["lineage_id"],
318
+ },
319
+ },
320
+ {
321
+ name: "get_project_policy",
322
+ description:
323
+ "Read the bounded effective .patchwarden/project-policy.json summary for a repository. Missing policy returns safe defaults. Project policy never expands PatchWarden command allow-lists, workspace confinement, sensitive-path blocking, or watcher/audit boundaries.",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ repo_path: {
328
+ type: "string",
329
+ description: "Repository path inside workspaceRoot.",
330
+ },
331
+ },
332
+ required: ["repo_path"],
333
+ },
334
+ },
204
335
  {
205
336
  name: "get_task_status",
206
337
  description: "Check task status, execution phase, watcher health, pending reason, current command, timeout, and change evidence.",
@@ -768,6 +899,29 @@ export function getToolDefs(): ToolDef[] {
768
899
  required: ["goal_id"],
769
900
  },
770
901
  },
902
+ {
903
+ name: "export_goal_report",
904
+ description: "Export a structured final report for a Goal session, aggregating subgoal completion, task evidence, and risk summary.",
905
+ inputSchema: {
906
+ type: "object",
907
+ properties: {
908
+ goal_id: { type: "string", description: "The goal id to export report for." },
909
+ },
910
+ required: ["goal_id"],
911
+ },
912
+ },
913
+ {
914
+ name: "import_speckit_tasks",
915
+ description: "Import Spec Kit tasks into a Goal session as subgoals, mapping task files to scope hints and acceptance criteria.",
916
+ inputSchema: {
917
+ type: "object",
918
+ properties: {
919
+ goal_id: { type: "string", description: "The goal id to import tasks into." },
920
+ spec_kit_json: { type: "string", description: "Spec Kit JSON text containing spec, tasks[], and acceptance[] fields." },
921
+ },
922
+ required: ["goal_id", "spec_kit_json"],
923
+ },
924
+ },
771
925
  {
772
926
  name: "check_release_gate",
773
927
  description:
@@ -789,6 +943,91 @@ export function getToolDefs(): ToolDef[] {
789
943
  required: ["repo_path", "target_stage"],
790
944
  },
791
945
  },
946
+ {
947
+ name: "release_check",
948
+ description:
949
+ "v1.3.0: Run a bounded release readiness check by wrapping the existing release gate. Local stages use existing guarded release-gate commands; remote stages are read-only. Does not publish, push, tag, or create a GitHub Release.",
950
+ inputSchema: {
951
+ type: "object",
952
+ properties: {
953
+ repo_path: { type: "string", description: "Repository path inside workspaceRoot." },
954
+ target_stage: {
955
+ type: "string",
956
+ enum: ["local_ready", "packed_ready", "published_verified", "github_release_verified", "ci_verified"],
957
+ default: "local_ready",
958
+ description: "Release gate stage to check. Defaults to local_ready.",
959
+ },
960
+ package_name: { type: "string", description: "npm package name for remote verification." },
961
+ version: { type: "string", description: "Version to verify. Defaults to project policy/package.json when omitted." },
962
+ github_repo: { type: "string", description: "GitHub repo in owner/repo form for release/CI verification." },
963
+ branch: { type: "string", default: "main", description: "Branch for CI verification." },
964
+ },
965
+ required: ["repo_path"],
966
+ },
967
+ },
968
+ {
969
+ name: "release_prepare",
970
+ description:
971
+ "v1.3.0: Run project-policy release preparation commands only when each command is already accepted by the existing PatchWarden command guard. Returns command status only, never stdout/stderr. Does not publish, push, tag, or create a GitHub Release.",
972
+ inputSchema: {
973
+ type: "object",
974
+ properties: {
975
+ repo_path: { type: "string", description: "Repository path inside workspaceRoot." },
976
+ required_commands: {
977
+ type: "array",
978
+ maxItems: 10,
979
+ items: {
980
+ type: "string",
981
+ ...(testCommands.length > 0 ? { enum: testCommands } : {}),
982
+ },
983
+ description: "Optional exact-match release preparation commands. Defaults to project policy release_mode.required_commands.",
984
+ },
985
+ timeout_seconds: {
986
+ type: "integer",
987
+ minimum: 1,
988
+ maximum: config.maxTaskTimeoutSeconds,
989
+ default: 300,
990
+ description: "Per-command timeout in seconds.",
991
+ },
992
+ },
993
+ required: ["repo_path"],
994
+ },
995
+ },
996
+ {
997
+ name: "release_verify",
998
+ description:
999
+ "v1.3.0: Verify npm/GitHub/CI release facts with read-only HTTPS requests. Does not run local shell commands and does not publish, push, tag, or create a GitHub Release.",
1000
+ inputSchema: {
1001
+ type: "object",
1002
+ properties: {
1003
+ repo_path: { type: "string", description: "Repository path inside workspaceRoot." },
1004
+ package_name: { type: "string", description: "npm package name. Defaults to package.json name." },
1005
+ version: { type: "string", description: "Version to verify. Defaults to project policy/package.json." },
1006
+ github_repo: { type: "string", description: "GitHub repo in owner/repo form. Defaults to package.json repository when available." },
1007
+ branch: { type: "string", default: "main", description: "Branch for CI verification." },
1008
+ },
1009
+ required: ["repo_path"],
1010
+ },
1011
+ },
1012
+ {
1013
+ name: "release_cleanup",
1014
+ description:
1015
+ "v1.3.0: Clean up release artifacts using project-policy auto_cleanup rules. Defaults to dry_run=true. Non-dry-run cleanup only removes low-risk ignored/untracked artifacts under repo_path and writes an audit summary.",
1016
+ inputSchema: {
1017
+ type: "object",
1018
+ properties: {
1019
+ repo_path: { type: "string", description: "Repository path inside workspaceRoot." },
1020
+ dry_run: { type: "boolean", default: true, description: "Preview cleanup by default. Set false to remove eligible artifacts." },
1021
+ patterns: {
1022
+ type: "array",
1023
+ maxItems: 20,
1024
+ items: { type: "string" },
1025
+ description: "Optional cleanup patterns. Defaults to project policy auto_cleanup.patterns.",
1026
+ },
1027
+ },
1028
+ required: ["repo_path"],
1029
+ },
1030
+ },
792
1031
  {
793
1032
  name: "merge_worktree",
794
1033
  description:
@@ -818,7 +1057,6 @@ export function getToolDefs(): ToolDef[] {
818
1057
  ];
819
1058
 
820
1059
  // Direct session tools
821
- const directCommands = getAllConfiguredDirectCommands(config);
822
1060
  tools.push({
823
1061
  name: "create_direct_session",
824
1062
  description:
@@ -908,6 +1146,30 @@ export function getToolDefs(): ToolDef[] {
908
1146
  },
909
1147
  });
910
1148
 
1149
+ tools.push({
1150
+ name: "run_direct_verification_bundle",
1151
+ description:
1152
+ "Run multiple allowlisted Direct verification commands sequentially and return only bounded structured status. Omits stdout/stderr tails and log content.",
1153
+ inputSchema: {
1154
+ type: "object",
1155
+ properties: {
1156
+ session_id: { type: "string", description: "Direct session ID" },
1157
+ commands: {
1158
+ type: "array",
1159
+ minItems: 1,
1160
+ maxItems: 20,
1161
+ items: {
1162
+ type: "string",
1163
+ ...(directCommands.length > 0 ? { enum: directCommands } : {}),
1164
+ },
1165
+ description: "Verification commands to run in order. Each command must be accepted by the existing Direct command guard.",
1166
+ },
1167
+ timeout_seconds: { type: "integer", minimum: 1, maximum: Math.min(config.maxTaskTimeoutSeconds, config.directSessionTtlSeconds), default: 120 },
1168
+ },
1169
+ required: ["session_id", "commands"],
1170
+ },
1171
+ });
1172
+
911
1173
  tools.push({
912
1174
  name: "finalize_direct_session",
913
1175
  description:
@@ -993,7 +1255,7 @@ export function getToolDefs(): ToolDef[] {
993
1255
  });
994
1256
 
995
1257
  // run_task: only available when explicitly enabled
996
- if ((config as any).enableRunTaskTool === true) {
1258
+ if (config.enableRunTaskTool === true) {
997
1259
  tools.push({
998
1260
  name: "run_task",
999
1261
  description:
@@ -1020,21 +1282,32 @@ export function getToolCatalogSnapshot(): ToolCatalogSnapshot {
1020
1282
  return buildToolCatalogSnapshot(tools, resolveToolProfile(config.toolProfile));
1021
1283
  }
1022
1284
 
1023
- // ── Request handler ───────────────────────────────────────────────
1285
+ // ── Dispatch map ─────────────────────────────────────────────────
1024
1286
 
1025
- function guardDirectProfileEnabled(): void {
1287
+ /**
1288
+ * Combined dispatch map assembled from all domain handler maps.
1289
+ * run_task is conditionally added when enableRunTaskTool is true,
1290
+ * mirroring the conditional registration in getToolDefs().
1291
+ */
1292
+ function buildDispatchMap(): ToolHandlerMap {
1026
1293
  const config = getConfig();
1027
- if (!config.enableDirectProfile) {
1028
- throw new PatchWardenError(
1029
- "direct_profile_disabled",
1030
- "Direct profile is disabled by local config.",
1031
- "Set enableDirectProfile: true in patchwarden.config.json to use Direct session tools.",
1032
- true,
1033
- { operation: "direct_tool_call" }
1034
- );
1294
+ const map: ToolHandlerMap = {
1295
+ ...coreHandlers,
1296
+ ...goalHandlers,
1297
+ ...directHandlers,
1298
+ ...releaseHandlers,
1299
+ ...diagnosticHandlers,
1300
+ };
1301
+ if (config.enableRunTaskTool === true) {
1302
+ map.run_task = runTaskHandler;
1035
1303
  }
1304
+ return map;
1036
1305
  }
1037
1306
 
1307
+ const dispatchMap: ToolHandlerMap = buildDispatchMap();
1308
+
1309
+ // ── Request handler ───────────────────────────────────────────────
1310
+
1038
1311
  export async function handleToolCall(name: string, args: Record<string, unknown> | undefined) {
1039
1312
  const startTime = Date.now();
1040
1313
  const taskId = args?.task_id ? String(args.task_id) : args?.session_id ? String(args.session_id) : undefined;
@@ -1050,453 +1323,11 @@ export async function handleToolCall(name: string, args: Record<string, unknown>
1050
1323
  }
1051
1324
 
1052
1325
  async function handleToolCallInternal(name: string, args: Record<string, unknown> | undefined) {
1053
- switch (name) {
1054
- case "save_plan": {
1055
- return toResult(
1056
- savePlan({
1057
- title: String(args?.title ?? ""),
1058
- content: args?.content !== undefined ? String(args.content) : "",
1059
- plan_ref: args?.plan_ref ? String(args.plan_ref) : undefined,
1060
- })
1061
- );
1062
- }
1063
-
1064
- case "get_plan": {
1065
- return toResult(
1066
- getPlan({ plan_id: String(args?.plan_id ?? "") })
1067
- );
1068
- }
1069
-
1070
- case "create_task": {
1071
- return toResult(
1072
- createTask({
1073
- plan_id: args?.plan_id ? String(args.plan_id) : undefined,
1074
- inline_plan: args?.inline_plan ? String(args.inline_plan) : undefined,
1075
- plan_title: args?.plan_title ? String(args.plan_title) : undefined,
1076
- template: args?.template ? String(args.template) as any : undefined,
1077
- goal: args?.goal ? String(args.goal) : undefined,
1078
- source_task_id: args?.source_task_id ? String(args.source_task_id) : undefined,
1079
- agent: String(args?.agent ?? ""),
1080
- repo_path: args?.repo_path ? String(args.repo_path) : undefined,
1081
- test_command: args?.test_command ? String(args.test_command) : undefined,
1082
- verify_commands: Array.isArray(args?.verify_commands)
1083
- ? args.verify_commands.map((command) => String(command))
1084
- : undefined,
1085
- timeout_seconds: args?.timeout_seconds !== undefined
1086
- ? Number(args.timeout_seconds)
1087
- : undefined,
1088
- execution_mode: args?.execution_mode === "assess_only" ? "assess_only" : "execute",
1089
- assessment_id: args?.assessment_id ? String(args.assessment_id) : undefined,
1090
- })
1091
- );
1092
- }
1093
-
1094
- case "get_task_status": {
1095
- return toResult(getTaskStatus(String(args?.task_id ?? "")));
1096
- }
1097
-
1098
- case "get_result": {
1099
- return toResult(getResult(String(args?.task_id ?? "")));
1100
- }
1101
-
1102
- case "get_result_json": {
1103
- return toResult(getResultJson(String(args?.task_id ?? "")));
1104
- }
1105
-
1106
- case "get_diff": {
1107
- return toResult(getDiff(String(args?.task_id ?? "")));
1108
- }
1109
-
1110
- case "get_test_log": {
1111
- return toResult(getTestLog(String(args?.task_id ?? "")));
1112
- }
1113
-
1114
- case "list_workspace": {
1115
- return toResult(
1116
- listWorkspace(args?.path ? String(args.path) : undefined)
1117
- );
1118
- }
1119
-
1120
- case "read_workspace_file": {
1121
- const sessionId = args?.session_id ? String(args.session_id) : undefined;
1122
- return toResult(readWorkspaceFile({
1123
- path: String(args?.path ?? ""),
1124
- session_id: sessionId,
1125
- }));
1126
- }
1127
-
1128
- case "list_tasks": {
1129
- return toResult(listTasks({
1130
- status: args?.status ? String(args.status) : undefined,
1131
- repo_path: args?.repo_path ? String(args.repo_path) : undefined,
1132
- active_only: args?.active_only !== undefined ? Boolean(args.active_only) : undefined,
1133
- limit: args?.limit ? Number(args.limit) : undefined,
1134
- }));
1135
- }
1136
-
1137
- case "list_agents": {
1138
- return toResult(listAgents());
1139
- }
1140
-
1141
- case "health_check": {
1142
- return toResult(healthCheck(getToolCatalogSnapshot(), {
1143
- detail: args?.detail === "self_diagnostic" ? "self_diagnostic" : "standard",
1144
- }));
1145
- }
1146
-
1147
- case "cancel_task": {
1148
- return toResult(cancelTask(String(args?.task_id ?? "")));
1149
- }
1150
-
1151
- case "kill_task": {
1152
- return toResult(killTask(String(args?.task_id ?? "")));
1153
- }
1154
-
1155
- case "retry_task": {
1156
- return toResult(retryTask(String(args?.task_id ?? "")));
1157
- }
1158
-
1159
- case "get_task_stdout_tail": {
1160
- return toResult(getTaskStdoutTail(
1161
- String(args?.task_id ?? ""),
1162
- args?.lines ? Number(args.lines) : undefined
1163
- ));
1164
- }
1165
-
1166
- case "get_task_log_tail": {
1167
- return toResult(getTaskLogTail(
1168
- String(args?.task_id ?? ""),
1169
- (args?.file as "stdout" | "stderr" | "test" | "verify") || "stdout",
1170
- {
1171
- lines: args?.lines ? Number(args.lines) : undefined,
1172
- redact: args?.redact !== undefined ? Boolean(args.redact) : undefined,
1173
- }
1174
- ));
1175
- }
1176
-
1177
- case "get_task_progress": {
1178
- return toResult(getTaskProgress(String(args?.task_id ?? "")));
1179
- }
1180
-
1181
- case "wait_for_task": {
1182
- const waitSeconds = normalizeWaitSeconds(args);
1183
- return toResult(await waitForTask(
1184
- String(args?.task_id ?? ""),
1185
- waitSeconds
1186
- ));
1187
- }
1188
-
1189
- case "get_task_summary": {
1190
- return toResult(getTaskSummary(String(args?.task_id ?? ""), {
1191
- view: normalizeSummaryView(args?.view),
1192
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1193
- }));
1194
- }
1195
-
1196
- case "audit_task": {
1197
- return toResult(auditTask(String(args?.task_id ?? "")));
1198
- }
1199
-
1200
- case "safe_status": {
1201
- return toResult(safeStatus(String(args?.task_id ?? "")));
1202
- }
1203
-
1204
- case "safe_result": {
1205
- return toResult(safeResult(String(args?.task_id ?? ""), {
1206
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1207
- }));
1208
- }
1209
-
1210
- case "safe_audit": {
1211
- return toResult(safeAudit(String(args?.task_id ?? ""), {
1212
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1213
- }));
1214
- }
1215
-
1216
- case "safe_test_summary": {
1217
- return toResult(safeTestSummary(String(args?.task_id ?? "")));
1218
- }
1219
-
1220
- case "safe_diff_summary": {
1221
- return toResult(safeDiffSummary(String(args?.task_id ?? ""), {
1222
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1223
- }));
1224
- }
1225
- case "diagnose_task": {
1226
- return toResult(diagnoseTask({
1227
- task_id: String(args?.task_id ?? ""),
1228
- include_logs: args?.include_logs !== undefined ? Boolean(args.include_logs) : undefined,
1229
- }));
1230
- }
1231
-
1232
- case "reconcile_tasks": {
1233
- return toResult(reconcileTasks({
1234
- mode: args?.mode === "safe_fix" ? "safe_fix" : "report_only",
1235
- max_age_minutes: args?.max_age_minutes !== undefined ? Number(args.max_age_minutes) : undefined,
1236
- include_done_candidates: args?.include_done_candidates !== undefined ? Boolean(args.include_done_candidates) : undefined,
1237
- }));
1238
- }
1239
-
1240
- case "discover_tools": {
1241
- const profile = args?.profile === "full" || args?.profile === "chatgpt_core" || args?.profile === "chatgpt_direct" || args?.profile === "chatgpt_search"
1242
- ? args.profile : undefined;
1243
- const mode = args?.mode === "delegate" || args?.mode === "direct" || args?.mode === "audit" || args?.mode === "release" || args?.mode === "diagnostic"
1244
- ? args.mode : undefined;
1245
- const riskCeiling = ["readonly", "workspace_read_sensitive", "workspace_write", "command", "release", "credential_sensitive"]
1246
- .includes(String(args?.riskCeiling ?? "")) ? String(args?.riskCeiling) as any : undefined;
1247
- return toResult(discoverTools({
1248
- query: String(args?.query ?? ""),
1249
- profile,
1250
- mode,
1251
- maxResults: args?.maxResults !== undefined ? Number(args.maxResults) : undefined,
1252
- riskCeiling,
1253
- includeHighRisk: args?.includeHighRisk !== undefined ? Boolean(args.includeHighRisk) : undefined,
1254
- }, getToolDefs()));
1255
- }
1256
-
1257
- case "explain_tool": {
1258
- return toResult(explainTool({
1259
- name: String(args?.name ?? ""),
1260
- includeSchema: args?.includeSchema !== undefined ? Boolean(args.includeSchema) : undefined,
1261
- }, getToolDefs()));
1262
- }
1263
-
1264
- case "invoke_discovered_tool": {
1265
- const profile = resolveToolProfile(getConfig().toolProfile);
1266
- const result = await invokeDiscoveredTool({
1267
- toolName: String(args?.toolName ?? ""),
1268
- arguments: (args?.arguments && typeof args.arguments === "object" ? args.arguments : {}) as Record<string, unknown>,
1269
- discoveryToken: String(args?.discoveryToken ?? ""),
1270
- assessmentId: args?.assessmentId ? String(args.assessmentId) : undefined,
1271
- }, {
1272
- tools: getToolDefs(),
1273
- profile,
1274
- dispatch: async (name, dispatchArgs) => {
1275
- return handleToolCall(name, dispatchArgs);
1276
- },
1277
- });
1278
- return toResult(result);
1279
- }
1280
-
1281
- case "run_task": {
1282
- const config = getConfig();
1283
- if ((config as any).enableRunTaskTool !== true) {
1284
- throw new Error(
1285
- "run_task is disabled. Set enableRunTaskTool: true in config to enable. Prefer using the local watcher (npm run watch)."
1286
- );
1287
- }
1288
- const taskId = String(args?.task_id ?? "");
1289
- const result = await runTask(taskId);
1290
- return toResult(result);
1291
- }
1292
-
1293
- case "create_direct_session": {
1294
- guardDirectProfileEnabled();
1295
- return toResult(createDirectSession({
1296
- repo_path: String(args?.repo_path ?? ""),
1297
- title: args?.title ? String(args.title) : undefined,
1298
- }));
1299
- }
1300
-
1301
- case "search_workspace": {
1302
- guardDirectProfileEnabled();
1303
- return toResult(searchWorkspace({
1304
- session_id: String(args?.session_id ?? ""),
1305
- query: String(args?.query ?? ""),
1306
- max_results: args?.max_results ? Number(args.max_results) : undefined,
1307
- case_sensitive: args?.case_sensitive !== undefined ? Boolean(args.case_sensitive) : undefined,
1308
- max_preview_chars: args?.max_preview_chars ? Number(args.max_preview_chars) : undefined,
1309
- include_globs: Array.isArray(args?.include_globs) ? args.include_globs.map(String) : undefined,
1310
- }));
1311
- }
1312
-
1313
- case "apply_patch": {
1314
- guardDirectProfileEnabled();
1315
- return toResult(applyPatch({
1316
- session_id: String(args?.session_id ?? ""),
1317
- path: String(args?.path ?? ""),
1318
- expected_sha256: String(args?.expected_sha256 ?? ""),
1319
- operations: Array.isArray(args?.operations) ? args.operations as any : [],
1320
- }));
1321
- }
1322
-
1323
- case "run_verification": {
1324
- guardDirectProfileEnabled();
1325
- return toResult(await runVerification({
1326
- session_id: String(args?.session_id ?? ""),
1327
- command: String(args?.command ?? ""),
1328
- timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
1329
- }));
1330
- }
1331
-
1332
- case "finalize_direct_session": {
1333
- guardDirectProfileEnabled();
1334
- return toResult(finalizeDirectSession({
1335
- session_id: String(args?.session_id ?? ""),
1336
- }));
1337
- }
1338
-
1339
- case "audit_session": {
1340
- guardDirectProfileEnabled();
1341
- return toResult(auditSession({
1342
- session_id: String(args?.session_id ?? ""),
1343
- }));
1344
- }
1345
-
1346
- case "safe_direct_summary": {
1347
- guardDirectProfileEnabled();
1348
- return toResult(safeDirectSummary(String(args?.session_id ?? ""), {
1349
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1350
- }));
1351
- }
1352
-
1353
- case "safe_finalize_direct_session": {
1354
- guardDirectProfileEnabled();
1355
- return toResult(safeFinalizeDirectSession(String(args?.session_id ?? ""), {
1356
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1357
- }));
1358
- }
1359
-
1360
- case "safe_audit_direct_session": {
1361
- guardDirectProfileEnabled();
1362
- return toResult(safeAuditDirectSession(String(args?.session_id ?? ""), {
1363
- max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
1364
- }));
1365
- }
1366
- case "sync_file": {
1367
- guardDirectProfileEnabled();
1368
- return toResult(syncFile(
1369
- String(args?.session_id ?? ""),
1370
- String(args?.source_path ?? ""),
1371
- String(args?.target_path ?? ""),
1372
- {
1373
- expected_source_sha256: args?.expected_source_sha256 ? String(args.expected_source_sha256) : undefined,
1374
- expected_target_sha256: args?.expected_target_sha256 ? String(args.expected_target_sha256) : undefined,
1375
- }
1376
- ));
1377
- }
1378
-
1379
- case "create_goal": {
1380
- return toResult(createGoal(
1381
- String(args?.repo_path ?? ""),
1382
- String(args?.title ?? ""),
1383
- String(args?.goal_description ?? "")
1384
- ));
1385
- }
1386
-
1387
- case "list_goals": {
1388
- return toResult({ goals: listGoals() });
1389
- }
1390
-
1391
- case "read_goal": {
1392
- return toResult(readGoal(String(args?.goal_id ?? "")));
1393
- }
1394
-
1395
- case "create_subgoal_task": {
1396
- return toResult(createSubgoalTask({
1397
- goal_id: String(args?.goal_id ?? ""),
1398
- subgoal_title: String(args?.subgoal_title ?? ""),
1399
- depends_on: Array.isArray(args?.depends_on) ? args.depends_on.map(String) : undefined,
1400
- plan_id: args?.plan_id ? String(args.plan_id) : undefined,
1401
- inline_plan: args?.inline_plan ? String(args.inline_plan) : undefined,
1402
- plan_title: args?.plan_title ? String(args.plan_title) : undefined,
1403
- template: args?.template ? String(args.template) as any : undefined,
1404
- goal: args?.goal ? String(args.goal) : undefined,
1405
- agent: args?.agent ? String(args.agent) : undefined,
1406
- repo_path: String(args?.repo_path ?? ""),
1407
- test_command: args?.test_command ? String(args.test_command) : undefined,
1408
- verify_commands: Array.isArray(args?.verify_commands) ? args.verify_commands.map(String) : undefined,
1409
- timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
1410
- scope: Array.isArray(args?.scope) ? args.scope.map(String) : undefined,
1411
- forbidden: Array.isArray(args?.forbidden) ? args.forbidden.map(String) : undefined,
1412
- verification: Array.isArray(args?.verification) ? args.verification.map(String) : undefined,
1413
- done_evidence: Array.isArray(args?.done_evidence) ? args.done_evidence.map(String) : undefined,
1414
- isolate_worktree: args?.isolate_worktree === undefined ? undefined : Boolean(args.isolate_worktree),
1415
- }));
1416
- }
1417
-
1418
- case "accept_subgoal": {
1419
- return toResult(acceptSubgoal(
1420
- String(args?.goal_id ?? ""),
1421
- String(args?.subgoal_id ?? "")
1422
- ));
1423
- }
1424
-
1425
- case "reject_subgoal": {
1426
- return toResult(rejectSubgoal(
1427
- String(args?.goal_id ?? ""),
1428
- String(args?.subgoal_id ?? ""),
1429
- String(args?.reason ?? "")
1430
- ));
1431
- }
1432
-
1433
- case "suggest_next_subgoal": {
1434
- const goalStatus = readGoalStatus(String(args?.goal_id ?? ""));
1435
- return toResult(suggestNextSubgoal(goalStatus));
1436
- }
1437
-
1438
- case "summarize_goal_progress": {
1439
- return toResult(summarizeGoalProgress(String(args?.goal_id ?? "")));
1440
- }
1441
-
1442
- case "export_handoff": {
1443
- const goalId = String(args?.goal_id ?? "");
1444
- const goalStatus = readGoalStatus(goalId);
1445
- return toResult(exportHandoff(goalId, goalStatus));
1446
- }
1447
-
1448
- case "check_release_gate": {
1449
- return toResult(await checkReleaseGate({
1450
- repo_path: String(args?.repo_path ?? ""),
1451
- target_stage: String(args?.target_stage ?? "local_ready") as any,
1452
- package_name: args?.package_name ? String(args.package_name) : undefined,
1453
- version: args?.version ? String(args.version) : undefined,
1454
- github_repo: args?.github_repo ? String(args.github_repo) : undefined,
1455
- branch: args?.branch ? String(args.branch) : undefined,
1456
- }));
1457
- }
1458
-
1459
- case "merge_worktree": {
1460
- return toResult(mergeWorktreeTool({
1461
- worktree_id: String(args?.worktree_id ?? ""),
1462
- repo_path: String(args?.repo_path ?? ""),
1463
- }));
1464
- }
1465
-
1466
- case "discard_worktree": {
1467
- return toResult(discardWorktreeTool({
1468
- worktree_id: String(args?.worktree_id ?? ""),
1469
- repo_path: String(args?.repo_path ?? ""),
1470
- }));
1471
- }
1472
-
1473
- default:
1474
- throw new Error(`Unknown tool: ${name}`);
1475
- }
1476
- }
1477
-
1478
- function normalizeWaitSeconds(args: Record<string, unknown> | undefined): number | undefined {
1479
- const legacy = args?.wait_seconds;
1480
- const preferred = args?.timeout_seconds;
1481
- if (legacy !== undefined && preferred !== undefined && Number(legacy) !== Number(preferred)) {
1482
- throw new Error("wait_seconds and timeout_seconds must match when both are supplied.");
1326
+ const handler = dispatchMap[name];
1327
+ if (!handler) {
1328
+ throw new Error(`Unknown tool: ${name}`);
1483
1329
  }
1484
- const value = preferred ?? legacy;
1485
- return value === undefined ? undefined : Number(value);
1486
- }
1487
-
1488
- function normalizeSummaryView(value: unknown): "compact" | "standard" {
1489
- if (value === undefined) return "standard";
1490
- if (value !== "compact" && value !== "standard") {
1491
- throw new Error('view must be "compact" or "standard".');
1492
- }
1493
- return value;
1494
- }
1495
-
1496
- function toResult(data: unknown) {
1497
- return {
1498
- content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
1499
- };
1330
+ return handler(args);
1500
1331
  }
1501
1332
 
1502
1333
  // ── Register on MCP Server ────────────────────────────────────────
@@ -1508,6 +1339,18 @@ export function registerTools(server: Server) {
1508
1339
  const activeTools = getToolDefs();
1509
1340
  const activeNames = new Set(activeTools.map((tool) => tool.name));
1510
1341
 
1342
+ // Verify every registered tool has a dispatch handler.
1343
+ // The dispatch map may contain extra handlers for tools exposed under
1344
+ // other profiles (e.g. get_plan is only in the "full" profile), so we
1345
+ // only check the subset direction: every active tool must have a handler.
1346
+ for (const tool of activeTools) {
1347
+ if (!dispatchMap[tool.name]) {
1348
+ throw new Error(
1349
+ `Dispatch map is missing handler for registered tool "${tool.name}".`,
1350
+ );
1351
+ }
1352
+ }
1353
+
1511
1354
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1512
1355
  const catalog = getLastToolCatalogSnapshot();
1513
1356
  return {