patchwarden 0.4.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/PatchWarden.cmd +51 -0
  2. package/README.en.md +1022 -0
  3. package/README.md +897 -358
  4. package/dist/assessments/agentAssessor.d.ts +15 -0
  5. package/dist/assessments/agentAssessor.js +293 -0
  6. package/dist/assessments/assessmentStore.d.ts +133 -0
  7. package/dist/assessments/assessmentStore.js +238 -0
  8. package/dist/assessments/confirmCli.d.ts +9 -0
  9. package/dist/assessments/confirmCli.js +26 -0
  10. package/dist/config.d.ts +22 -1
  11. package/dist/config.js +156 -3
  12. package/dist/direct/directAudit.d.ts +23 -0
  13. package/dist/direct/directAudit.js +309 -0
  14. package/dist/direct/directGuards.d.ts +20 -0
  15. package/dist/direct/directGuards.js +137 -0
  16. package/dist/direct/directPatch.d.ts +17 -0
  17. package/dist/direct/directPatch.js +113 -0
  18. package/dist/direct/directSessionStore.d.ts +63 -0
  19. package/dist/direct/directSessionStore.js +192 -0
  20. package/dist/direct/directVerification.d.ts +12 -0
  21. package/dist/direct/directVerification.js +96 -0
  22. package/dist/doctor.js +73 -5
  23. package/dist/logging.d.ts +52 -0
  24. package/dist/logging.js +123 -0
  25. package/dist/runner/agentInvocation.d.ts +23 -0
  26. package/dist/runner/agentInvocation.js +106 -0
  27. package/dist/runner/changeCapture.d.ts +70 -0
  28. package/dist/runner/changeCapture.js +278 -6
  29. package/dist/runner/runTask.js +271 -65
  30. package/dist/runner/simpleProcess.d.ts +21 -0
  31. package/dist/runner/simpleProcess.js +206 -0
  32. package/dist/runner/watch.js +17 -2
  33. package/dist/security/commandGuard.d.ts +2 -1
  34. package/dist/security/commandGuard.js +25 -3
  35. package/dist/security/riskEngine.d.ts +27 -0
  36. package/dist/security/riskEngine.js +118 -0
  37. package/dist/security/runtimeGuard.d.ts +6 -0
  38. package/dist/security/runtimeGuard.js +28 -0
  39. package/dist/smoke-test.js +1353 -7
  40. package/dist/test/unit/android-doctor.test.d.ts +1 -0
  41. package/dist/test/unit/android-doctor.test.js +118 -0
  42. package/dist/test/unit/chinese-path.test.d.ts +1 -0
  43. package/dist/test/unit/chinese-path.test.js +91 -0
  44. package/dist/test/unit/command-guard.test.d.ts +1 -0
  45. package/dist/test/unit/command-guard.test.js +160 -0
  46. package/dist/test/unit/direct-guards.test.d.ts +1 -0
  47. package/dist/test/unit/direct-guards.test.js +213 -0
  48. package/dist/test/unit/logging.test.d.ts +1 -0
  49. package/dist/test/unit/logging.test.js +275 -0
  50. package/dist/test/unit/path-guard.test.d.ts +1 -0
  51. package/dist/test/unit/path-guard.test.js +109 -0
  52. package/dist/test/unit/safe-status.test.d.ts +1 -0
  53. package/dist/test/unit/safe-status.test.js +165 -0
  54. package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
  55. package/dist/test/unit/sensitive-guard.test.js +104 -0
  56. package/dist/test/unit/sync-file.test.d.ts +1 -0
  57. package/dist/test/unit/sync-file.test.js +154 -0
  58. package/dist/test/unit/watcher-status.test.d.ts +1 -0
  59. package/dist/test/unit/watcher-status.test.js +169 -0
  60. package/dist/tools/androidDoctor.d.ts +38 -0
  61. package/dist/tools/androidDoctor.js +391 -0
  62. package/dist/tools/applyPatch.d.ts +16 -0
  63. package/dist/tools/applyPatch.js +41 -0
  64. package/dist/tools/auditSession.d.ts +5 -0
  65. package/dist/tools/auditSession.js +12 -0
  66. package/dist/tools/auditTask.d.ts +7 -0
  67. package/dist/tools/auditTask.js +105 -7
  68. package/dist/tools/createDirectSession.d.ts +14 -0
  69. package/dist/tools/createDirectSession.js +54 -0
  70. package/dist/tools/createTask.d.ts +48 -1
  71. package/dist/tools/createTask.js +298 -47
  72. package/dist/tools/finalizeDirectSession.d.ts +19 -0
  73. package/dist/tools/finalizeDirectSession.js +84 -0
  74. package/dist/tools/getTaskSummary.d.ts +50 -0
  75. package/dist/tools/getTaskSummary.js +79 -2
  76. package/dist/tools/healthCheck.d.ts +27 -12
  77. package/dist/tools/healthCheck.js +44 -6
  78. package/dist/tools/readWorkspaceFile.d.ts +7 -1
  79. package/dist/tools/readWorkspaceFile.js +48 -2
  80. package/dist/tools/registry.js +270 -19
  81. package/dist/tools/runVerification.d.ts +16 -0
  82. package/dist/tools/runVerification.js +32 -0
  83. package/dist/tools/safeStatus.d.ts +19 -0
  84. package/dist/tools/safeStatus.js +72 -0
  85. package/dist/tools/savePlan.d.ts +1 -0
  86. package/dist/tools/savePlan.js +38 -7
  87. package/dist/tools/searchWorkspace.d.ts +19 -0
  88. package/dist/tools/searchWorkspace.js +205 -0
  89. package/dist/tools/syncFile.d.ts +18 -0
  90. package/dist/tools/syncFile.js +65 -0
  91. package/dist/tools/taskOutputs.d.ts +2 -2
  92. package/dist/tools/taskTemplates.js +2 -1
  93. package/dist/tools/toolCatalog.d.ts +4 -3
  94. package/dist/tools/toolCatalog.js +34 -5
  95. package/dist/tools/waitForTask.d.ts +2 -2
  96. package/dist/tools/waitForTask.js +1 -1
  97. package/dist/version.d.ts +2 -2
  98. package/dist/version.js +2 -2
  99. package/dist/watcherStatus.d.ts +1 -0
  100. package/dist/watcherStatus.js +96 -4
  101. package/docs/chatgpt-usage.md +101 -0
  102. package/docs/performance-notes.md +55 -0
  103. package/docs/release-checklist.md +14 -0
  104. package/docs/release-v0.6.0.md +71 -0
  105. package/docs/release-v0.6.1.md +75 -0
  106. package/examples/config.example.json +5 -0
  107. package/examples/openai-tunnel/README.md +11 -4
  108. package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
  109. package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
  110. package/package.json +13 -8
  111. package/scripts/brand-check.js +58 -12
  112. package/scripts/control-smoke.js +206 -0
  113. package/scripts/http-mcp-smoke.js +10 -2
  114. package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
  115. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  116. package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
  117. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
  118. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
  119. package/scripts/lifecycle-smoke.js +402 -8
  120. package/scripts/manage-patchwarden.ps1 +639 -0
  121. package/scripts/mcp-manifest-check.js +123 -58
  122. package/scripts/mcp-smoke.js +160 -2
  123. package/scripts/pack-clean.js +160 -5
  124. package/scripts/package-manifest-check.js +78 -0
  125. package/scripts/patchwarden-mcp-direct.cmd +7 -0
  126. package/scripts/patchwarden-mcp-stdio.cmd +1 -1
  127. package/scripts/restart-patchwarden.ps1 +5 -4
  128. package/scripts/start-patchwarden-tunnel.ps1 +261 -30
  129. package/scripts/tunnel-supervisor-smoke.js +36 -4
  130. package/scripts/unit-tests.js +36 -0
  131. package/scripts/watcher-supervisor-smoke.js +10 -6
  132. package/src/assessments/agentAssessor.ts +324 -0
  133. package/src/assessments/assessmentStore.ts +426 -0
  134. package/src/assessments/confirmCli.ts +29 -0
  135. package/src/config.ts +178 -4
  136. package/src/direct/directAudit.ts +400 -0
  137. package/src/direct/directGuards.ts +279 -0
  138. package/src/direct/directPatch.ts +258 -0
  139. package/src/direct/directSessionStore.ts +345 -0
  140. package/src/direct/directVerification.ts +138 -0
  141. package/src/doctor.ts +103 -7
  142. package/src/logging.ts +152 -0
  143. package/src/runner/agentInvocation.ts +125 -0
  144. package/src/runner/changeCapture.ts +352 -6
  145. package/src/runner/runTask.ts +279 -63
  146. package/src/runner/simpleProcess.ts +223 -0
  147. package/src/runner/watch.ts +18 -2
  148. package/src/security/commandGuard.ts +46 -4
  149. package/src/security/riskEngine.ts +160 -0
  150. package/src/security/runtimeGuard.ts +41 -0
  151. package/src/smoke-test.ts +1291 -4
  152. package/src/test/unit/android-doctor.test.ts +158 -0
  153. package/src/test/unit/chinese-path.test.ts +106 -0
  154. package/src/test/unit/command-guard.test.ts +221 -0
  155. package/src/test/unit/direct-guards.test.ts +297 -0
  156. package/src/test/unit/logging.test.ts +325 -0
  157. package/src/test/unit/path-guard.test.ts +150 -0
  158. package/src/test/unit/safe-status.test.ts +187 -0
  159. package/src/test/unit/sensitive-guard.test.ts +124 -0
  160. package/src/test/unit/sync-file.test.ts +231 -0
  161. package/src/test/unit/watcher-status.test.ts +190 -0
  162. package/src/tools/androidDoctor.ts +424 -0
  163. package/src/tools/applyPatch.ts +86 -0
  164. package/src/tools/auditSession.ts +28 -0
  165. package/src/tools/auditTask.ts +111 -7
  166. package/src/tools/createDirectSession.ts +113 -0
  167. package/src/tools/createTask.ts +405 -55
  168. package/src/tools/finalizeDirectSession.ts +144 -0
  169. package/src/tools/getTaskSummary.ts +133 -2
  170. package/src/tools/healthCheck.ts +45 -6
  171. package/src/tools/readWorkspaceFile.ts +85 -2
  172. package/src/tools/registry.ts +305 -19
  173. package/src/tools/runVerification.ts +58 -0
  174. package/src/tools/safeStatus.ts +96 -0
  175. package/src/tools/savePlan.ts +57 -7
  176. package/src/tools/searchWorkspace.ts +275 -0
  177. package/src/tools/syncFile.ts +122 -0
  178. package/src/tools/taskTemplates.ts +2 -1
  179. package/src/tools/toolCatalog.ts +37 -6
  180. package/src/tools/waitForTask.ts +3 -3
  181. package/src/version.ts +2 -2
  182. package/src/watcherStatus.ts +101 -4
  183. package/tsconfig.json +18 -17
  184. package/Check-PatchWarden-Health.cmd +0 -6
  185. package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
  186. package/Restart-PatchWarden.cmd +0 -19
  187. package/Start-PatchWarden-Tunnel.cmd +0 -7
  188. package/docs/release-v0.3.0.md +0 -43
  189. package/docs/release-v0.4.0.md +0 -74
@@ -3,7 +3,7 @@
3
3
  * Used by both stdio (index.ts) and HTTP (httpServer.ts) transports.
4
4
  */
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
6
- import { getConfig } from "../config.js";
6
+ import { getAllConfiguredTestCommands, getAllConfiguredDirectCommands, getConfig } from "../config.js";
7
7
  import { savePlan } from "../tools/savePlan.js";
8
8
  import { getPlan } from "../tools/getPlan.js";
9
9
  import { createTask } from "../tools/createTask.js";
@@ -23,7 +23,16 @@ import { getTaskSummary } from "../tools/getTaskSummary.js";
23
23
  import { waitForTask } from "../tools/waitForTask.js";
24
24
  import { errorPayload, PatchWardenError } from "../errors.js";
25
25
  import { auditTask } from "../tools/auditTask.js";
26
+ import { safeStatus } from "../tools/safeStatus.js";
27
+ import { logger } from "../logging.js";
26
28
  import { runTask } from "../runner/runTask.js";
29
+ import { createDirectSession } from "../tools/createDirectSession.js";
30
+ import { searchWorkspace } from "../tools/searchWorkspace.js";
31
+ import { applyPatch } from "../tools/applyPatch.js";
32
+ import { runVerification } from "../tools/runVerification.js";
33
+ import { finalizeDirectSession } from "../tools/finalizeDirectSession.js";
34
+ import { auditSession } from "../tools/auditSession.js";
35
+ import { syncFile } from "../tools/syncFile.js";
27
36
  import { TASK_TEMPLATE_NAMES } from "./taskTemplates.js";
28
37
  import { buildToolCatalogSnapshot, getLastToolCatalogSnapshot, resolveToolProfile, selectToolsForProfile, } from "./toolCatalog.js";
29
38
  export function getToolDefs() {
@@ -32,18 +41,18 @@ export function getToolDefs() {
32
41
  const agentDescription = agentNames.length > 0
33
42
  ? `Configured local agent name. Available agents: ${agentNames.map((name) => JSON.stringify(name)).join(", ")}`
34
43
  : "Configured local agent name. No agents are currently configured.";
35
- const testCommands = [...config.allowedTestCommands].sort();
44
+ const testCommands = getAllConfiguredTestCommands(config).sort();
36
45
  const tools = [
37
46
  {
38
47
  name: "save_plan",
39
- description: "Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution.",
48
+ description: "Save an execution plan — ChatGPT writes the plan, PatchWarden stores it for local agent execution. Supports plan_ref to load a plan file already placed inside .patchwarden/plans.",
40
49
  inputSchema: {
41
50
  type: "object",
42
51
  properties: {
43
- title: { type: "string", description: "Plan title" },
44
- content: { type: "string", description: "Plan content in Markdown" },
52
+ title: { type: "string", description: "Plan title. Defaults to 'Inline plan' or 'Plan from file' when omitted." },
53
+ content: { type: "string", description: "Plan content in Markdown. Required unless plan_ref is provided." },
54
+ plan_ref: { type: "string", description: "Relative path to a plan file already inside .patchwarden/plans. When provided, the file content is loaded and title defaults to 'Plan from file' if title is empty." },
45
55
  },
46
- required: ["title", "content"],
47
56
  },
48
57
  },
49
58
  {
@@ -82,7 +91,7 @@ export function getToolDefs() {
82
91
  },
83
92
  {
84
93
  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.",
94
+ description: "Create a repo-scoped task from exactly one source. For ChatGPT, prefer the guarded inspect_only, feature_small, or fix_tests template when it fits. A stale watcher preserves the task but returns execution_blocked and directs the client to health_check; otherwise call wait_for_task until terminal. Use execution_mode=assess_only to pre-assess risk and get an assessment_id without creating a task; then invoke the returned next_tool_call using only execution_mode=execute and the full assessment_id.",
86
95
  inputSchema: {
87
96
  type: "object",
88
97
  properties: {
@@ -92,7 +101,7 @@ export function getToolDefs() {
92
101
  template: {
93
102
  type: "string",
94
103
  enum: [...TASK_TEMPLATE_NAMES],
95
- description: "Built-in guarded task template. Use with goal; rollback_scope_violation also requires source_task_id.",
104
+ description: "Built-in guarded task template. ChatGPT should prefer inspect_only for diagnosis, feature_small for a scoped change, and fix_tests for known failing verification. Use with goal; rollback_scope_violation also requires source_task_id.",
96
105
  },
97
106
  goal: { type: "string", description: "Required task goal when template is supplied." },
98
107
  source_task_id: { type: "string", description: "Required source task for rollback_scope_violation review." },
@@ -119,7 +128,7 @@ export function getToolDefs() {
119
128
  type: "string",
120
129
  ...(testCommands.length > 0 ? { enum: testCommands } : {}),
121
130
  },
122
- description: "Recommended allow-listed commands PatchWarden runs independently after the agent exits.",
131
+ description: "Recommended exact-match commands PatchWarden runs independently after the agent exits. Repository-scoped commands are re-authorized after repo_path is resolved.",
123
132
  },
124
133
  timeout_seconds: {
125
134
  type: "integer",
@@ -128,8 +137,18 @@ export function getToolDefs() {
128
137
  default: config.defaultTaskTimeoutSeconds,
129
138
  description: `Total task timeout in seconds (default ${config.defaultTaskTimeoutSeconds}, max ${config.maxTaskTimeoutSeconds})`,
130
139
  },
140
+ execution_mode: {
141
+ type: "string",
142
+ enum: ["assess_only", "execute"],
143
+ default: "execute",
144
+ description: "assess_only: run deterministic risk checks and return an assessment_id without creating a task. execute (default): create and queue the task. When combined with assessment_id, the task parameters are loaded from the assessment record and freshness is revalidated.",
145
+ },
146
+ assessment_id: {
147
+ type: "string",
148
+ description: "Assessment ID from a prior assess_only call. When provided with execution_mode=execute, task parameters are loaded from the assessment record and locked to it. The full 128-bit ID (32 hex chars) must be provided; short IDs are display-only.",
149
+ },
131
150
  },
132
- required: ["agent", "repo_path"],
151
+ required: [],
133
152
  },
134
153
  },
135
154
  {
@@ -202,13 +221,17 @@ export function getToolDefs() {
202
221
  },
203
222
  {
204
223
  name: "read_workspace_file",
205
- description: "Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked.",
224
+ description: "Read a file within the workspace. Sensitive files (secrets, keys, tokens) are blocked. In Direct mode (with session_id), reads are scoped to the session's repo_path and return sha256.",
206
225
  inputSchema: {
207
226
  type: "object",
208
227
  properties: {
209
228
  path: {
210
229
  type: "string",
211
- description: "Relative path to a file inside the workspace",
230
+ description: "Relative path to a file inside the workspace or session repo",
231
+ },
232
+ session_id: {
233
+ type: "string",
234
+ description: "Optional Direct session ID. When provided, read scope is limited to the session's repo_path and sha256 is returned.",
212
235
  },
213
236
  },
214
237
  required: ["path"],
@@ -333,18 +356,20 @@ export function getToolDefs() {
333
356
  },
334
357
  {
335
358
  name: "get_task_summary",
336
- description: "Return one structured acceptance summary: terminal status, scope violations, verification evidence, changed files, artifact availability, warnings, and errors.",
359
+ description: "Return structured acceptance evidence. Use view=compact first for bounded counts and risk excerpts; use standard only when full changed-file and log-tail detail is required.",
337
360
  inputSchema: {
338
361
  type: "object",
339
362
  properties: {
340
363
  task_id: { type: "string", description: "Task ID" },
364
+ view: { type: "string", enum: ["compact", "standard"], default: "standard", description: "Compact returns bounded acceptance evidence; standard preserves the full legacy summary." },
365
+ max_items: { type: "integer", minimum: 1, maximum: 50, default: 8, description: "Maximum entries per compact evidence group." },
341
366
  },
342
367
  required: ["task_id"],
343
368
  },
344
369
  },
345
370
  {
346
371
  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.",
372
+ 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. Evidence-backed failures, possible heuristic false positives, and manual-verification items are returned separately. Writes independent-review.md to the task directory.",
348
373
  inputSchema: {
349
374
  type: "object",
350
375
  properties: {
@@ -353,7 +378,138 @@ export function getToolDefs() {
353
378
  required: ["task_id"],
354
379
  },
355
380
  },
381
+ {
382
+ name: "safe_status",
383
+ description: "Return minimal task lifecycle status without exposing diff, log content, file contents, or sensitive paths. Use this when only task state is needed and content-bearing tools may be blocked by upper-layer security.",
384
+ inputSchema: {
385
+ type: "object",
386
+ properties: {
387
+ task_id: { type: "string", description: "Task ID to check" },
388
+ },
389
+ required: ["task_id"],
390
+ },
391
+ },
356
392
  ];
393
+ // Direct session tools
394
+ const directCommands = getAllConfiguredDirectCommands(config);
395
+ tools.push({
396
+ name: "create_direct_session",
397
+ description: "Create a Direct editing session for ChatGPT to apply patches directly. Requires enableDirectProfile: true in config.",
398
+ inputSchema: {
399
+ type: "object",
400
+ properties: {
401
+ repo_path: {
402
+ type: "string",
403
+ description: "Repository path inside workspaceRoot (e.g., 'my-project')",
404
+ },
405
+ title: {
406
+ type: "string",
407
+ description: "Optional title describing the session's purpose",
408
+ },
409
+ },
410
+ required: ["repo_path"],
411
+ },
412
+ });
413
+ tools.push({
414
+ name: "search_workspace",
415
+ description: "Search file contents (grep-like) within a Direct session's repo_path. Skips .git, node_modules, dist, release, and sensitive files.",
416
+ inputSchema: {
417
+ type: "object",
418
+ properties: {
419
+ session_id: { type: "string", description: "Session ID from create_direct_session" },
420
+ query: { type: "string", description: "Search query string" },
421
+ max_results: { type: "number", description: "Max results (default 20)" },
422
+ case_sensitive: { type: "boolean", description: "Case sensitive search (default false)" },
423
+ max_preview_chars: { type: "number", description: "Max preview chars per match (default 200)" },
424
+ include_globs: {
425
+ type: "array",
426
+ items: { type: "string" },
427
+ description: "Optional file name glob patterns to include (e.g., ['*.ts', '*.js'])",
428
+ },
429
+ },
430
+ required: ["session_id", "query"],
431
+ },
432
+ });
433
+ tools.push({
434
+ name: "apply_patch",
435
+ description: "Apply JSON patch operations to a file within a Direct session's repo_path. Validates expected_sha256 before applying. Supports replace_exact, insert_before, insert_after, replace_whole_file.",
436
+ inputSchema: {
437
+ type: "object",
438
+ properties: {
439
+ session_id: { type: "string", description: "Session ID" },
440
+ path: { type: "string", description: "Relative file path within the session repo" },
441
+ expected_sha256: { type: "string", description: "Expected SHA-256 hash of the current file content" },
442
+ operations: {
443
+ type: "array",
444
+ items: {
445
+ type: "object",
446
+ properties: {
447
+ type: { type: "string", enum: ["replace_exact", "insert_before", "insert_after", "replace_whole_file"] },
448
+ old_text: { type: "string", description: "Text to find (required for replace_exact, insert_before, insert_after)" },
449
+ new_text: { type: "string", description: "Replacement or insertion text" },
450
+ occurrence: { type: "string", enum: ["first", "all", "exactly_once"], description: "Match mode for replace_exact (default first)" },
451
+ },
452
+ required: ["type", "new_text"],
453
+ },
454
+ },
455
+ },
456
+ required: ["session_id", "path", "expected_sha256", "operations"],
457
+ },
458
+ });
459
+ tools.push({
460
+ name: "run_verification",
461
+ description: "Run a whitelisted verification command within a Direct session. Command must be in the Direct allowlist.",
462
+ inputSchema: {
463
+ type: "object",
464
+ properties: {
465
+ session_id: { type: "string", description: "Session ID" },
466
+ command: {
467
+ type: "string",
468
+ description: "Verification command to run",
469
+ ...(directCommands.length > 0 ? { enum: directCommands } : {}),
470
+ },
471
+ timeout_seconds: { type: "number", description: "Timeout in seconds (default 120)" },
472
+ },
473
+ required: ["session_id", "command"],
474
+ },
475
+ });
476
+ tools.push({
477
+ name: "finalize_direct_session",
478
+ description: "Finalize a Direct session: capture after snapshot, generate diff/summary/change artifacts, mark session as finalized. Must be called before audit_session.",
479
+ inputSchema: {
480
+ type: "object",
481
+ properties: {
482
+ session_id: { type: "string", description: "Session ID to finalize" },
483
+ },
484
+ required: ["session_id"],
485
+ },
486
+ });
487
+ tools.push({
488
+ name: "audit_session",
489
+ description: "Independently audit a Direct session's changes. Performs 16 deterministic checks and returns pass/warn/fail decision. Requires session to be finalized first.",
490
+ inputSchema: {
491
+ type: "object",
492
+ properties: {
493
+ session_id: { type: "string", description: "Session ID to audit" },
494
+ },
495
+ required: ["session_id"],
496
+ },
497
+ });
498
+ tools.push({
499
+ name: "sync_file",
500
+ description: "Copy a file from source to target within the same Direct session repo. Both paths must be inside the session repo_path. Returns before/after sha256 hashes and whether the target changed.",
501
+ inputSchema: {
502
+ type: "object",
503
+ properties: {
504
+ session_id: { type: "string", description: "Direct session ID" },
505
+ source_path: { type: "string", description: "Relative path to source file within repo" },
506
+ target_path: { type: "string", description: "Relative path to target file within repo" },
507
+ expected_source_sha256: { type: "string", description: "Optional: expected sha256 of source file" },
508
+ expected_target_sha256: { type: "string", description: "Optional: expected sha256 of target file before copy" },
509
+ },
510
+ required: ["session_id", "source_path", "target_path"],
511
+ },
512
+ });
357
513
  // run_task: only available when explicitly enabled
358
514
  if (config.enableRunTaskTool === true) {
359
515
  tools.push({
@@ -369,7 +525,7 @@ export function getToolDefs() {
369
525
  });
370
526
  }
371
527
  const profile = resolveToolProfile(config.toolProfile);
372
- const selected = selectToolsForProfile(tools, profile);
528
+ const selected = selectToolsForProfile(tools, profile, config.enableDirectProfile);
373
529
  buildToolCatalogSnapshot(selected, profile);
374
530
  return selected;
375
531
  }
@@ -379,12 +535,33 @@ export function getToolCatalogSnapshot() {
379
535
  return buildToolCatalogSnapshot(tools, resolveToolProfile(config.toolProfile));
380
536
  }
381
537
  // ── Request handler ───────────────────────────────────────────────
538
+ function guardDirectProfileEnabled() {
539
+ const config = getConfig();
540
+ if (!config.enableDirectProfile) {
541
+ throw new PatchWardenError("direct_profile_disabled", "Direct profile is disabled by local config.", "Set enableDirectProfile: true in patchwarden.config.json to use Direct session tools.", true, { operation: "direct_tool_call" });
542
+ }
543
+ }
382
544
  export async function handleToolCall(name, args) {
545
+ const startTime = Date.now();
546
+ const taskId = args?.task_id ? String(args.task_id) : args?.session_id ? String(args.session_id) : undefined;
547
+ try {
548
+ const result = await handleToolCallInternal(name, args);
549
+ logger.audit(name, true, Date.now() - startTime, undefined, taskId);
550
+ return result;
551
+ }
552
+ catch (err) {
553
+ const errorReason = err instanceof Error ? err.message : String(err);
554
+ logger.audit(name, false, Date.now() - startTime, errorReason, taskId);
555
+ throw err;
556
+ }
557
+ }
558
+ async function handleToolCallInternal(name, args) {
383
559
  switch (name) {
384
560
  case "save_plan": {
385
561
  return toResult(savePlan({
386
562
  title: String(args?.title ?? ""),
387
- content: String(args?.content ?? ""),
563
+ content: args?.content !== undefined ? String(args.content) : "",
564
+ plan_ref: args?.plan_ref ? String(args.plan_ref) : undefined,
388
565
  }));
389
566
  }
390
567
  case "get_plan": {
@@ -407,6 +584,8 @@ export async function handleToolCall(name, args) {
407
584
  timeout_seconds: args?.timeout_seconds !== undefined
408
585
  ? Number(args.timeout_seconds)
409
586
  : undefined,
587
+ execution_mode: args?.execution_mode === "assess_only" ? "assess_only" : "execute",
588
+ assessment_id: args?.assessment_id ? String(args.assessment_id) : undefined,
410
589
  }));
411
590
  }
412
591
  case "get_task_status": {
@@ -428,7 +607,11 @@ export async function handleToolCall(name, args) {
428
607
  return toResult(listWorkspace(args?.path ? String(args.path) : undefined));
429
608
  }
430
609
  case "read_workspace_file": {
431
- return toResult(readWorkspaceFile(String(args?.path ?? "")));
610
+ const sessionId = args?.session_id ? String(args.session_id) : undefined;
611
+ return toResult(readWorkspaceFile({
612
+ path: String(args?.path ?? ""),
613
+ session_id: sessionId,
614
+ }));
432
615
  }
433
616
  case "list_tasks": {
434
617
  return toResult(listTasks({
@@ -472,11 +655,17 @@ export async function handleToolCall(name, args) {
472
655
  return toResult(await waitForTask(String(args?.task_id ?? ""), waitSeconds));
473
656
  }
474
657
  case "get_task_summary": {
475
- return toResult(getTaskSummary(String(args?.task_id ?? "")));
658
+ return toResult(getTaskSummary(String(args?.task_id ?? ""), {
659
+ view: normalizeSummaryView(args?.view),
660
+ max_items: args?.max_items !== undefined ? Number(args.max_items) : undefined,
661
+ }));
476
662
  }
477
663
  case "audit_task": {
478
664
  return toResult(auditTask(String(args?.task_id ?? "")));
479
665
  }
666
+ case "safe_status": {
667
+ return toResult(safeStatus(String(args?.task_id ?? "")));
668
+ }
480
669
  case "run_task": {
481
670
  const config = getConfig();
482
671
  if (config.enableRunTaskTool !== true) {
@@ -486,6 +675,60 @@ export async function handleToolCall(name, args) {
486
675
  const result = await runTask(taskId);
487
676
  return toResult(result);
488
677
  }
678
+ case "create_direct_session": {
679
+ guardDirectProfileEnabled();
680
+ return toResult(createDirectSession({
681
+ repo_path: String(args?.repo_path ?? ""),
682
+ title: args?.title ? String(args.title) : undefined,
683
+ }));
684
+ }
685
+ case "search_workspace": {
686
+ guardDirectProfileEnabled();
687
+ return toResult(searchWorkspace({
688
+ session_id: String(args?.session_id ?? ""),
689
+ query: String(args?.query ?? ""),
690
+ max_results: args?.max_results ? Number(args.max_results) : undefined,
691
+ case_sensitive: args?.case_sensitive !== undefined ? Boolean(args.case_sensitive) : undefined,
692
+ max_preview_chars: args?.max_preview_chars ? Number(args.max_preview_chars) : undefined,
693
+ include_globs: Array.isArray(args?.include_globs) ? args.include_globs.map(String) : undefined,
694
+ }));
695
+ }
696
+ case "apply_patch": {
697
+ guardDirectProfileEnabled();
698
+ return toResult(applyPatch({
699
+ session_id: String(args?.session_id ?? ""),
700
+ path: String(args?.path ?? ""),
701
+ expected_sha256: String(args?.expected_sha256 ?? ""),
702
+ operations: Array.isArray(args?.operations) ? args.operations : [],
703
+ }));
704
+ }
705
+ case "run_verification": {
706
+ guardDirectProfileEnabled();
707
+ return toResult(await runVerification({
708
+ session_id: String(args?.session_id ?? ""),
709
+ command: String(args?.command ?? ""),
710
+ timeout_seconds: args?.timeout_seconds ? Number(args.timeout_seconds) : undefined,
711
+ }));
712
+ }
713
+ case "finalize_direct_session": {
714
+ guardDirectProfileEnabled();
715
+ return toResult(finalizeDirectSession({
716
+ session_id: String(args?.session_id ?? ""),
717
+ }));
718
+ }
719
+ case "audit_session": {
720
+ guardDirectProfileEnabled();
721
+ return toResult(auditSession({
722
+ session_id: String(args?.session_id ?? ""),
723
+ }));
724
+ }
725
+ case "sync_file": {
726
+ guardDirectProfileEnabled();
727
+ return toResult(syncFile(String(args?.session_id ?? ""), String(args?.source_path ?? ""), String(args?.target_path ?? ""), {
728
+ expected_source_sha256: args?.expected_source_sha256 ? String(args.expected_source_sha256) : undefined,
729
+ expected_target_sha256: args?.expected_target_sha256 ? String(args.expected_target_sha256) : undefined,
730
+ }));
731
+ }
489
732
  default:
490
733
  throw new Error(`Unknown tool: ${name}`);
491
734
  }
@@ -499,6 +742,14 @@ function normalizeWaitSeconds(args) {
499
742
  const value = preferred ?? legacy;
500
743
  return value === undefined ? undefined : Number(value);
501
744
  }
745
+ function normalizeSummaryView(value) {
746
+ if (value === undefined)
747
+ return "standard";
748
+ if (value !== "compact" && value !== "standard") {
749
+ throw new Error('view must be "compact" or "standard".');
750
+ }
751
+ return value;
752
+ }
502
753
  function toResult(data) {
503
754
  return {
504
755
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -547,7 +798,7 @@ export function registerTools(server) {
547
798
  arguments: { detail: "self_diagnostic" },
548
799
  },
549
800
  connector_refresh_steps: [
550
- "1. Run Check-PatchWarden-Health.cmd locally to confirm the active profile and manifest hash.",
801
+ "1. Run PatchWarden.cmd health locally to confirm the active profile and manifest hash.",
551
802
  "2. In ChatGPT Platform, refresh or reconnect the Connector (do not reuse an old session).",
552
803
  "3. Open a NEW ChatGPT conversation; old conversations retain their cached tool catalog.",
553
804
  "4. Call health_check in the new conversation and verify tool_manifest_sha256 matches the local report.",
@@ -0,0 +1,16 @@
1
+ export interface RunVerificationInput {
2
+ session_id: string;
3
+ command: string;
4
+ timeout_seconds?: number;
5
+ }
6
+ export interface RunVerificationOutput {
7
+ command: string;
8
+ exit_code: number | null;
9
+ passed: boolean;
10
+ timed_out: boolean;
11
+ stdout_tail: string;
12
+ stderr_tail: string;
13
+ log_path: string;
14
+ next_action: string;
15
+ }
16
+ export declare function runVerification(input: RunVerificationInput): Promise<RunVerificationOutput>;
@@ -0,0 +1,32 @@
1
+ import { readDirectSession, appendDirectSessionVerificationRun, } from "../direct/directSessionStore.js";
2
+ import { guardDirectSessionActive } from "../direct/directGuards.js";
3
+ import { runDirectVerification } from "../direct/directVerification.js";
4
+ export async function runVerification(input) {
5
+ // 1. Read session and guard active
6
+ const session = readDirectSession(input.session_id);
7
+ guardDirectSessionActive(session);
8
+ // 2. Call runDirectVerification with command, resolvedRepoPath,
9
+ // sessionId, and timeoutSeconds
10
+ const timeoutSeconds = input.timeout_seconds ?? 120;
11
+ const result = await runDirectVerification({
12
+ command: input.command,
13
+ resolvedRepoPath: session.resolved_repo_path,
14
+ sessionId: input.session_id,
15
+ timeoutSeconds,
16
+ });
17
+ // 3. Append verification run to session
18
+ appendDirectSessionVerificationRun(input.session_id, result.run);
19
+ // 4. Return result
20
+ return {
21
+ command: result.run.command,
22
+ exit_code: result.run.exit_code,
23
+ passed: result.run.passed,
24
+ timed_out: result.run.timed_out,
25
+ stdout_tail: result.run.stdout_tail,
26
+ stderr_tail: result.run.stderr_tail,
27
+ log_path: result.run.log_path,
28
+ next_action: result.run.passed
29
+ ? "Call finalize_direct_session to complete the session."
30
+ : "Review the verification output and apply_patch to fix issues.",
31
+ };
32
+ }
@@ -0,0 +1,19 @@
1
+ import { type PatchWardenConfig } from "../config.js";
2
+ import type { TaskStatus, TaskPhase } from "./createTask.js";
3
+ export interface SafeStatusOutput {
4
+ task_id: string;
5
+ status: TaskStatus | "not_found";
6
+ phase: TaskPhase | null;
7
+ created_at: string | null;
8
+ started_at: string | null;
9
+ updated_at: string | null;
10
+ finished_at: string | null;
11
+ last_heartbeat_at: string | null;
12
+ current_command: string | null;
13
+ verify_status: "passed" | "failed" | "skipped" | null;
14
+ artifact_status: string | null;
15
+ watcher_state: string | null;
16
+ error_code: string | null;
17
+ error_summary: string | null;
18
+ }
19
+ export declare function safeStatus(taskId: string, config?: PatchWardenConfig): SafeStatusOutput;
@@ -0,0 +1,72 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import { getTasksDir, getConfig } from "../config.js";
4
+ import { guardReadPath } from "../security/pathGuard.js";
5
+ import { guardSensitivePath } from "../security/sensitiveGuard.js";
6
+ import { readTaskRuntime } from "../taskRuntime.js";
7
+ import { readWatcherStatus } from "../watcherStatus.js";
8
+ export function safeStatus(taskId, config) {
9
+ const cfg = config || getConfig();
10
+ const tasksDir = getTasksDir(cfg);
11
+ const taskDir = resolve(tasksDir, taskId);
12
+ const statusFile = join(taskDir, "status.json");
13
+ // Check existence BEFORE guardReadPath so that a non-existent task
14
+ // returns a structured not_found response instead of throwing.
15
+ if (!existsSync(statusFile)) {
16
+ return {
17
+ task_id: taskId,
18
+ status: "not_found",
19
+ phase: null,
20
+ created_at: null,
21
+ started_at: null,
22
+ updated_at: null,
23
+ finished_at: null,
24
+ last_heartbeat_at: null,
25
+ current_command: null,
26
+ verify_status: null,
27
+ artifact_status: null,
28
+ watcher_state: null,
29
+ error_code: null,
30
+ error_summary: null,
31
+ };
32
+ }
33
+ guardReadPath(statusFile, cfg.workspaceRoot, cfg.tasksDir);
34
+ guardSensitivePath(statusFile);
35
+ const raw = readFileSync(statusFile, "utf-8");
36
+ const status = JSON.parse(raw);
37
+ const runtime = readTaskRuntime(taskDir);
38
+ const phase = (runtime.phase || status.phase || "queued");
39
+ const watcher = readWatcherStatus(cfg);
40
+ // Extract short error summary — never the full error text
41
+ let errorCode = null;
42
+ let errorSummary = null;
43
+ if (status.error && typeof status.error === "string") {
44
+ const errorStr = status.error;
45
+ // Keep only the first 200 chars as summary
46
+ errorSummary = errorStr.length > 200 ? errorStr.slice(0, 200) + "..." : errorStr;
47
+ // Try to extract an error code from the status
48
+ if (typeof status.status === "string" && status.status.startsWith("failed")) {
49
+ errorCode = status.status;
50
+ }
51
+ }
52
+ else if (typeof status.status === "string" && status.status.startsWith("failed")) {
53
+ errorCode = status.status;
54
+ errorSummary = `Task ended with status: ${status.status}`;
55
+ }
56
+ return {
57
+ task_id: taskId,
58
+ status: status.status || "not_found",
59
+ phase,
60
+ created_at: typeof status.created_at === "string" ? status.created_at : null,
61
+ started_at: typeof status.started_at === "string" ? status.started_at : null,
62
+ updated_at: typeof status.updated_at === "string" ? status.updated_at : null,
63
+ finished_at: typeof status.finished_at === "string" ? status.finished_at : null,
64
+ last_heartbeat_at: runtime.last_heartbeat_at || (typeof status.last_heartbeat_at === "string" ? status.last_heartbeat_at : null),
65
+ current_command: runtime.current_command ?? (typeof status.current_command === "string" ? status.current_command : null) ?? null,
66
+ verify_status: typeof status.verify_status === "string" ? status.verify_status : null,
67
+ artifact_status: typeof status.artifact_status === "string" ? status.artifact_status : null,
68
+ watcher_state: watcher.status,
69
+ error_code: errorCode,
70
+ error_summary: errorSummary,
71
+ };
72
+ }
@@ -1,6 +1,7 @@
1
1
  export interface SavePlanInput {
2
2
  title: string;
3
3
  content: string;
4
+ plan_ref?: string;
4
5
  }
5
6
  export interface SavePlanOutput {
6
7
  plan_id: string;
@@ -1,24 +1,55 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { resolve, join } from "node:path";
1
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
2
+ import { resolve, join, relative } from "node:path";
3
3
  import { getPlansDir, getConfig } from "../config.js";
4
4
  import { guardPath } from "../security/pathGuard.js";
5
5
  import { guardPlanContent } from "../security/planGuard.js";
6
+ import { PatchWardenError } from "../errors.js";
6
7
  export function savePlan(input) {
7
8
  const config = getConfig();
8
9
  const plansDir = getPlansDir(config);
9
- guardPlanContent(input.title, input.content);
10
- const planId = `plan_${Date.now()}_${sanitizeTitle(input.title)}`;
10
+ let content = input.content || "";
11
+ let title = input.title || "";
12
+ if (input.plan_ref) {
13
+ // 收缩 #3: plan_ref 只能读取 .patchwarden/plans 内文件,禁止任意路径
14
+ // plan_ref is relative to plansDir, not workspaceRoot
15
+ const targetPath = resolve(plansDir, input.plan_ref);
16
+ // Guard: must stay inside workspace and inside plansDir
17
+ guardPath(targetPath, config.workspaceRoot, config.plansDir);
18
+ const relativeToPlans = relative(plansDir, targetPath);
19
+ if (relativeToPlans.startsWith("..")) {
20
+ throw new PatchWardenError("plan_ref_outside_plans_dir", `plan_ref must point to a file inside .patchwarden/plans.`, "Use a path relative to .patchwarden/plans/.", true, { plan_ref: input.plan_ref, resolved: targetPath });
21
+ }
22
+ if (!existsSync(targetPath)) {
23
+ throw new PatchWardenError("plan_ref_not_found", `plan_ref file not found: "${input.plan_ref}".`, "Place the plan file under .patchwarden/plans/ first, then reference it.", true, { plan_ref: input.plan_ref });
24
+ }
25
+ content = readFileSync(targetPath, "utf-8");
26
+ if (!title || title.trim() === "") {
27
+ title = "Plan from file";
28
+ }
29
+ }
30
+ else {
31
+ // Without plan_ref, content is required
32
+ if (!content || content.trim() === "") {
33
+ throw new PatchWardenError("plan_content_required", "save_plan requires content or plan_ref.", "Pass content with the plan text, or use plan_ref to load a file from .patchwarden/plans.");
34
+ }
35
+ }
36
+ // Default title when empty
37
+ if (!title || title.trim() === "") {
38
+ title = "Inline plan";
39
+ }
40
+ guardPlanContent(title, content);
41
+ const planId = `plan_${Date.now()}_${sanitizeTitle(title)}`;
11
42
  const planDir = resolve(plansDir, planId);
12
43
  // Guards: plan dir & file must stay inside workspace
13
44
  guardPath(planDir, config.workspaceRoot, config.plansDir);
14
45
  mkdirSync(planDir, { recursive: true });
15
46
  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");
47
+ const header = `# ${title}\n\n> Plan ID: ${planId}\n> Created: ${new Date().toISOString()}\n\n`;
48
+ writeFileSync(planFile, header + content, "utf-8");
18
49
  return {
19
50
  plan_id: planId,
20
51
  path: planFile,
21
- title: input.title,
52
+ title: title,
22
53
  };
23
54
  }
24
55
  function sanitizeTitle(title) {