patchwarden 0.4.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/Check-PatchWarden-Health.cmd +6 -0
- package/LICENSE +21 -0
- package/README.md +526 -0
- package/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/Restart-PatchWarden.cmd +19 -0
- package/Start-PatchWarden-Tunnel.cmd +7 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +131 -0
- package/dist/doctor.d.ts +10 -0
- package/dist/doctor.js +388 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +27 -0
- package/dist/httpServer.d.ts +17 -0
- package/dist/httpServer.js +255 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +24 -0
- package/dist/runner/changeCapture.d.ts +43 -0
- package/dist/runner/changeCapture.js +245 -0
- package/dist/runner/cli.d.ts +9 -0
- package/dist/runner/cli.js +26 -0
- package/dist/runner/runTask.d.ts +8 -0
- package/dist/runner/runTask.js +821 -0
- package/dist/runner/watch.d.ts +19 -0
- package/dist/runner/watch.js +136 -0
- package/dist/security/commandGuard.d.ts +22 -0
- package/dist/security/commandGuard.js +56 -0
- package/dist/security/contentRedaction.d.ts +12 -0
- package/dist/security/contentRedaction.js +65 -0
- package/dist/security/pathGuard.d.ts +3 -0
- package/dist/security/pathGuard.js +98 -0
- package/dist/security/planGuard.d.ts +1 -0
- package/dist/security/planGuard.js +65 -0
- package/dist/security/sensitiveGuard.d.ts +2 -0
- package/dist/security/sensitiveGuard.js +60 -0
- package/dist/smoke-test.d.ts +15 -0
- package/dist/smoke-test.js +989 -0
- package/dist/taskProgress.d.ts +6 -0
- package/dist/taskProgress.js +36 -0
- package/dist/taskRuntime.d.ts +10 -0
- package/dist/taskRuntime.js +25 -0
- package/dist/tools/auditTask.d.ts +19 -0
- package/dist/tools/auditTask.js +307 -0
- package/dist/tools/cancelTask.d.ts +30 -0
- package/dist/tools/cancelTask.js +64 -0
- package/dist/tools/createTask.d.ts +42 -0
- package/dist/tools/createTask.js +188 -0
- package/dist/tools/getPlan.d.ts +12 -0
- package/dist/tools/getPlan.js +29 -0
- package/dist/tools/getTaskFile.d.ts +24 -0
- package/dist/tools/getTaskFile.js +84 -0
- package/dist/tools/getTaskProgress.d.ts +1 -0
- package/dist/tools/getTaskProgress.js +4 -0
- package/dist/tools/getTaskStatus.d.ts +46 -0
- package/dist/tools/getTaskStatus.js +36 -0
- package/dist/tools/getTaskStdoutTail.d.ts +9 -0
- package/dist/tools/getTaskStdoutTail.js +56 -0
- package/dist/tools/getTaskSummary.d.ts +66 -0
- package/dist/tools/getTaskSummary.js +237 -0
- package/dist/tools/healthCheck.d.ts +79 -0
- package/dist/tools/healthCheck.js +230 -0
- package/dist/tools/killTask.d.ts +15 -0
- package/dist/tools/killTask.js +4 -0
- package/dist/tools/listAgents.d.ts +12 -0
- package/dist/tools/listAgents.js +42 -0
- package/dist/tools/listTasks.d.ts +36 -0
- package/dist/tools/listTasks.js +94 -0
- package/dist/tools/listWorkspace.d.ts +10 -0
- package/dist/tools/listWorkspace.js +54 -0
- package/dist/tools/readWorkspaceFile.d.ts +8 -0
- package/dist/tools/readWorkspaceFile.js +26 -0
- package/dist/tools/registry.d.ts +24 -0
- package/dist/tools/registry.js +566 -0
- package/dist/tools/retryTask.d.ts +8 -0
- package/dist/tools/retryTask.js +45 -0
- package/dist/tools/savePlan.d.ts +10 -0
- package/dist/tools/savePlan.js +28 -0
- package/dist/tools/taskOutputs.d.ts +49 -0
- package/dist/tools/taskOutputs.js +136 -0
- package/dist/tools/taskTemplates.d.ts +15 -0
- package/dist/tools/taskTemplates.js +116 -0
- package/dist/tools/toolCatalog.d.ts +19 -0
- package/dist/tools/toolCatalog.js +71 -0
- package/dist/tools/waitForTask.d.ts +25 -0
- package/dist/tools/waitForTask.js +59 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/watcherStatus.d.ts +20 -0
- package/dist/watcherStatus.js +77 -0
- package/docs/assets/patchwarden-chatgpt-demo.svg +30 -0
- package/docs/demo.md +58 -0
- package/docs/migration-from-safe-bifrost.md +31 -0
- package/docs/release-v0.3.0.md +43 -0
- package/docs/release-v0.4.0.md +74 -0
- package/examples/config.example.json +40 -0
- package/examples/openai-tunnel/README.md +48 -0
- package/examples/openai-tunnel/chatgpt-test-prompt.md +53 -0
- package/examples/openai-tunnel/tunnel-client.example.yaml +39 -0
- package/package.json +73 -0
- package/scripts/brand-check.js +44 -0
- package/scripts/doctor-smoke.js +52 -0
- package/scripts/get-patchwarden-health.ps1 +114 -0
- package/scripts/http-mcp-smoke.js +351 -0
- package/scripts/lifecycle-smoke.js +453 -0
- package/scripts/mcp-manifest-check.js +120 -0
- package/scripts/mcp-smoke.js +336 -0
- package/scripts/pack-clean.js +142 -0
- package/scripts/patchwarden-mcp-stdio.cmd +7 -0
- package/scripts/restart-patchwarden.ps1 +242 -0
- package/scripts/start-patchwarden-tunnel.ps1 +503 -0
- package/scripts/tunnel-supervisor-smoke.js +77 -0
- package/scripts/watcher-supervisor-smoke.js +195 -0
- package/src/config.ts +165 -0
- package/src/doctor.ts +438 -0
- package/src/errors.ts +26 -0
- package/src/httpServer.ts +277 -0
- package/src/index.ts +34 -0
- package/src/runner/changeCapture.ts +304 -0
- package/src/runner/cli.ts +34 -0
- package/src/runner/runTask.ts +935 -0
- package/src/runner/watch.ts +152 -0
- package/src/security/commandGuard.ts +97 -0
- package/src/security/contentRedaction.ts +85 -0
- package/src/security/pathGuard.ts +175 -0
- package/src/security/planGuard.ts +93 -0
- package/src/security/sensitiveGuard.ts +72 -0
- package/src/smoke-test.ts +1078 -0
- package/src/taskProgress.ts +41 -0
- package/src/taskRuntime.ts +37 -0
- package/src/tools/auditTask.ts +340 -0
- package/src/tools/cancelTask.ts +70 -0
- package/src/tools/createTask.ts +337 -0
- package/src/tools/getPlan.ts +48 -0
- package/src/tools/getTaskFile.ts +111 -0
- package/src/tools/getTaskProgress.ts +5 -0
- package/src/tools/getTaskStatus.ts +85 -0
- package/src/tools/getTaskStdoutTail.ts +61 -0
- package/src/tools/getTaskSummary.ts +307 -0
- package/src/tools/healthCheck.ts +234 -0
- package/src/tools/killTask.ts +5 -0
- package/src/tools/listAgents.ts +54 -0
- package/src/tools/listTasks.ts +139 -0
- package/src/tools/listWorkspace.ts +71 -0
- package/src/tools/readWorkspaceFile.ts +41 -0
- package/src/tools/registry.ts +666 -0
- package/src/tools/retryTask.ts +53 -0
- package/src/tools/savePlan.ts +46 -0
- package/src/tools/taskOutputs.ts +155 -0
- package/src/tools/taskTemplates.ts +162 -0
- package/src/tools/toolCatalog.ts +92 -0
- package/src/tools/waitForTask.ts +90 -0
- package/src/version.ts +2 -0
- package/src/watcherStatus.ts +102 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared tool registry for PatchWarden MCP server.
|
|
3
|
+
* Used by both stdio (index.ts) and HTTP (httpServer.ts) transports.
|
|
4
|
+
*/
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { getConfig } from "../config.js";
|
|
7
|
+
import { savePlan } from "../tools/savePlan.js";
|
|
8
|
+
import { getPlan } from "../tools/getPlan.js";
|
|
9
|
+
import { createTask } from "../tools/createTask.js";
|
|
10
|
+
import { getTaskStatus } from "../tools/getTaskStatus.js";
|
|
11
|
+
import { getResult, getResultJson, getDiff, getTestLog, getTaskLogTail } from "../tools/taskOutputs.js";
|
|
12
|
+
import { listWorkspace } from "../tools/listWorkspace.js";
|
|
13
|
+
import { readWorkspaceFile } from "../tools/readWorkspaceFile.js";
|
|
14
|
+
import { listTasks } from "../tools/listTasks.js";
|
|
15
|
+
import { cancelTask } from "../tools/cancelTask.js";
|
|
16
|
+
import { killTask } from "../tools/killTask.js";
|
|
17
|
+
import { retryTask } from "../tools/retryTask.js";
|
|
18
|
+
import { getTaskStdoutTail } from "../tools/getTaskStdoutTail.js";
|
|
19
|
+
import { getTaskProgress } from "../tools/getTaskProgress.js";
|
|
20
|
+
import { listAgents } from "../tools/listAgents.js";
|
|
21
|
+
import { healthCheck } from "../tools/healthCheck.js";
|
|
22
|
+
import { getTaskSummary } from "../tools/getTaskSummary.js";
|
|
23
|
+
import { waitForTask } from "../tools/waitForTask.js";
|
|
24
|
+
import { errorPayload, PatchWardenError } from "../errors.js";
|
|
25
|
+
import { auditTask } from "../tools/auditTask.js";
|
|
26
|
+
import { runTask } from "../runner/runTask.js";
|
|
27
|
+
import { TASK_TEMPLATE_NAMES } from "./taskTemplates.js";
|
|
28
|
+
import { buildToolCatalogSnapshot, getLastToolCatalogSnapshot, resolveToolProfile, selectToolsForProfile, } from "./toolCatalog.js";
|
|
29
|
+
export function getToolDefs() {
|
|
30
|
+
const config = getConfig();
|
|
31
|
+
const agentNames = Object.keys(config.agents).sort();
|
|
32
|
+
const agentDescription = agentNames.length > 0
|
|
33
|
+
? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}`
|
|
34
|
+
: "Configured local agent name. No agents are currently configured.";
|
|
35
|
+
const testCommands = [...config.allowedTestCommands].sort();
|
|
36
|
+
const tools = [
|
|
37
|
+
{
|
|
38
|
+
name: "save_plan",
|
|
39
|
+
description: "Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
title: { type: "string", description: "Plan title" },
|
|
44
|
+
content: { type: "string", description: "Plan content in Markdown" },
|
|
45
|
+
},
|
|
46
|
+
required: ["title", "content"],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "get_plan",
|
|
51
|
+
description: "Read a saved plan by its plan_id.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: {
|
|
55
|
+
plan_id: { type: "string", description: "Plan ID returned by save_plan" },
|
|
56
|
+
},
|
|
57
|
+
required: ["plan_id"],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "health_check",
|
|
62
|
+
description: "Check MCP catalog consistency, watcher freshness/supervisor state, workspace readiness, and configured agents. Use detail=self_diagnostic for expanded read-only evidence.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
detail: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["standard", "self_diagnostic"],
|
|
69
|
+
default: "standard",
|
|
70
|
+
description: "Use self_diagnostic for catalog, watcher, agent, allowlist, workspace, and recent failure evidence.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "list_agents",
|
|
77
|
+
description: "List configured local agents and check whether each executable currently exists. This does not start an agent or contact its model provider.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "create_task",
|
|
85
|
+
description: "Create a repo-scoped task from exactly one source. A stale watcher preserves the task but returns execution_blocked and directs the client to health_check; otherwise call wait_for_task until terminal.",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
plan_id: { type: "string", description: "Plan ID from save_plan" },
|
|
90
|
+
inline_plan: { type: "string", description: "Inline Markdown plan. It is safety-checked and persisted as an auditable saved plan before task creation." },
|
|
91
|
+
plan_title: { type: "string", description: "Optional title used when inline_plan is supplied." },
|
|
92
|
+
template: {
|
|
93
|
+
type: "string",
|
|
94
|
+
enum: [...TASK_TEMPLATE_NAMES],
|
|
95
|
+
description: "Built-in guarded task template. Use with goal; rollback_scope_violation also requires source_task_id.",
|
|
96
|
+
},
|
|
97
|
+
goal: { type: "string", description: "Required task goal when template is supplied." },
|
|
98
|
+
source_task_id: { type: "string", description: "Required source task for rollback_scope_violation review." },
|
|
99
|
+
agent: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: agentDescription,
|
|
102
|
+
...(agentNames.length > 0 ? { enum: agentNames } : {}),
|
|
103
|
+
},
|
|
104
|
+
repo_path: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Required repository path inside workspaceRoot. No implicit workspace-root fallback is allowed.",
|
|
107
|
+
},
|
|
108
|
+
test_command: {
|
|
109
|
+
type: "string",
|
|
110
|
+
description: testCommands.length
|
|
111
|
+
? `Optional exact-match verification command. Allowed: ${testCommands.map((command) => JSON.stringify(command)).join(", ")}`
|
|
112
|
+
: "Optional exact-match verification command. No commands are currently allowed.",
|
|
113
|
+
...(testCommands.length > 0 ? { enum: testCommands } : {}),
|
|
114
|
+
},
|
|
115
|
+
verify_commands: {
|
|
116
|
+
type: "array",
|
|
117
|
+
maxItems: 20,
|
|
118
|
+
items: {
|
|
119
|
+
type: "string",
|
|
120
|
+
...(testCommands.length > 0 ? { enum: testCommands } : {}),
|
|
121
|
+
},
|
|
122
|
+
description: "Recommended allow-listed commands PatchWarden runs independently after the agent exits.",
|
|
123
|
+
},
|
|
124
|
+
timeout_seconds: {
|
|
125
|
+
type: "integer",
|
|
126
|
+
minimum: 1,
|
|
127
|
+
maximum: config.maxTaskTimeoutSeconds,
|
|
128
|
+
default: config.defaultTaskTimeoutSeconds,
|
|
129
|
+
description: `Total task timeout in seconds (default ${config.defaultTaskTimeoutSeconds}, max ${config.maxTaskTimeoutSeconds})`,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ["agent", "repo_path"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "get_task_status",
|
|
137
|
+
description: "Check task status, execution phase, watcher health, pending reason, current command, timeout, and change evidence.",
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
task_id: { type: "string", description: "Task ID from create_task" },
|
|
142
|
+
},
|
|
143
|
+
required: ["task_id"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "get_result",
|
|
148
|
+
description: "Read result.md, or return structured availability and watcher evidence while the task is not terminal.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
task_id: { type: "string", description: "Task ID" },
|
|
153
|
+
},
|
|
154
|
+
required: ["task_id"],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "get_result_json",
|
|
159
|
+
description: "Read the structured result.json for deterministic task acceptance.",
|
|
160
|
+
inputSchema: {
|
|
161
|
+
type: "object",
|
|
162
|
+
properties: {
|
|
163
|
+
task_id: { type: "string", description: "Task ID" },
|
|
164
|
+
},
|
|
165
|
+
required: ["task_id"],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "get_diff",
|
|
170
|
+
description: "Read task diff evidence, or return structured availability and watcher evidence while it is not ready.",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: "object",
|
|
173
|
+
properties: {
|
|
174
|
+
task_id: { type: "string", description: "Task ID" },
|
|
175
|
+
},
|
|
176
|
+
required: ["task_id"],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "get_test_log",
|
|
181
|
+
description: "Read test.log, or return structured availability and watcher evidence while it is not ready.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
task_id: { type: "string", description: "Task ID" },
|
|
186
|
+
},
|
|
187
|
+
required: ["task_id"],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "list_workspace",
|
|
192
|
+
description: "List files and directories within the workspace (sensitive files excluded).",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: {
|
|
196
|
+
path: {
|
|
197
|
+
type: "string",
|
|
198
|
+
description: "Optional relative path within workspace (default: root)",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "read_workspace_file",
|
|
205
|
+
description: "Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked.",
|
|
206
|
+
inputSchema: {
|
|
207
|
+
type: "object",
|
|
208
|
+
properties: {
|
|
209
|
+
path: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Relative path to a file inside the workspace",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
required: ["path"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "list_tasks",
|
|
219
|
+
description: "List recent tasks with status/repo/active filters plus watcher state and computed pending reasons.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
status: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Filter by status: pending, running, done, failed, failed_verification, failed_scope_violation, failed_policy_violation, canceled",
|
|
226
|
+
},
|
|
227
|
+
limit: {
|
|
228
|
+
type: "number",
|
|
229
|
+
description: "Max tasks to return (default 20, max 100)",
|
|
230
|
+
},
|
|
231
|
+
repo_path: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Optional exact repo_path or resolved_repo_path filter.",
|
|
234
|
+
},
|
|
235
|
+
active_only: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
description: "When true, return only pending and running tasks.",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "cancel_task",
|
|
244
|
+
description: "Request graceful cancellation. The runner that owns the child process performs termination; the MCP server never kills a PID read from task files.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
task_id: { type: "string", description: "Task ID to cancel" },
|
|
249
|
+
},
|
|
250
|
+
required: ["task_id"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "kill_task",
|
|
255
|
+
description: "Request immediate termination of a pending or running task. The runner validates and kills only the child process it owns.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
task_id: { type: "string", description: "Task ID to terminate" },
|
|
260
|
+
},
|
|
261
|
+
required: ["task_id"],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "retry_task",
|
|
266
|
+
description: "Create a new task with the same plan, agent, repo_path, and test_command as an existing task. The original task is unchanged.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
task_id: { type: "string", description: "Task ID to retry" },
|
|
271
|
+
},
|
|
272
|
+
required: ["task_id"],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: "get_task_stdout_tail",
|
|
277
|
+
description: "Read the last N lines of agent stdout/stderr. Reads from real-time stdout.log/stderr.log during execution, falls back to result.md after completion. Works on pending, running, and completed tasks. Default 80 lines.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
task_id: { type: "string", description: "Task ID" },
|
|
282
|
+
lines: { type: "number", description: "Tail line count (default 80, max 200)" },
|
|
283
|
+
},
|
|
284
|
+
required: ["task_id"],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
name: "get_task_log_tail",
|
|
289
|
+
description: "Read the last N lines of a task log file (stdout/stderr/test/verify) with automatic secret redaction. Default 80 lines, max 200. Always returns tail only — never the full file. Use this instead of read_workspace_file to avoid triggering platform content filters on log output.",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
task_id: { type: "string", description: "Task ID" },
|
|
294
|
+
file: {
|
|
295
|
+
type: "string",
|
|
296
|
+
enum: ["stdout", "stderr", "test", "verify"],
|
|
297
|
+
description: "Log file to read: stdout (stdout.log), stderr (stderr.log), test (test.log), verify (verify.log)",
|
|
298
|
+
},
|
|
299
|
+
lines: { type: "number", description: "Tail line count (default 80, max 200)" },
|
|
300
|
+
redact: { type: "boolean", description: "Apply secret redaction (default true)" },
|
|
301
|
+
},
|
|
302
|
+
required: ["task_id", "file"],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "get_task_progress",
|
|
307
|
+
description: "Read progress.md for task phases and the most recent heartbeat/current command.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
task_id: { type: "string", description: "Task ID" },
|
|
312
|
+
},
|
|
313
|
+
required: ["task_id"],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "wait_for_task",
|
|
318
|
+
description: "Long-poll a task for up to 30 seconds. If continuation_required=true, call wait_for_task again immediately and do not finish the assistant turn. Terminal responses include get_task_summary acceptance evidence.",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
task_id: { type: "string", description: "Task ID from create_task" },
|
|
323
|
+
wait_seconds: { type: "integer", minimum: 1, maximum: 30, default: 25 },
|
|
324
|
+
timeout_seconds: {
|
|
325
|
+
type: "integer",
|
|
326
|
+
minimum: 1,
|
|
327
|
+
maximum: 30,
|
|
328
|
+
description: "Preferred alias for wait_seconds. Maximum 30 seconds to stay within connector request limits.",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
required: ["task_id"],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "get_task_summary",
|
|
336
|
+
description: "Return one structured acceptance summary: terminal status, scope violations, verification evidence, changed files, artifact availability, warnings, and errors.",
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: "object",
|
|
339
|
+
properties: {
|
|
340
|
+
task_id: { type: "string", description: "Task ID" },
|
|
341
|
+
},
|
|
342
|
+
required: ["task_id"],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "audit_task",
|
|
347
|
+
description: "Independently audit a task's outputs. Verifies status, result.md, test.log, git.diff, repo_path consistency, cross-references agent claims with package.json scripts, and flags unverified release/publish claims. Writes independent-review.md to the task directory.",
|
|
348
|
+
inputSchema: {
|
|
349
|
+
type: "object",
|
|
350
|
+
properties: {
|
|
351
|
+
task_id: { type: "string", description: "Task ID to audit" },
|
|
352
|
+
},
|
|
353
|
+
required: ["task_id"],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
// run_task: only available when explicitly enabled
|
|
358
|
+
if (config.enableRunTaskTool === true) {
|
|
359
|
+
tools.push({
|
|
360
|
+
name: "run_task",
|
|
361
|
+
description: "Manually trigger execution of a pending task. WARNING: requires enableRunTaskTool=true in config. Prefer using the local watcher instead.",
|
|
362
|
+
inputSchema: {
|
|
363
|
+
type: "object",
|
|
364
|
+
properties: {
|
|
365
|
+
task_id: { type: "string", description: "Task ID to execute" },
|
|
366
|
+
},
|
|
367
|
+
required: ["task_id"],
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const profile = resolveToolProfile(config.toolProfile);
|
|
372
|
+
const selected = selectToolsForProfile(tools, profile);
|
|
373
|
+
buildToolCatalogSnapshot(selected, profile);
|
|
374
|
+
return selected;
|
|
375
|
+
}
|
|
376
|
+
export function getToolCatalogSnapshot() {
|
|
377
|
+
const tools = getToolDefs();
|
|
378
|
+
const config = getConfig();
|
|
379
|
+
return buildToolCatalogSnapshot(tools, resolveToolProfile(config.toolProfile));
|
|
380
|
+
}
|
|
381
|
+
// ── Request handler ───────────────────────────────────────────────
|
|
382
|
+
export async function handleToolCall(name, args) {
|
|
383
|
+
switch (name) {
|
|
384
|
+
case "save_plan": {
|
|
385
|
+
return toResult(savePlan({
|
|
386
|
+
title: String(args?.title ?? ""),
|
|
387
|
+
content: String(args?.content ?? ""),
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
case "get_plan": {
|
|
391
|
+
return toResult(getPlan({ plan_id: String(args?.plan_id ?? "") }));
|
|
392
|
+
}
|
|
393
|
+
case "create_task": {
|
|
394
|
+
return toResult(createTask({
|
|
395
|
+
plan_id: args?.plan_id ? String(args.plan_id) : undefined,
|
|
396
|
+
inline_plan: args?.inline_plan ? String(args.inline_plan) : undefined,
|
|
397
|
+
plan_title: args?.plan_title ? String(args.plan_title) : undefined,
|
|
398
|
+
template: args?.template ? String(args.template) : undefined,
|
|
399
|
+
goal: args?.goal ? String(args.goal) : undefined,
|
|
400
|
+
source_task_id: args?.source_task_id ? String(args.source_task_id) : undefined,
|
|
401
|
+
agent: String(args?.agent ?? ""),
|
|
402
|
+
repo_path: args?.repo_path ? String(args.repo_path) : undefined,
|
|
403
|
+
test_command: args?.test_command ? String(args.test_command) : undefined,
|
|
404
|
+
verify_commands: Array.isArray(args?.verify_commands)
|
|
405
|
+
? args.verify_commands.map((command) => String(command))
|
|
406
|
+
: undefined,
|
|
407
|
+
timeout_seconds: args?.timeout_seconds !== undefined
|
|
408
|
+
? Number(args.timeout_seconds)
|
|
409
|
+
: undefined,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
case "get_task_status": {
|
|
413
|
+
return toResult(getTaskStatus(String(args?.task_id ?? "")));
|
|
414
|
+
}
|
|
415
|
+
case "get_result": {
|
|
416
|
+
return toResult(getResult(String(args?.task_id ?? "")));
|
|
417
|
+
}
|
|
418
|
+
case "get_result_json": {
|
|
419
|
+
return toResult(getResultJson(String(args?.task_id ?? "")));
|
|
420
|
+
}
|
|
421
|
+
case "get_diff": {
|
|
422
|
+
return toResult(getDiff(String(args?.task_id ?? "")));
|
|
423
|
+
}
|
|
424
|
+
case "get_test_log": {
|
|
425
|
+
return toResult(getTestLog(String(args?.task_id ?? "")));
|
|
426
|
+
}
|
|
427
|
+
case "list_workspace": {
|
|
428
|
+
return toResult(listWorkspace(args?.path ? String(args.path) : undefined));
|
|
429
|
+
}
|
|
430
|
+
case "read_workspace_file": {
|
|
431
|
+
return toResult(readWorkspaceFile(String(args?.path ?? "")));
|
|
432
|
+
}
|
|
433
|
+
case "list_tasks": {
|
|
434
|
+
return toResult(listTasks({
|
|
435
|
+
status: args?.status ? String(args.status) : undefined,
|
|
436
|
+
repo_path: args?.repo_path ? String(args.repo_path) : undefined,
|
|
437
|
+
active_only: args?.active_only !== undefined ? Boolean(args.active_only) : undefined,
|
|
438
|
+
limit: args?.limit ? Number(args.limit) : undefined,
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
case "list_agents": {
|
|
442
|
+
return toResult(listAgents());
|
|
443
|
+
}
|
|
444
|
+
case "health_check": {
|
|
445
|
+
return toResult(healthCheck(getToolCatalogSnapshot(), {
|
|
446
|
+
detail: args?.detail === "self_diagnostic" ? "self_diagnostic" : "standard",
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
case "cancel_task": {
|
|
450
|
+
return toResult(cancelTask(String(args?.task_id ?? "")));
|
|
451
|
+
}
|
|
452
|
+
case "kill_task": {
|
|
453
|
+
return toResult(killTask(String(args?.task_id ?? "")));
|
|
454
|
+
}
|
|
455
|
+
case "retry_task": {
|
|
456
|
+
return toResult(retryTask(String(args?.task_id ?? "")));
|
|
457
|
+
}
|
|
458
|
+
case "get_task_stdout_tail": {
|
|
459
|
+
return toResult(getTaskStdoutTail(String(args?.task_id ?? ""), args?.lines ? Number(args.lines) : undefined));
|
|
460
|
+
}
|
|
461
|
+
case "get_task_log_tail": {
|
|
462
|
+
return toResult(getTaskLogTail(String(args?.task_id ?? ""), args?.file || "stdout", {
|
|
463
|
+
lines: args?.lines ? Number(args.lines) : undefined,
|
|
464
|
+
redact: args?.redact !== undefined ? Boolean(args.redact) : undefined,
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
case "get_task_progress": {
|
|
468
|
+
return toResult(getTaskProgress(String(args?.task_id ?? "")));
|
|
469
|
+
}
|
|
470
|
+
case "wait_for_task": {
|
|
471
|
+
const waitSeconds = normalizeWaitSeconds(args);
|
|
472
|
+
return toResult(await waitForTask(String(args?.task_id ?? ""), waitSeconds));
|
|
473
|
+
}
|
|
474
|
+
case "get_task_summary": {
|
|
475
|
+
return toResult(getTaskSummary(String(args?.task_id ?? "")));
|
|
476
|
+
}
|
|
477
|
+
case "audit_task": {
|
|
478
|
+
return toResult(auditTask(String(args?.task_id ?? "")));
|
|
479
|
+
}
|
|
480
|
+
case "run_task": {
|
|
481
|
+
const config = getConfig();
|
|
482
|
+
if (config.enableRunTaskTool !== true) {
|
|
483
|
+
throw new Error("run_task is disabled. Set enableRunTaskTool: true in config to enable. Prefer using the local watcher (npm run watch).");
|
|
484
|
+
}
|
|
485
|
+
const taskId = String(args?.task_id ?? "");
|
|
486
|
+
const result = await runTask(taskId);
|
|
487
|
+
return toResult(result);
|
|
488
|
+
}
|
|
489
|
+
default:
|
|
490
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function normalizeWaitSeconds(args) {
|
|
494
|
+
const legacy = args?.wait_seconds;
|
|
495
|
+
const preferred = args?.timeout_seconds;
|
|
496
|
+
if (legacy !== undefined && preferred !== undefined && Number(legacy) !== Number(preferred)) {
|
|
497
|
+
throw new Error("wait_seconds and timeout_seconds must match when both are supplied.");
|
|
498
|
+
}
|
|
499
|
+
const value = preferred ?? legacy;
|
|
500
|
+
return value === undefined ? undefined : Number(value);
|
|
501
|
+
}
|
|
502
|
+
function toResult(data) {
|
|
503
|
+
return {
|
|
504
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
// ── Register on MCP Server ────────────────────────────────────────
|
|
508
|
+
export function registerTools(server) {
|
|
509
|
+
// Compute the active tool list ONCE to guarantee list/call consistency.
|
|
510
|
+
// Re-calling getToolDefs() on every request risks divergence between
|
|
511
|
+
// tools/list and tools/call when the profile is reconfigured at runtime.
|
|
512
|
+
const activeTools = getToolDefs();
|
|
513
|
+
const activeNames = new Set(activeTools.map((tool) => tool.name));
|
|
514
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
515
|
+
const catalog = getLastToolCatalogSnapshot();
|
|
516
|
+
return {
|
|
517
|
+
tools: activeTools,
|
|
518
|
+
...(catalog
|
|
519
|
+
? {
|
|
520
|
+
_meta: {
|
|
521
|
+
server_version: catalog.server_version,
|
|
522
|
+
schema_epoch: catalog.schema_epoch,
|
|
523
|
+
tool_profile: catalog.tool_profile,
|
|
524
|
+
tool_count: catalog.tool_count,
|
|
525
|
+
tool_manifest_sha256: catalog.tool_manifest_sha256,
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
: {}),
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
532
|
+
const { name, arguments: args } = request.params;
|
|
533
|
+
try {
|
|
534
|
+
if (!activeNames.has(name)) {
|
|
535
|
+
const catalog = getToolCatalogSnapshot();
|
|
536
|
+
throw new PatchWardenError("tool_catalog_mismatch", `Tool "${name}" is not available in the active ${catalog.tool_profile} profile. The client may be using a stale tool catalog.`, "Refresh or reconnect the ChatGPT Connector and open a new conversation before retrying.", true, {
|
|
537
|
+
requested_tool: name,
|
|
538
|
+
refresh_required: true,
|
|
539
|
+
server_version: catalog.server_version,
|
|
540
|
+
schema_epoch: catalog.schema_epoch,
|
|
541
|
+
tool_profile: catalog.tool_profile,
|
|
542
|
+
tool_count: catalog.tool_count,
|
|
543
|
+
tool_names: catalog.tool_names,
|
|
544
|
+
tool_manifest_sha256: catalog.tool_manifest_sha256,
|
|
545
|
+
next_tool_call: {
|
|
546
|
+
name: "health_check",
|
|
547
|
+
arguments: { detail: "self_diagnostic" },
|
|
548
|
+
},
|
|
549
|
+
connector_refresh_steps: [
|
|
550
|
+
"1. Run Check-PatchWarden-Health.cmd locally to confirm the active profile and manifest hash.",
|
|
551
|
+
"2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
|
|
552
|
+
"3. Open a NEW ChatGPT conversation; old conversations retain their cached tool catalog.",
|
|
553
|
+
"4. Call health_check in the new conversation and verify tool_manifest_sha256 matches the local report.",
|
|
554
|
+
],
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return await handleToolCall(name, args);
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
return {
|
|
561
|
+
content: [{ type: "text", text: JSON.stringify(errorPayload(err)) }],
|
|
562
|
+
isError: true,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getTasksDir, getConfig } from "../config.js";
|
|
4
|
+
import { guardReadPath } from "../security/pathGuard.js";
|
|
5
|
+
import { createTask } from "./createTask.js";
|
|
6
|
+
export function retryTask(taskId) {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
const tasksDir = getTasksDir(config);
|
|
9
|
+
const taskDir = join(tasksDir, taskId);
|
|
10
|
+
const statusFile = join(taskDir, "status.json");
|
|
11
|
+
guardReadPath(statusFile, config.workspaceRoot, config.tasksDir);
|
|
12
|
+
if (!existsSync(statusFile)) {
|
|
13
|
+
throw new Error(`Task not found: "${taskId}"`);
|
|
14
|
+
}
|
|
15
|
+
const data = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
16
|
+
if (!data.plan_id || !data.agent) {
|
|
17
|
+
throw new Error(`Task "${taskId}" is missing plan_id or agent. Cannot retry.`);
|
|
18
|
+
}
|
|
19
|
+
// Create a new task with the same parameters
|
|
20
|
+
const newTask = createTask({
|
|
21
|
+
plan_id: data.plan_id,
|
|
22
|
+
agent: data.agent,
|
|
23
|
+
repo_path: data.repo_path,
|
|
24
|
+
test_command: data.test_command,
|
|
25
|
+
verify_commands: data.verify_commands,
|
|
26
|
+
timeout_seconds: data.timeout_seconds,
|
|
27
|
+
});
|
|
28
|
+
// Record retry relationship in the new task
|
|
29
|
+
const newStatusFile = join(newTask.path, "status.json");
|
|
30
|
+
const newData = JSON.parse(readFileSync(newStatusFile, "utf-8"));
|
|
31
|
+
newData.retry_of = taskId;
|
|
32
|
+
newData.retry_count = (data.retry_count || 0) + 1;
|
|
33
|
+
newData.plan_source = data.plan_source || "saved";
|
|
34
|
+
newData.template = data.template || null;
|
|
35
|
+
newData.change_policy = data.change_policy || "repo_scoped_changes";
|
|
36
|
+
writeFileSync(newStatusFile, JSON.stringify(newData, null, 2), "utf-8");
|
|
37
|
+
return {
|
|
38
|
+
original_task_id: taskId,
|
|
39
|
+
new_task_id: newTask.task_id,
|
|
40
|
+
plan_id: newTask.plan_id,
|
|
41
|
+
agent: newTask.agent,
|
|
42
|
+
status: newTask.status,
|
|
43
|
+
message: `New task created from retry of ${taskId}. Original task is unchanged.`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { getPlansDir, getConfig } from "../config.js";
|
|
4
|
+
import { guardPath } from "../security/pathGuard.js";
|
|
5
|
+
import { guardPlanContent } from "../security/planGuard.js";
|
|
6
|
+
export function savePlan(input) {
|
|
7
|
+
const config = getConfig();
|
|
8
|
+
const plansDir = getPlansDir(config);
|
|
9
|
+
guardPlanContent(input.title, input.content);
|
|
10
|
+
const planId = `plan_${Date.now()}_${sanitizeTitle(input.title)}`;
|
|
11
|
+
const planDir = resolve(plansDir, planId);
|
|
12
|
+
// Guards: plan dir & file must stay inside workspace
|
|
13
|
+
guardPath(planDir, config.workspaceRoot, config.plansDir);
|
|
14
|
+
mkdirSync(planDir, { recursive: true });
|
|
15
|
+
const planFile = join(planDir, "plan.md");
|
|
16
|
+
const header = `# ${input.title}\n\n> Plan ID: ${planId}\n> Created: ${new Date().toISOString()}\n\n`;
|
|
17
|
+
writeFileSync(planFile, header + input.content, "utf-8");
|
|
18
|
+
return {
|
|
19
|
+
plan_id: planId,
|
|
20
|
+
path: planFile,
|
|
21
|
+
title: input.title,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function sanitizeTitle(title) {
|
|
25
|
+
return title
|
|
26
|
+
.replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, "_")
|
|
27
|
+
.slice(0, 64);
|
|
28
|
+
}
|