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.
- package/README.en.md +18 -18
- package/README.md +20 -20
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +15 -2
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +175 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/package.json +3 -2
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +53 -6
- package/scripts/checks/mcp-smoke.js +36 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +17 -3
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
package/dist/watcherStatus.js
CHANGED
|
@@ -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": "
|
|
4
|
-
"description": "
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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 !==
|
|
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:
|
|
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 !==
|
|
460
|
-
throw new Error(`chatgpt_direct enabled tool_count should be
|
|
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 !==
|
|
481
|
-
throw new Error(`direct_tool_count should be
|
|
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
|
|
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:
|
|
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: "
|
|
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 (
|
|
228
|
-
|
|
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");
|