patchwarden 0.6.4 โ†’ 1.1.0

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 (212) hide show
  1. package/README.en.md +18 -18
  2. package/README.md +20 -20
  3. package/dist/agents/agentRouter.d.ts +40 -0
  4. package/dist/agents/agentRouter.js +95 -0
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -2
  7. package/dist/controlCenter.js +15 -2
  8. package/dist/doctor.js +32 -1
  9. package/dist/goal/acceptanceEngine.d.ts +62 -0
  10. package/dist/goal/acceptanceEngine.js +103 -0
  11. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  12. package/dist/goal/acceptanceTemplate.js +104 -0
  13. package/dist/goal/goalGraph.d.ts +58 -0
  14. package/dist/goal/goalGraph.js +189 -0
  15. package/dist/goal/goalProgress.d.ts +81 -0
  16. package/dist/goal/goalProgress.js +216 -0
  17. package/dist/goal/goalStatus.d.ts +60 -0
  18. package/dist/goal/goalStatus.js +137 -0
  19. package/dist/goal/goalStore.d.ts +79 -0
  20. package/dist/goal/goalStore.js +211 -0
  21. package/dist/goal/handoffExport.d.ts +34 -0
  22. package/dist/goal/handoffExport.js +183 -0
  23. package/dist/goal/subgoalSync.d.ts +40 -0
  24. package/dist/goal/subgoalSync.js +72 -0
  25. package/dist/goal/worktreeManager.d.ts +96 -0
  26. package/dist/goal/worktreeManager.js +292 -0
  27. package/dist/logging.d.ts +44 -0
  28. package/dist/logging.js +68 -0
  29. package/dist/release/releaseGate.d.ts +99 -0
  30. package/dist/release/releaseGate.js +475 -0
  31. package/dist/runner/postTaskCleanup.d.ts +13 -0
  32. package/dist/runner/postTaskCleanup.js +147 -0
  33. package/dist/runner/runTask.js +50 -10
  34. package/dist/security/discoveryTokenStore.d.ts +62 -0
  35. package/dist/security/discoveryTokenStore.js +100 -0
  36. package/dist/security/toolInvocationGuard.d.ts +35 -0
  37. package/dist/security/toolInvocationGuard.js +153 -0
  38. package/dist/smoke-test.js +18 -10
  39. package/dist/taskRuntime.d.ts +17 -0
  40. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  41. package/dist/test/unit/acceptance-engine.test.js +228 -0
  42. package/dist/test/unit/agent-router.test.d.ts +1 -0
  43. package/dist/test/unit/agent-router.test.js +287 -0
  44. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  45. package/dist/test/unit/audit-checks.test.js +350 -0
  46. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  47. package/dist/test/unit/diagnose-task.test.js +457 -0
  48. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  49. package/dist/test/unit/discovery-token-store.test.js +139 -0
  50. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  51. package/dist/test/unit/goal-graph.test.js +298 -0
  52. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  53. package/dist/test/unit/goal-progress.test.js +381 -0
  54. package/dist/test/unit/goal-status.test.d.ts +1 -0
  55. package/dist/test/unit/goal-status.test.js +215 -0
  56. package/dist/test/unit/goal-store.test.d.ts +1 -0
  57. package/dist/test/unit/goal-store.test.js +253 -0
  58. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  59. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  60. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  61. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  62. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  63. package/dist/test/unit/handoff-export.test.js +263 -0
  64. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  65. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  66. package/dist/test/unit/logging.test.js +127 -5
  67. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  68. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  69. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  70. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  71. package/dist/test/unit/release-gate.test.d.ts +1 -0
  72. package/dist/test/unit/release-gate.test.js +242 -0
  73. package/dist/test/unit/safe-views.test.d.ts +1 -0
  74. package/dist/test/unit/safe-views.test.js +171 -0
  75. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  76. package/dist/test/unit/schema-drift-check.test.js +175 -0
  77. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  78. package/dist/test/unit/subgoal-sync.test.js +183 -0
  79. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  80. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  81. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  82. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  83. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  84. package/dist/test/unit/toolSearch.test.js +571 -0
  85. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  86. package/dist/test/unit/worktree-manager.test.js +176 -0
  87. package/dist/tools/auditTask.d.ts +103 -1
  88. package/dist/tools/auditTask.js +461 -8
  89. package/dist/tools/cancelTask.js +1 -1
  90. package/dist/tools/checkReleaseGate.d.ts +21 -0
  91. package/dist/tools/checkReleaseGate.js +22 -0
  92. package/dist/tools/createTask.d.ts +18 -2
  93. package/dist/tools/createTask.js +29 -1
  94. package/dist/tools/diagnoseTask.d.ts +45 -0
  95. package/dist/tools/diagnoseTask.js +347 -0
  96. package/dist/tools/discardWorktree.d.ts +24 -0
  97. package/dist/tools/discardWorktree.js +27 -0
  98. package/dist/tools/discoverTools.d.ts +11 -0
  99. package/dist/tools/discoverTools.js +39 -0
  100. package/dist/tools/explainTool.d.ts +11 -0
  101. package/dist/tools/explainTool.js +21 -0
  102. package/dist/tools/getTaskSummary.js +2 -1
  103. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  104. package/dist/tools/goalSubgoalTask.js +110 -0
  105. package/dist/tools/healthCheck.d.ts +1 -0
  106. package/dist/tools/healthCheck.js +5 -1
  107. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  108. package/dist/tools/invokeDiscoveredTool.js +112 -0
  109. package/dist/tools/listTasks.d.ts +3 -1
  110. package/dist/tools/listTasks.js +14 -1
  111. package/dist/tools/mergeWorktree.d.ts +24 -0
  112. package/dist/tools/mergeWorktree.js +27 -0
  113. package/dist/tools/reconcileTasks.d.ts +41 -0
  114. package/dist/tools/reconcileTasks.js +352 -0
  115. package/dist/tools/registry.js +550 -0
  116. package/dist/tools/safeStatus.d.ts +31 -1
  117. package/dist/tools/safeStatus.js +43 -1
  118. package/dist/tools/safeViews.d.ts +256 -0
  119. package/dist/tools/safeViews.js +250 -0
  120. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  121. package/dist/tools/schemaDriftCheck.js +80 -0
  122. package/dist/tools/toolCatalog.d.ts +4 -3
  123. package/dist/tools/toolCatalog.js +33 -11
  124. package/dist/tools/toolRegistry.d.ts +61 -0
  125. package/dist/tools/toolRegistry.js +724 -0
  126. package/dist/tools/toolSearch.d.ts +110 -0
  127. package/dist/tools/toolSearch.js +331 -0
  128. package/dist/tools/toolUsageStats.d.ts +41 -0
  129. package/dist/tools/toolUsageStats.js +116 -0
  130. package/dist/tools/waitForTask.js +1 -0
  131. package/dist/version.d.ts +2 -2
  132. package/dist/version.js +2 -2
  133. package/dist/watcherStatus.d.ts +29 -0
  134. package/dist/watcherStatus.js +92 -1
  135. package/package.json +3 -2
  136. package/scripts/checks/control-smoke.js +2 -2
  137. package/scripts/checks/doctor-smoke.js +1 -0
  138. package/scripts/checks/lifecycle-smoke.js +13 -9
  139. package/scripts/checks/mcp-manifest-check.js +53 -6
  140. package/scripts/checks/mcp-smoke.js +36 -9
  141. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  142. package/src/agents/agentRouter.ts +149 -0
  143. package/src/config.ts +9 -3
  144. package/src/controlCenter.ts +17 -3
  145. package/src/doctor.ts +36 -1
  146. package/src/goal/acceptanceEngine.ts +160 -0
  147. package/src/goal/acceptanceTemplate.ts +121 -0
  148. package/src/goal/goalGraph.ts +225 -0
  149. package/src/goal/goalProgress.ts +301 -0
  150. package/src/goal/goalStatus.ts +234 -0
  151. package/src/goal/goalStore.ts +306 -0
  152. package/src/goal/handoffExport.ts +211 -0
  153. package/src/goal/subgoalSync.ts +82 -0
  154. package/src/goal/worktreeManager.ts +404 -0
  155. package/src/logging.ts +91 -0
  156. package/src/release/releaseGate.ts +567 -0
  157. package/src/runner/postTaskCleanup.ts +154 -0
  158. package/src/runner/runTask.ts +49 -10
  159. package/src/security/discoveryTokenStore.ts +157 -0
  160. package/src/security/toolInvocationGuard.ts +251 -0
  161. package/src/smoke-test.ts +15 -7
  162. package/src/taskRuntime.ts +17 -0
  163. package/src/test/unit/acceptance-engine.test.ts +261 -0
  164. package/src/test/unit/agent-router.test.ts +342 -0
  165. package/src/test/unit/audit-checks.test.ts +567 -0
  166. package/src/test/unit/diagnose-task.test.ts +544 -0
  167. package/src/test/unit/discovery-token-store.test.ts +181 -0
  168. package/src/test/unit/goal-graph.test.ts +347 -0
  169. package/src/test/unit/goal-progress.test.ts +538 -0
  170. package/src/test/unit/goal-status.test.ts +270 -0
  171. package/src/test/unit/goal-store.test.ts +318 -0
  172. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  173. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  174. package/src/test/unit/handoff-export.test.ts +295 -0
  175. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  176. package/src/test/unit/logging.test.ts +177 -5
  177. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  178. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  179. package/src/test/unit/release-gate.test.ts +314 -0
  180. package/src/test/unit/safe-views.test.ts +184 -0
  181. package/src/test/unit/schema-drift-check.test.ts +258 -0
  182. package/src/test/unit/subgoal-sync.test.ts +236 -0
  183. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  184. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  185. package/src/test/unit/toolSearch.test.ts +756 -0
  186. package/src/test/unit/worktree-manager.test.ts +247 -0
  187. package/src/tools/auditTask.ts +831 -402
  188. package/src/tools/cancelTask.ts +1 -1
  189. package/src/tools/checkReleaseGate.ts +45 -0
  190. package/src/tools/createTask.ts +64 -6
  191. package/src/tools/diagnoseTask.ts +460 -0
  192. package/src/tools/discardWorktree.ts +42 -0
  193. package/src/tools/discoverTools.ts +51 -0
  194. package/src/tools/explainTool.ts +26 -0
  195. package/src/tools/getTaskSummary.ts +2 -1
  196. package/src/tools/goalSubgoalTask.ts +170 -0
  197. package/src/tools/healthCheck.ts +4 -1
  198. package/src/tools/invokeDiscoveredTool.ts +163 -0
  199. package/src/tools/listTasks.ts +16 -2
  200. package/src/tools/mergeWorktree.ts +42 -0
  201. package/src/tools/reconcileTasks.ts +464 -0
  202. package/src/tools/registry.ts +618 -1
  203. package/src/tools/safeStatus.ts +73 -2
  204. package/src/tools/safeViews.ts +271 -0
  205. package/src/tools/schemaDriftCheck.ts +120 -0
  206. package/src/tools/toolCatalog.ts +34 -11
  207. package/src/tools/toolRegistry.ts +786 -0
  208. package/src/tools/toolSearch.ts +464 -0
  209. package/src/tools/toolUsageStats.ts +151 -0
  210. package/src/tools/waitForTask.ts +1 -0
  211. package/src/version.ts +2 -2
  212. package/src/watcherStatus.ts +97 -1
@@ -157,7 +157,7 @@ export function derivePendingReason(task, watcher) {
157
157
  return "queued_but_watcher_unreadable";
158
158
  return "queued_waiting_for_watcher";
159
159
  }
160
- if (task.status !== "running")
160
+ if (task.status !== "running" && task.status !== "collecting_artifacts")
161
161
  return null;
162
162
  if (task.phase === "executing_agent")
163
163
  return "agent_running";
@@ -167,3 +167,94 @@ export function derivePendingReason(task, watcher) {
167
167
  return "collecting_artifacts";
168
168
  return "preparing";
169
169
  }
170
+ // โ”€โ”€ v0.7.0: Watcher ownership helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
171
+ /**
172
+ * Read the current watcher's instance_id from the heartbeat file.
173
+ * Returns null when the heartbeat is missing or unreadable.
174
+ *
175
+ * This is the source of truth for "which watcher is currently alive".
176
+ * Tasks whose runtime.watcher_instance_id differs from this value are
177
+ * considered orphaned by the diagnosis layer.
178
+ */
179
+ export function readWatcherInstanceId(config = getConfig()) {
180
+ const heartbeatPath = getWatcherHeartbeatPath(config);
181
+ if (!existsSync(heartbeatPath))
182
+ return null;
183
+ try {
184
+ const raw = readFileSync(heartbeatPath, "utf-8");
185
+ const data = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
186
+ return typeof data.instance_id === "string" ? data.instance_id : null;
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ }
192
+ /**
193
+ * v0.7.0: Determine whether the current watcher still owns a task.
194
+ *
195
+ * Ownership is established by comparing the watcher_instance_id recorded in
196
+ * the task's runtime.json with the instance_id of the currently alive
197
+ * watcher. A task is considered "owned" only when:
198
+ * - the current watcher heartbeat is healthy, AND
199
+ * - the task runtime recorded a watcher_instance_id, AND
200
+ * - both instance IDs match.
201
+ *
202
+ * If the task has no recorded watcher_instance_id (legacy tasks created
203
+ * before v0.7.0), ownership cannot be confirmed and this returns false.
204
+ * The diagnosis layer treats "no owner" as a stale signal, not as active.
205
+ */
206
+ export function isWatcherOwningTask(taskDir, config = getConfig()) {
207
+ const runtimePath = join(taskDir, "runtime.json");
208
+ let taskWatcherInstanceId = null;
209
+ if (existsSync(runtimePath)) {
210
+ try {
211
+ const runtime = JSON.parse(readFileSync(runtimePath, "utf-8"));
212
+ if (typeof runtime.watcher_instance_id === "string") {
213
+ taskWatcherInstanceId = runtime.watcher_instance_id;
214
+ }
215
+ }
216
+ catch {
217
+ // corrupted runtime โ€” treat as no record
218
+ }
219
+ }
220
+ if (!taskWatcherInstanceId) {
221
+ return {
222
+ owned: false,
223
+ reason: "no_runtime_record",
224
+ task_watcher_instance_id: null,
225
+ current_watcher_instance_id: null,
226
+ };
227
+ }
228
+ const watcher = readWatcherStatus(config);
229
+ if (watcher.status !== "healthy") {
230
+ return {
231
+ owned: false,
232
+ reason: watcher.status === "missing" ? "watcher_missing" : "watcher_unhealthy",
233
+ task_watcher_instance_id: taskWatcherInstanceId,
234
+ current_watcher_instance_id: watcher.instance_id,
235
+ };
236
+ }
237
+ const currentInstanceId = watcher.instance_id;
238
+ if (!currentInstanceId) {
239
+ return {
240
+ owned: false,
241
+ reason: "watcher_missing",
242
+ task_watcher_instance_id: taskWatcherInstanceId,
243
+ current_watcher_instance_id: null,
244
+ };
245
+ }
246
+ if (currentInstanceId !== taskWatcherInstanceId) {
247
+ return {
248
+ owned: false,
249
+ reason: "instance_mismatch",
250
+ task_watcher_instance_id: taskWatcherInstanceId,
251
+ current_watcher_instance_id: currentInstanceId,
252
+ };
253
+ }
254
+ return {
255
+ owned: true,
256
+ reason: "owned",
257
+ task_watcher_instance_id: taskWatcherInstanceId,
258
+ current_watcher_instance_id: currentInstanceId,
259
+ };
260
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchwarden",
3
- "version": "0.6.4",
4
- "description": "A secure local MCP bridge: clients save plans, local agents execute tasks, and results are returned through workspace-scoped files.",
3
+ "version": "1.1.0",
4
+ "description": "Secure local MCP bridge for ChatGPT and local agents, with workspace-scoped tasks, safe summaries, audits, and Direct editing.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -50,6 +50,7 @@
50
50
  "test:watcher-supervisor": "npm run build && node scripts/checks/watcher-supervisor-smoke.js",
51
51
  "check:tool-manifest": "npm run build && node scripts/checks/mcp-manifest-check.js",
52
52
  "check:direct-tool-manifest": "npm run build && node scripts/checks/mcp-manifest-check.js --profile chatgpt_direct",
53
+ "check:search-tool-manifest": "npm run build && node scripts/checks/mcp-manifest-check.js --profile chatgpt_search",
53
54
  "check:brand": "node scripts/checks/brand-check.js",
54
55
  "test:mcp": "npm run build && node scripts/checks/mcp-smoke.js",
55
56
  "test:http-mcp": "npm run build && node scripts/checks/http-mcp-smoke.js",
@@ -76,7 +76,7 @@ try {
76
76
  ready: false,
77
77
  pid: null,
78
78
  tool_profile: "chatgpt_direct",
79
- tool_count: 9,
79
+ tool_count: 13,
80
80
  tools_ready: true,
81
81
  }), "utf8");
82
82
  writeFileSync(join(directRuntime, "tunnel-client.pid"), String(fakeTunnel.pid), "utf8");
@@ -114,7 +114,7 @@ try {
114
114
  reason_code: "stale_fixture",
115
115
  last_error: "stale failure",
116
116
  tool_profile: "chatgpt_core",
117
- tool_count: 16,
117
+ tool_count: 21,
118
118
  tools_ready: true,
119
119
  }), "utf8");
120
120
  healthServer = spawn(
@@ -42,6 +42,7 @@ try {
42
42
  "[OK] Task directory writable",
43
43
  "[OK] Example task directory read/write",
44
44
  "[OK] allowedTestCommands is non-empty",
45
+ "[OK] Release gate module loadable",
45
46
  ];
46
47
  for (const line of requiredLines) {
47
48
  if (!result.stdout.includes(line)) throw new Error(`doctor output missing stable line: ${line}`);
@@ -12,6 +12,10 @@ import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
13
13
  import { setTimeout as sleep } from "node:timers/promises";
14
14
 
15
+ // v0.7.2: tasks complete with "done_by_agent" (pending acceptance) instead of "done".
16
+ // Both are valid successful completion statuses for lifecycle smoke tests.
17
+ const isDoneStatus = (s) => s === "done" || s === "done_by_agent";
18
+
15
19
  const tempRoot = mkdtempSync(join(tmpdir(), "patchwarden-lifecycle-"));
16
20
  const workspaceRoot = join(tempRoot, "workspace");
17
21
  const repoPath = join(workspaceRoot, "repo");
@@ -178,7 +182,7 @@ try {
178
182
  verify_commands: ["node --check main.js", "node --check second.js"],
179
183
  });
180
184
  const result = await runTask(task.task_id);
181
- if (result.status !== "done") throw new Error(`Writer task ended ${result.status}: ${result.error}`);
185
+ if (!isDoneStatus(result.status)) throw new Error(`Writer task ended ${result.status}: ${result.error}`);
182
186
  const status = getTaskStatus(task.task_id);
183
187
  const changed = status.changed_files || [];
184
188
  if (!changed.some((file) => file.path === "README.md" && file.change === "modified")) {
@@ -253,7 +257,7 @@ try {
253
257
  });
254
258
  const result = await runTask(task.task_id);
255
259
  const verify = JSON.parse(readFileSync(join(task.path, "verify.json"), "utf-8"));
256
- if (result.status !== "done" || verify.status !== "passed" || verify.commands.length !== 1) {
260
+ if (!isDoneStatus(result.status) || verify.status !== "passed" || verify.commands.length !== 1) {
257
261
  throw new Error(`Legacy verification mismatch: ${JSON.stringify({ result, verify })}`);
258
262
  }
259
263
  if (verify.commands[0].cwd !== repoPath || !("stdout_tail" in verify.commands[0]) || !("stderr_tail" in verify.commands[0])) {
@@ -265,7 +269,7 @@ try {
265
269
  const plan = savePlan({ title: "No diff", content: "Do not change files." });
266
270
  const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo" });
267
271
  const result = await runTask(task.task_id);
268
- if (result.status !== "done") throw new Error(`No-op task failed: ${JSON.stringify(result)}`);
272
+ if (!isDoneStatus(result.status)) throw new Error(`No-op task failed: ${JSON.stringify(result)}`);
269
273
  const diff = getDiff(task.task_id);
270
274
  const verify = JSON.parse(readFileSync(join(task.path, "verify.json"), "utf-8"));
271
275
  if (verify.status !== "skipped") throw new Error(`Expected skipped verification: ${JSON.stringify(verify)}`);
@@ -286,7 +290,7 @@ try {
286
290
  const plan = savePlan({ title: "Large diff", content: "Create a large text file." });
287
291
  const task = createTask({ plan_id: plan.plan_id, agent: "largewriter", repo_path: "repo" });
288
292
  const result = await runTask(task.task_id);
289
- if (result.status !== "done") throw new Error(`Large task failed: ${JSON.stringify(result)}`);
293
+ if (!isDoneStatus(result.status)) throw new Error(`Large task failed: ${JSON.stringify(result)}`);
290
294
  const diff = getDiff(task.task_id);
291
295
  const patchSize = readFileSync(join(task.path, "diff.patch"), "utf-8").length;
292
296
  if (!diff.truncated || !diff.patch_head || !diff.diff_patch_path || patchSize <= diff.content.length) {
@@ -298,7 +302,7 @@ try {
298
302
  const plan = savePlan({ title: "Non-Git evidence", content: "Modify files in a non-Git repository." });
299
303
  const task = createTask({ plan_id: plan.plan_id, agent: "writer", repo_path: "plain-repo" });
300
304
  const result = await runTask(task.task_id);
301
- if (result.status !== "done") throw new Error(`Non-Git task failed: ${JSON.stringify(result)}`);
305
+ if (!isDoneStatus(result.status)) throw new Error(`Non-Git task failed: ${JSON.stringify(result)}`);
302
306
  const diff = getDiff(task.task_id);
303
307
  if (
304
308
  diff.patch_mode !== "hash_only" ||
@@ -313,7 +317,7 @@ try {
313
317
  const plan = savePlan({ title: "Binary evidence", content: "Create a binary fixture." });
314
318
  const task = createTask({ plan_id: plan.plan_id, agent: "binarywriter", repo_path: "repo" });
315
319
  const result = await runTask(task.task_id);
316
- if (result.status !== "done") throw new Error(`Binary task failed: ${JSON.stringify(result)}`);
320
+ if (!isDoneStatus(result.status)) throw new Error(`Binary task failed: ${JSON.stringify(result)}`);
317
321
  const diff = getDiff(task.task_id);
318
322
  if (diff.patch_mode !== "textual" || !diff.content.includes("GIT binary patch")) {
319
323
  throw new Error(`Binary patch evidence mismatch: ${JSON.stringify(diff)}`);
@@ -324,7 +328,7 @@ try {
324
328
  const plan = savePlan({ title: "Artifact hygiene", content: "Generate representative task outputs." });
325
329
  const task = createTask({ plan_id: plan.plan_id, agent: "artifactwriter", repo_path: "repo" });
326
330
  const result = await runTask(task.task_id);
327
- if (result.status !== "done") throw new Error(`Artifact task failed: ${JSON.stringify(result)}`);
331
+ if (!isDoneStatus(result.status)) throw new Error(`Artifact task failed: ${JSON.stringify(result)}`);
328
332
  const standard = getTaskSummary(task.task_id);
329
333
  const compact = getTaskSummary(task.task_id, { view: "compact", max_items: 1 });
330
334
  const counts = standard.artifact_hygiene?.counts || {};
@@ -353,7 +357,7 @@ try {
353
357
  const plan = savePlan({ title: "Delete fixture", content: "Delete the designated fixture file." });
354
358
  const task = createTask({ plan_id: plan.plan_id, agent: "deleter", repo_path: "repo" });
355
359
  const result = await runTask(task.task_id);
356
- if (result.status !== "done") throw new Error(`Delete task failed: ${JSON.stringify(result)}`);
360
+ if (!isDoneStatus(result.status)) throw new Error(`Delete task failed: ${JSON.stringify(result)}`);
357
361
  const diff = getDiff(task.task_id);
358
362
  if (!diff.file_stats?.some((file) => file.path === "delete-me.txt" && file.status === "deleted")) {
359
363
  throw new Error(`Deleted file stats missing: ${JSON.stringify(diff.file_stats)}`);
@@ -504,7 +508,7 @@ try {
504
508
  const task = createTask({ plan_id: plan.plan_id, agent: "noop", repo_path: "repo", verify_commands: ["node --check main.js"] });
505
509
  const result = await runTask(task.task_id);
506
510
 
507
- if (result.status !== "done") {
511
+ if (!isDoneStatus(result.status)) {
508
512
  throw new Error(`Expected done, got: ${JSON.stringify(result)}`);
509
513
  }
510
514
  const structured = JSON.parse(readFileSync(join(task.path, "result.json"), "utf-8"));
@@ -4,18 +4,20 @@ import { resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
6
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
- import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
7
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, CHATGPT_SEARCH_TOOL_NAMES } from "../../dist/tools/toolCatalog.js";
8
8
 
9
9
  const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
10
10
  const jsonOnly = process.argv.includes("--json");
11
11
  const profileIndex = process.argv.indexOf("--profile");
12
12
  const profile = profileIndex >= 0 ? process.argv[profileIndex + 1] : "chatgpt_core";
13
- if (profile !== "chatgpt_core" && profile !== "chatgpt_direct") {
13
+ if (profile !== "chatgpt_core" && profile !== "chatgpt_direct" && profile !== "chatgpt_search") {
14
14
  throw new Error(`Unsupported manifest profile "${profile}".`);
15
15
  }
16
16
  const expectedTools = profile === "chatgpt_direct"
17
17
  ? [...CHATGPT_DIRECT_TOOL_NAMES]
18
- : [...CHATGPT_CORE_TOOL_NAMES];
18
+ : profile === "chatgpt_search"
19
+ ? [...CHATGPT_SEARCH_TOOL_NAMES]
20
+ : [...CHATGPT_CORE_TOOL_NAMES];
19
21
  const defaultConfigPath = resolve(root, "patchwarden.config.json");
20
22
  const transportEnv = {
21
23
  ...process.env,
@@ -102,13 +104,31 @@ try {
102
104
  if (!summaryProperties.includes("view") || !summaryProperties.includes("max_items")) {
103
105
  throw new Error("get_task_summary schema must expose view and max_items");
104
106
  }
105
- } else {
107
+ const safeRequirements = {
108
+ safe_result: ["task_id"],
109
+ safe_audit: ["task_id"],
110
+ safe_test_summary: ["task_id"],
111
+ safe_diff_summary: ["task_id"],
112
+ };
113
+ for (const [toolName, requiredProperties] of Object.entries(safeRequirements)) {
114
+ const tool = listed.tools.find((entry) => entry.name === toolName);
115
+ const properties = Object.keys(tool?.inputSchema?.properties || {});
116
+ for (const property of requiredProperties) {
117
+ if (!properties.includes(property)) {
118
+ throw new Error(`${toolName} schema is missing ${property}`);
119
+ }
120
+ }
121
+ }
122
+ } else if (profile === "chatgpt_direct") {
106
123
  const directRequirements = {
107
124
  create_direct_session: ["repo_path"],
108
125
  apply_patch: ["session_id", "path", "expected_sha256", "operations"],
109
126
  run_verification: ["session_id", "command"],
110
127
  finalize_direct_session: ["session_id"],
111
128
  audit_session: ["session_id"],
129
+ safe_direct_summary: ["session_id"],
130
+ safe_finalize_direct_session: ["session_id"],
131
+ safe_audit_direct_session: ["session_id"],
112
132
  };
113
133
  for (const [toolName, requiredProperties] of Object.entries(directRequirements)) {
114
134
  const tool = listed.tools.find((entry) => entry.name === toolName);
@@ -119,6 +139,21 @@ try {
119
139
  }
120
140
  }
121
141
  }
142
+ } else {
143
+ const searchRequirements = {
144
+ discover_tools: ["query"],
145
+ explain_tool: ["name"],
146
+ invoke_discovered_tool: ["toolName", "arguments", "discoveryToken"],
147
+ };
148
+ for (const [toolName, requiredProperties] of Object.entries(searchRequirements)) {
149
+ const tool = listed.tools.find((entry) => entry.name === toolName);
150
+ const properties = Object.keys(tool?.inputSchema?.properties || {});
151
+ for (const property of requiredProperties) {
152
+ if (!properties.includes(property)) {
153
+ throw new Error(`${toolName} schema is missing ${property}`);
154
+ }
155
+ }
156
+ }
122
157
  }
123
158
  const healthResult = await client.callTool({ name: "health_check", arguments: {} });
124
159
  if (healthResult.isError) throw new Error(String(healthResult.content?.[0]?.text || "health_check failed"));
@@ -127,7 +162,8 @@ try {
127
162
  health.tool_profile !== profile ||
128
163
  health.tool_count !== expectedTools.length ||
129
164
  !health.tool_manifest_sha256 ||
130
- (profile === "chatgpt_direct" && health.direct_profile_enabled !== true)
165
+ (profile === "chatgpt_direct" && health.direct_profile_enabled !== true) ||
166
+ (profile === "chatgpt_search" && health.search_tool_count !== expectedTools.length)
131
167
  ) {
132
168
  throw new Error(`health_check catalog mismatch: ${JSON.stringify(health)}`);
133
169
  }
@@ -164,12 +200,23 @@ try {
164
200
  health_check: ["detail"],
165
201
  list_tasks: ["repo_path", "active_only"],
166
202
  get_task_summary: ["view", "max_items"],
167
- } : {
203
+ safe_result: ["task_id"],
204
+ safe_audit: ["task_id"],
205
+ safe_test_summary: ["task_id"],
206
+ safe_diff_summary: ["task_id"],
207
+ } : profile === "chatgpt_direct" ? {
168
208
  create_direct_session: ["repo_path"],
169
209
  apply_patch: ["session_id", "path", "expected_sha256", "operations"],
170
210
  run_verification: ["session_id", "command"],
171
211
  finalize_direct_session: ["session_id"],
172
212
  audit_session: ["session_id"],
213
+ safe_direct_summary: ["session_id"],
214
+ safe_finalize_direct_session: ["session_id"],
215
+ safe_audit_direct_session: ["session_id"],
216
+ } : {
217
+ discover_tools: ["query"],
218
+ explain_tool: ["name"],
219
+ invoke_discovered_tool: ["toolName", "arguments", "discoveryToken"],
173
220
  },
174
221
  };
175
222
  console.log(jsonOnly ? JSON.stringify(output) : JSON.stringify(output, null, 2));
@@ -103,12 +103,21 @@ try {
103
103
  const tools = await client.listTools();
104
104
  const names = tools.tools.map((tool) => tool.name).sort();
105
105
  const expected = [
106
+ "accept_subgoal",
106
107
  "apply_patch",
107
108
  "audit_session",
108
109
  "audit_task",
109
110
  "cancel_task",
111
+ "check_release_gate",
110
112
  "create_direct_session",
113
+ "create_goal",
114
+ "create_subgoal_task",
111
115
  "create_task",
116
+ "diagnose_task",
117
+ "discard_worktree",
118
+ "discover_tools",
119
+ "explain_tool",
120
+ "export_handoff",
112
121
  "finalize_direct_session",
113
122
  "get_diff",
114
123
  "get_plan",
@@ -121,16 +130,31 @@ try {
121
130
  "get_task_summary",
122
131
  "get_test_log",
123
132
  "health_check",
133
+ "invoke_discovered_tool",
124
134
  "kill_task",
125
135
  "list_agents",
136
+ "list_goals",
126
137
  "list_tasks",
127
138
  "list_workspace",
139
+ "merge_worktree",
140
+ "read_goal",
128
141
  "read_workspace_file",
142
+ "reconcile_tasks",
143
+ "reject_subgoal",
129
144
  "retry_task",
130
145
  "run_verification",
146
+ "safe_audit",
147
+ "safe_audit_direct_session",
148
+ "safe_diff_summary",
149
+ "safe_direct_summary",
150
+ "safe_finalize_direct_session",
151
+ "safe_result",
131
152
  "safe_status",
153
+ "safe_test_summary",
132
154
  "save_plan",
133
155
  "search_workspace",
156
+ "suggest_next_subgoal",
157
+ "summarize_goal_progress",
134
158
  "sync_file",
135
159
  "wait_for_task",
136
160
  ];
@@ -143,7 +167,7 @@ try {
143
167
  if (!tools._meta || typeof tools._meta.tool_manifest_sha256 !== "string" || tools._meta.tool_manifest_sha256.length !== 64) {
144
168
  throw new Error(`tools/list _meta missing manifest hash: ${JSON.stringify(tools._meta || null)}`);
145
169
  }
146
- if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 30) {
170
+ if (tools._meta.tool_profile !== "full" || tools._meta.tool_count !== 54) {
147
171
  throw new Error(`tools/list _meta profile/count mismatch: ${JSON.stringify(tools._meta)}`);
148
172
  }
149
173
  if (typeof tools._meta.schema_epoch !== "string" || typeof tools._meta.server_version !== "string") {
@@ -296,8 +320,8 @@ try {
296
320
 
297
321
  const statusPath = join(task.path, "status.json");
298
322
  const statusAfter = JSON.parse(readFileSync(statusPath, "utf-8"));
299
- if (statusAfter.status !== "done") {
300
- throw new Error(`runner status should be done, got ${statusAfter.status}`);
323
+ if (statusAfter.status !== "done" && statusAfter.status !== "done_by_agent") {
324
+ throw new Error(`runner status should be done or done_by_agent, got ${statusAfter.status}`);
301
325
  }
302
326
  for (const fileName of ["result.md", "result.json", "diff.patch", "git.diff", "file-stats.json", "test.log", "verify.json", "verify.log"]) {
303
327
  if (!existsSync(join(task.path, fileName))) {
@@ -393,7 +417,7 @@ try {
393
417
  await disabledClient.close();
394
418
  ok("chatgpt_direct disabled exposes only health_check with diagnostic");
395
419
 
396
- // 2. chatgpt_direct enabled: 10 tools + minimal create_direct_session
420
+ // 2. chatgpt_direct enabled: 13 tools + minimal create_direct_session
397
421
  const enabledConfigPath = join(tempRoot, "direct-enabled.json");
398
422
  const directRepo = join(workspaceRoot, "direct-fixture");
399
423
  mkdirSync(join(directRepo, "src"), { recursive: true });
@@ -450,14 +474,17 @@ try {
450
474
  "list_workspace",
451
475
  "read_workspace_file",
452
476
  "run_verification",
477
+ "safe_audit_direct_session",
478
+ "safe_direct_summary",
479
+ "safe_finalize_direct_session",
453
480
  "search_workspace",
454
481
  "sync_file",
455
482
  ];
456
483
  if (JSON.stringify(enabledNames) !== JSON.stringify(expectedDirect)) {
457
484
  throw new Error(`chatgpt_direct enabled tools mismatch: ${enabledNames.join(", ")}`);
458
485
  }
459
- if (enabledTools._meta.tool_count !== 10) {
460
- throw new Error(`chatgpt_direct enabled tool_count should be 10, got ${enabledTools._meta.tool_count}`);
486
+ if (enabledTools._meta.tool_count !== 13) {
487
+ throw new Error(`chatgpt_direct enabled tool_count should be 13, got ${enabledTools._meta.tool_count}`);
461
488
  }
462
489
 
463
490
  // Minimal create_direct_session
@@ -477,12 +504,12 @@ try {
477
504
  if (enabledHealth.direct_profile_enabled !== true) {
478
505
  throw new Error(`direct_profile_enabled should be true, got ${enabledHealth.direct_profile_enabled}`);
479
506
  }
480
- if (enabledHealth.direct_tool_count !== 10) {
481
- throw new Error(`direct_tool_count should be 10, got ${enabledHealth.direct_tool_count}`);
507
+ if (enabledHealth.direct_tool_count !== 13) {
508
+ throw new Error(`direct_tool_count should be 13, got ${enabledHealth.direct_tool_count}`);
482
509
  }
483
510
 
484
511
  await enabledClient.close();
485
- ok("chatgpt_direct enabled exposes 10 tools and create_direct_session works");
512
+ ok("chatgpt_direct enabled exposes 13 tools and create_direct_session works");
486
513
  } catch (error) {
487
514
  fail("MCP smoke test", error);
488
515
  } finally {
@@ -105,7 +105,7 @@ function createFixture(name) {
105
105
  server_version: "0.6.0",
106
106
  schema_epoch: "2026-06-22-v6",
107
107
  tool_profile: "chatgpt_core",
108
- tool_count: 16,
108
+ tool_count: 21,
109
109
  tool_names: [],
110
110
  tool_manifest_sha256: "a".repeat(64),
111
111
  });
@@ -167,7 +167,7 @@ function fixtureEnv(fixture, mode, lifetimeMs) {
167
167
  ...process.env,
168
168
  APPDATA: fixture.appData,
169
169
  LOCALAPPDATA: fixture.localAppData,
170
- CONTROL_PLANE_API_KEY: "watcher-supervisor-smoke-secret",
170
+ CONTROL_PLANE_API_KEY: "test-key",
171
171
  WATCHER_FIXTURE_MODE: mode,
172
172
  TUNNEL_FIXTURE_LIFETIME_MS: String(lifetimeMs),
173
173
  };
@@ -0,0 +1,149 @@
1
+ import { PatchWardenError } from "../errors.js";
2
+
3
+ // โ”€โ”€ Types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
4
+
5
+ export interface AgentRouteInput {
6
+ goal?: string;
7
+ scope?: string[];
8
+ inline_plan?: string;
9
+ template?: string;
10
+ configuredAgents: string[];
11
+ }
12
+
13
+ export interface AgentRouteResult {
14
+ recommended_agent: string;
15
+ reason: string;
16
+ fallback: boolean;
17
+ }
18
+
19
+ // โ”€โ”€ Routing rules (roadmap 9.3) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
20
+
21
+ export const AGENT_ROUTING_RULES = {
22
+ /** ๅคง่ง„ๆจกไปฃ็ ไฟฎๆ”น โ†’ opencode */
23
+ largeScope: {
24
+ agent: "opencode",
25
+ threshold: 10, // scope.length > threshold
26
+ },
27
+ /** ๅ•ๆ–‡ไปถไฟฎๆ”น โ†’ patchwarden-direct */
28
+ singleFile: {
29
+ agent: "patchwarden-direct",
30
+ },
31
+ /** ้ชŒๆ”ถๅฎก่ฎก โ†’ patchwarden-audit */
32
+ audit: {
33
+ agent: "patchwarden-audit",
34
+ keywords: ["ๅฎก่ฎก", "audit", "้ชŒๆ”ถ", "verify"],
35
+ },
36
+ /** ๅคๆ‚ๆŽจ็†ๅ’Œ้‡ๆž„ โ†’ codex */
37
+ refactor: {
38
+ agent: "codex",
39
+ keywords: ["้‡ๆž„", "refactor", "่ทจๆจกๅ—", "redesign"],
40
+ },
41
+ /** ๆ–‡ๆกฃๆ•ด็† โ†’ claude๏ผˆfallback ๅˆฐ opencode๏ผ‰ */
42
+ documentation: {
43
+ agent: "claude",
44
+ fallbackAgent: "opencode",
45
+ keywords: ["ๆ–‡ๆกฃ", "readme", "changelog", "doc"],
46
+ },
47
+ } as const;
48
+
49
+ // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
50
+
51
+ function containsAny(text: string, keywords: readonly string[]): boolean {
52
+ return keywords.some((keyword) => text.includes(keyword));
53
+ }
54
+
55
+ function resolveRoute(
56
+ preferred: string,
57
+ reason: string,
58
+ configuredAgents: string[]
59
+ ): AgentRouteResult {
60
+ if (configuredAgents.includes(preferred)) {
61
+ return { recommended_agent: preferred, reason, fallback: false };
62
+ }
63
+ const fallback = configuredAgents[0];
64
+ return {
65
+ recommended_agent: fallback,
66
+ reason: `fallback: ${preferred} not configured, using ${fallback}`,
67
+ fallback: true,
68
+ };
69
+ }
70
+
71
+ // โ”€โ”€ Route agent โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
72
+
73
+ export function routeAgent(input: AgentRouteInput): AgentRouteResult {
74
+ if (!input.configuredAgents || input.configuredAgents.length === 0) {
75
+ throw new PatchWardenError(
76
+ "no_agent_configured",
77
+ "No agent is configured for routing.",
78
+ "Configure at least one agent in patchwarden.config.json under the agents field."
79
+ );
80
+ }
81
+
82
+ const scopeCount = input.scope?.length ?? 0;
83
+ const text = `${input.goal || ""} ${input.inline_plan || ""}`.toLowerCase();
84
+
85
+ // Rule 1: ๅคง่ง„ๆจกไปฃ็ ไฟฎๆ”น โ†’ opencode (scope files > 10)
86
+ if (scopeCount > AGENT_ROUTING_RULES.largeScope.threshold) {
87
+ return resolveRoute(
88
+ AGENT_ROUTING_RULES.largeScope.agent,
89
+ `large scope (${scopeCount} files)`,
90
+ input.configuredAgents
91
+ );
92
+ }
93
+
94
+ // Rule 2: ๅ•ๆ–‡ไปถไฟฎๆ”น โ†’ patchwarden-direct (scope files === 1)
95
+ if (scopeCount === 1) {
96
+ return resolveRoute(
97
+ AGENT_ROUTING_RULES.singleFile.agent,
98
+ "single file scope",
99
+ input.configuredAgents
100
+ );
101
+ }
102
+
103
+ // Rule 3: ้ชŒๆ”ถๅฎก่ฎก โ†’ patchwarden-audit
104
+ if (containsAny(text, AGENT_ROUTING_RULES.audit.keywords)) {
105
+ return resolveRoute(
106
+ AGENT_ROUTING_RULES.audit.agent,
107
+ "audit keywords",
108
+ input.configuredAgents
109
+ );
110
+ }
111
+
112
+ // Rule 4: ๅคๆ‚ๆŽจ็†ๅ’Œ้‡ๆž„ โ†’ codex
113
+ if (containsAny(text, AGENT_ROUTING_RULES.refactor.keywords)) {
114
+ return resolveRoute(
115
+ AGENT_ROUTING_RULES.refactor.agent,
116
+ "refactor keywords",
117
+ input.configuredAgents
118
+ );
119
+ }
120
+
121
+ // Rule 5: ๆ–‡ๆกฃๆ•ด็† โ†’ claude๏ผˆfallback ๅˆฐ opencode๏ผ‰
122
+ if (containsAny(text, AGENT_ROUTING_RULES.documentation.keywords)) {
123
+ const preferred = AGENT_ROUTING_RULES.documentation.agent;
124
+ const fallbackAgent = AGENT_ROUTING_RULES.documentation.fallbackAgent;
125
+ if (input.configuredAgents.includes(preferred)) {
126
+ return { recommended_agent: preferred, reason: "documentation keywords", fallback: false };
127
+ }
128
+ if (input.configuredAgents.includes(fallbackAgent)) {
129
+ return {
130
+ recommended_agent: fallbackAgent,
131
+ reason: `fallback: ${preferred} not configured, using ${fallbackAgent}`,
132
+ fallback: true,
133
+ };
134
+ }
135
+ const firstAgent = input.configuredAgents[0];
136
+ return {
137
+ recommended_agent: firstAgent,
138
+ reason: `fallback: ${preferred} not configured, using ${firstAgent}`,
139
+ fallback: true,
140
+ };
141
+ }
142
+
143
+ // Rule 6: ๆ— ๅŒน้… โ†’ fallback ๅˆฐ configuredAgents[0]
144
+ return {
145
+ recommended_agent: input.configuredAgents[0],
146
+ reason: `no specific routing rule matched, using default agent ${input.configuredAgents[0]}`,
147
+ fallback: true,
148
+ };
149
+ }
package/src/config.ts CHANGED
@@ -21,7 +21,7 @@ export interface PatchWardenConfig {
21
21
  defaultTaskTimeoutSeconds: number;
22
22
  maxTaskTimeoutSeconds: number;
23
23
  watcherStaleSeconds: number;
24
- toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct";
24
+ toolProfile?: "full" | "chatgpt_core" | "chatgpt_direct" | "chatgpt_search";
25
25
  repoAliases?: Record<string, string>;
26
26
  enableAgentAssessment?: boolean;
27
27
  agentAssessmentTimeoutSeconds?: number;
@@ -224,8 +224,14 @@ function normalizeConfig(config: PatchWardenConfig): PatchWardenConfig {
224
224
  if (!Number.isInteger(config.watcherStaleSeconds) || config.watcherStaleSeconds < 5 || config.watcherStaleSeconds > 3600) {
225
225
  throw new Error("watcherStaleSeconds must be an integer from 5 to 3600");
226
226
  }
227
- if (config.toolProfile !== undefined && config.toolProfile !== "full" && config.toolProfile !== "chatgpt_core" && config.toolProfile !== "chatgpt_direct") {
228
- throw new Error('toolProfile must be "full", "chatgpt_core", or "chatgpt_direct"');
227
+ if (
228
+ config.toolProfile !== undefined &&
229
+ config.toolProfile !== "full" &&
230
+ config.toolProfile !== "chatgpt_core" &&
231
+ config.toolProfile !== "chatgpt_direct" &&
232
+ config.toolProfile !== "chatgpt_search"
233
+ ) {
234
+ throw new Error('toolProfile must be "full", "chatgpt_core", "chatgpt_direct", or "chatgpt_search"');
229
235
  }
230
236
  if (config.enableAgentAssessment !== undefined && typeof config.enableAgentAssessment !== "boolean") {
231
237
  throw new Error("enableAgentAssessment must be a boolean");